/* * Copyright (C) 2012 - 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 "folderview.h" #include "foldermodel.h" #include #include #include #include "proxyfoldermodel.h" #include "folderitemdelegate.h" #include "dndactionmenu.h" #include "filemenu.h" #include "foldermenu.h" #include "filelauncher.h" #include #include #include #include #include #include #include #include #include // for XDS support #include // for XDS support #include "xdndworkaround.h" // for XDS support #include "path.h" #include "folderview_p.h" Q_DECLARE_OPAQUE_POINTER(FmFileInfo*) using namespace Fm; FolderViewListView::FolderViewListView(QWidget* parent): QListView(parent), activationAllowed_(true) { connect(this, &QListView::activated, this, &FolderViewListView::activation); } FolderViewListView::~FolderViewListView() { } void FolderViewListView::startDrag(Qt::DropActions supportedActions) { if(movement() != Static) QListView::startDrag(supportedActions); else QAbstractItemView::startDrag(supportedActions); } void FolderViewListView::mousePressEvent(QMouseEvent* event) { QListView::mousePressEvent(event); static_cast(parent())->childMousePressEvent(event); } QModelIndex FolderViewListView::indexAt(const QPoint& point) const { QModelIndex index = QListView::indexAt(point); // NOTE: QListView has a severe design flaw here. It does hit-testing based on the // total bound rect of the item. The width of an item is determined by max(icon_width, text_width). // So if the text label is much wider than the icon, when you click outside the icon but // the point is still within the outer bound rect, the item is still selected. // This results in very poor usability. Let's do precise hit-testing here. // An item is hit only when the point is in the icon or text label. // If the point is in the bound rectangle but outside the icon or text, it should not be selected. if(viewMode() == QListView::IconMode && index.isValid()) { // FIXME: this hack only improves the usability partially. We still need more precise sizeHint handling. // FolderItemDelegate* delegate = static_cast(itemDelegateForColumn(FolderModel::ColumnFileName)); // Q_ASSERT(delegate != nullptr); // We use the grid size - (2, 2) as the size of the bounding rectangle of the whole item. // The width of the text label hence is gridSize.width - 2, and the width and height of the icon is from iconSize(). QRect visRect = visualRect(index); // visibal area on the screen QSize itemSize = gridSize(); itemSize.setWidth(itemSize.width() - 2); itemSize.setHeight(itemSize.height() - 2); QSize _iconSize = iconSize(); int textHeight = itemSize.height() - _iconSize.height(); if(point.y() < visRect.bottom() - textHeight) { // the point is in the icon area, not over the text label int iconXMargin = (itemSize.width() - _iconSize.width()) / 2; if(point.x() < (visRect.left() + iconXMargin) || point.x() > (visRect.right() - iconXMargin)) return QModelIndex(); } // qDebug() << "visualRect: " << visRect << "point:" << point; } return index; } // NOTE: // QListView has a problem which I consider a bug or a design flaw. // When you set movement property to Static, theoratically the icons // should not be movable. However, if you turned on icon mode, // the icons becomes freely movable despite the value of movement is Static. // To overcome this bug, we override all drag handling methods, and // call QAbstractItemView directly, bypassing QListView. // In this way, we can workaround the buggy behavior. // The drag handlers of QListView basically does the same things // as its parent QAbstractItemView, but it also stores the currently // dragged item and paint them in the view as needed. // TODO: I really should file a bug report to Qt developers. void FolderViewListView::dragEnterEvent(QDragEnterEvent* event) { if(movement() != Static) QListView::dragEnterEvent(event); else QAbstractItemView::dragEnterEvent(event); qDebug("dragEnterEvent"); //static_cast(parent())->childDragEnterEvent(event); } void FolderViewListView::dragLeaveEvent(QDragLeaveEvent* e) { if(movement() != Static) QListView::dragLeaveEvent(e); else QAbstractItemView::dragLeaveEvent(e); static_cast(parent())->childDragLeaveEvent(e); } void FolderViewListView::dragMoveEvent(QDragMoveEvent* e) { if(movement() != Static) QListView::dragMoveEvent(e); else QAbstractItemView::dragMoveEvent(e); static_cast(parent())->childDragMoveEvent(e); } void FolderViewListView::dropEvent(QDropEvent* e) { static_cast(parent())->childDropEvent(e); if(movement() != Static) QListView::dropEvent(e); else QAbstractItemView::dropEvent(e); } void FolderViewListView::mouseReleaseEvent(QMouseEvent* event) { bool activationWasAllowed = activationAllowed_; if ((!style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) { activationAllowed_ = false; } QListView::mouseReleaseEvent(event); activationAllowed_ = activationWasAllowed; } void FolderViewListView::mouseDoubleClickEvent(QMouseEvent* event) { bool activationWasAllowed = activationAllowed_; if ((style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) { activationAllowed_ = false; } QListView::mouseDoubleClickEvent(event); activationAllowed_ = activationWasAllowed; } void FolderViewListView::activation(const QModelIndex &index) { if (activationAllowed_) { Q_EMIT activatedFiltered(index); } } //----------------------------------------------------------------------------- FolderViewTreeView::FolderViewTreeView(QWidget* parent): QTreeView(parent), doingLayout_(false), layoutTimer_(nullptr), activationAllowed_(true) { header()->setStretchLastSection(true); setIndentation(0); connect(this, &QTreeView::activated, this, &FolderViewTreeView::activation); } FolderViewTreeView::~FolderViewTreeView() { if(layoutTimer_) delete layoutTimer_; } void FolderViewTreeView::setModel(QAbstractItemModel* model) { QTreeView::setModel(model); layoutColumns(); if(ProxyFolderModel* proxyModel = qobject_cast(model)) { connect(proxyModel, &ProxyFolderModel::sortFilterChanged, this, &FolderViewTreeView::onSortFilterChanged, Qt::UniqueConnection); onSortFilterChanged(); } } void FolderViewTreeView::mousePressEvent(QMouseEvent* event) { QTreeView::mousePressEvent(event); static_cast(parent())->childMousePressEvent(event); } void FolderViewTreeView::dragEnterEvent(QDragEnterEvent* event) { QTreeView::dragEnterEvent(event); static_cast(parent())->childDragEnterEvent(event); } void FolderViewTreeView::dragLeaveEvent(QDragLeaveEvent* e) { QTreeView::dragLeaveEvent(e); static_cast(parent())->childDragLeaveEvent(e); } void FolderViewTreeView::dragMoveEvent(QDragMoveEvent* e) { QTreeView::dragMoveEvent(e); static_cast(parent())->childDragMoveEvent(e); } void FolderViewTreeView::dropEvent(QDropEvent* e) { static_cast(parent())->childDropEvent(e); QTreeView::dropEvent(e); } // the default list mode of QListView handles column widths // very badly (worse than gtk+) and it's not very flexible. // so, let's handle column widths outselves. void FolderViewTreeView::layoutColumns() { // qDebug("layoutColumns"); if(!model()) return; doingLayout_ = true; QHeaderView* headerView = header(); // the width that's available for showing the columns. int availWidth = viewport()->contentsRect().width(); int desiredWidth = 0; // get the width that every column want int numCols = headerView->count(); if(numCols > 0) { int* widths = new int[numCols]; // array to store the widths every column needs int column; for(column = 0; column < numCols; ++column) { int columnId = headerView->logicalIndex(column); // get the size that the column needs widths[column] = sizeHintForColumn(columnId); // compute the total width needed desiredWidth += widths[column]; } int filenameColumn = headerView->visualIndex(FolderModel::ColumnFileName); // if the total witdh we want exceeds the available space if(desiredWidth > availWidth) { // Compute the width available for the filename column int filenameAvailWidth = availWidth - desiredWidth + widths[filenameColumn]; // Compute the minimum acceptable width for the filename column int filenameMinWidth = qMin(200, sizeHintForColumn(filenameColumn)); if (filenameAvailWidth > filenameMinWidth) { // Shrink the filename column to the available width widths[filenameColumn] = filenameAvailWidth; } else { // Set the filename column to its minimum width widths[filenameColumn] = filenameMinWidth; } } else { // Fill the extra available space with the filename column widths[filenameColumn] += availWidth - desiredWidth; } // really do the resizing for every column for(int column = 0; column < numCols; ++column) { headerView->resizeSection(column, widths[column]); } delete []widths; } doingLayout_ = false; if(layoutTimer_) { delete layoutTimer_; layoutTimer_ = nullptr; } } void FolderViewTreeView::resizeEvent(QResizeEvent* event) { QAbstractItemView::resizeEvent(event); // prevent endless recursion. // When manually resizing columns, at the point where a horizontal scroll // bar has to be inserted or removed, the vertical size changes, a resize // event occurs and the column headers are flickering badly if the column // layout is modified at this point. Therefore only layout the columns if // the horizontal size changes. if(!doingLayout_ && event->size().width() != event->oldSize().width()) layoutColumns(); // layoutColumns() also triggers resizeEvent } void FolderViewTreeView::rowsInserted(const QModelIndex& parent, int start, int end) { QTreeView::rowsInserted(parent, start, end); queueLayoutColumns(); } void FolderViewTreeView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { QTreeView::rowsAboutToBeRemoved(parent, start, end); queueLayoutColumns(); } void FolderViewTreeView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { QTreeView::dataChanged(topLeft, bottomRight); // FIXME: this will be very inefficient // queueLayoutColumns(); } void FolderViewTreeView::reset() { // Sometimes when the content of the model is radically changed, Qt does reset() // on the model rather than doing large amount of insertion and deletion. // This is for performance reason so in this case rowsInserted() and rowsAboutToBeRemoved() // might not be called. Hence we also have to re-layout the columns when the model is reset. // This fixes bug #190 // https://github.com/lxde/pcmanfm-qt/issues/190 QTreeView::reset(); queueLayoutColumns(); } void FolderViewTreeView::queueLayoutColumns() { // qDebug("queueLayoutColumns"); if(!layoutTimer_) { layoutTimer_ = new QTimer(); layoutTimer_->setSingleShot(true); layoutTimer_->setInterval(0); connect(layoutTimer_, &QTimer::timeout, this, &FolderViewTreeView::layoutColumns); } layoutTimer_->start(); } void FolderViewTreeView::mouseReleaseEvent(QMouseEvent* event) { bool activationWasAllowed = activationAllowed_; if ((!style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) { activationAllowed_ = false; } QTreeView::mouseReleaseEvent(event); activationAllowed_ = activationWasAllowed; } void FolderViewTreeView::mouseDoubleClickEvent(QMouseEvent* event) { bool activationWasAllowed = activationAllowed_; if ((style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) { activationAllowed_ = false; } QTreeView::mouseDoubleClickEvent(event); activationAllowed_ = activationWasAllowed; } void FolderViewTreeView::activation(const QModelIndex &index) { if (activationAllowed_) { Q_EMIT activatedFiltered(index); } } void FolderViewTreeView::onSortFilterChanged() { if(QSortFilterProxyModel* proxyModel = qobject_cast(model())) { header()->setSortIndicatorShown(true); header()->setSortIndicator(proxyModel->sortColumn(), proxyModel->sortOrder()); if (!isSortingEnabled()) { setSortingEnabled(true); } } } //----------------------------------------------------------------------------- FolderView::FolderView(ViewMode _mode, QWidget* parent): QWidget(parent), view(nullptr), model_(nullptr), mode((ViewMode)0), fileLauncher_(nullptr), autoSelectionDelay_(600), autoSelectionTimer_(nullptr), selChangedTimer_(nullptr), itemDelegateMargins_(QSize(3, 3)) { iconSize_[IconMode - FirstViewMode] = QSize(48, 48); iconSize_[CompactMode - FirstViewMode] = QSize(24, 24); iconSize_[ThumbnailMode - FirstViewMode] = QSize(128, 128); iconSize_[DetailedListMode - FirstViewMode] = QSize(24, 24); QVBoxLayout* layout = new QVBoxLayout(); layout->setMargin(0); setLayout(layout); setViewMode(_mode); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); connect(this, &FolderView::clicked, this, &FolderView::onFileClicked); } FolderView::~FolderView() { } void FolderView::onItemActivated(QModelIndex index) { if(index.isValid() && index.model()) { QVariant data = index.model()->data(index, FolderModel::FileInfoRole); FmFileInfo* info = (FmFileInfo*)data.value(); if(info) { if (!(QApplication::keyboardModifiers() & (Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))) { Q_EMIT clicked(ActivatedClick, info); } } } } void FolderView::onSelChangedTimeout() { selChangedTimer_->deleteLater(); selChangedTimer_ = nullptr; QItemSelectionModel* selModel = selectionModel(); int nSel = 0; if(viewMode() == DetailedListMode) nSel = selModel->selectedRows().count(); else { nSel = selModel->selectedIndexes().count(); } // qDebug()<<"selected:" << nSel; Q_EMIT selChanged(nSel); // FIXME: this is inefficient } void FolderView::onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { // It's possible that the selected items change too often and this slot gets called for thousands of times. // For example, when you select thousands of files and delete them, we will get one selectionChanged() event // for every deleted file. So, we use a timer to delay the handling to avoid too frequent updates of the UI. if(!selChangedTimer_) { selChangedTimer_ = new QTimer(this); selChangedTimer_->setSingleShot(true); connect(selChangedTimer_, &QTimer::timeout, this, &FolderView::onSelChangedTimeout); selChangedTimer_->start(200); } } void FolderView::setViewMode(ViewMode _mode) { if(_mode == mode) // if it's the same more, ignore return; // FIXME: retain old selection // since only detailed list mode uses QTreeView, and others // all use QListView, it's wise to preserve QListView when possible. bool recreateView = false; if(view && (mode == DetailedListMode || _mode == DetailedListMode)) { delete view; // FIXME: no virtual dtor? view = nullptr; recreateView = true; } mode = _mode; QSize iconSize = iconSize_[mode - FirstViewMode]; if(mode == DetailedListMode) { FolderViewTreeView* treeView = new FolderViewTreeView(this); connect(treeView, &FolderViewTreeView::activatedFiltered, this, &FolderView::onItemActivated); setFocusProxy(treeView); view = treeView; treeView->setItemsExpandable(false); treeView->setRootIsDecorated(false); treeView->setAllColumnsShowFocus(false); // set our own custom delegate FolderItemDelegate* delegate = new FolderItemDelegate(treeView); treeView->setItemDelegateForColumn(FolderModel::ColumnFileName, delegate); } else { FolderViewListView* listView; if(view) listView = static_cast(view); else { listView = new FolderViewListView(this); connect(listView, &FolderViewListView::activatedFiltered, this, &FolderView::onItemActivated); view = listView; } setFocusProxy(listView); // set our own custom delegate FolderItemDelegate* delegate = new FolderItemDelegate(listView); listView->setItemDelegateForColumn(FolderModel::ColumnFileName, delegate); // FIXME: should we expose the delegate? listView->setMovement(QListView::Static); /* If listView is already visible, setMovement() will lay out items again with delay (see Qt, QListView::setMovement(), d->doDelayedItemsLayout()) and thus drop events will remain disabled for the viewport. So, we should re-enable drop events here. */ if(listView->viewport()->isVisible()) listView->viewport()->setAcceptDrops(true); listView->setResizeMode(QListView::Adjust); listView->setWrapping(true); switch(mode) { case IconMode: { listView->setViewMode(QListView::IconMode); listView->setWordWrap(true); listView->setFlow(QListView::LeftToRight); break; } case CompactMode: { listView->setViewMode(QListView::ListMode); listView->setWordWrap(false); listView->setFlow(QListView::QListView::TopToBottom); break; } case ThumbnailMode: { listView->setViewMode(QListView::IconMode); listView->setWordWrap(true); listView->setFlow(QListView::LeftToRight); break; } default:; } updateGridSize(); } if(view) { // we have to install the event filter on the viewport instead of the view itself. view->viewport()->installEventFilter(this); // we want the QEvent::HoverMove event for single click + auto-selection support view->viewport()->setAttribute(Qt::WA_Hover, true); view->setContextMenuPolicy(Qt::NoContextMenu); // defer the context menu handling to parent widgets view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); view->setIconSize(iconSize); view->setSelectionMode(QAbstractItemView::ExtendedSelection); layout()->addWidget(view); // enable dnd view->setDragEnabled(true); view->setAcceptDrops(true); view->setDragDropMode(QAbstractItemView::DragDrop); view->setDropIndicatorShown(true); if(model_) { // FIXME: preserve selections model_->setThumbnailSize(iconSize.width()); view->setModel(model_); if(recreateView) connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FolderView::onSelectionChanged); } } } // set proper grid size for the QListView based on current view mode, icon size, and font size. void FolderView::updateGridSize() { if(mode == DetailedListMode || !view) return; FolderViewListView* listView = static_cast(view); QSize icon = iconSize(mode); // size of the icon QFontMetrics fm = fontMetrics(); // size of current font QSize grid; // the final grid size switch(mode) { case IconMode: case ThumbnailMode: { // NOTE by PCMan about finding the optimal text label size: // The average filename length on my root filesystem is roughly 18-20 chars. // So, a reasonable size for the text label is about 10 chars each line since string of this length // can be shown in two lines. If you consider word wrap, then the result is around 10 chars per word. // In average, 10 char per line should be enough to display a "word" in the filename without breaking. // The values can be estimated with this command: // > find / | xargs basename -a | sed -e s'/[_-]/ /g' | wc -mcw // However, this average only applies to English. For some Asian characters, such as Chinese chars, // each char actually takes doubled space. To be safe, we use 13 chars per line x average char width // to get a nearly optimal width for the text label. As most of the filenames have less than 40 chars // 13 chars x 3 lines should be enough to show the full filenames for most files. int textWidth = fm.averageCharWidth() * 13; int textHeight = fm.lineSpacing() * 3; grid.setWidth(qMax(icon.width(), textWidth) + 4); // a margin of 2 px for selection rects grid.setHeight(icon.height() + textHeight + 4); // a margin of 2 px for selection rects break; } default: ; // do not use grid size } if(mode == IconMode || mode == ThumbnailMode) listView->setGridSize(grid + 2 * itemDelegateMargins_); // the default spacing is 6(=2x3) px else listView->setGridSize(grid); FolderItemDelegate* delegate = static_cast(listView->itemDelegateForColumn(FolderModel::ColumnFileName)); delegate->setGridSize(grid); } void FolderView::setIconSize(ViewMode mode, QSize size) { Q_ASSERT(mode >= FirstViewMode && mode <= LastViewMode); iconSize_[mode - FirstViewMode] = size; if(viewMode() == mode) { view->setIconSize(size); if(model_) model_->setThumbnailSize(size.width()); updateGridSize(); } } QSize FolderView::iconSize(ViewMode mode) const { Q_ASSERT(mode >= FirstViewMode && mode <= LastViewMode); return iconSize_[mode - FirstViewMode]; } void FolderView::setMargins(QSize size) { if (itemDelegateMargins_ != size.expandedTo(QSize(0, 0))) { itemDelegateMargins_ = size.expandedTo(QSize(0, 0)); updateGridSize(); } } FolderView::ViewMode FolderView::viewMode() const { return mode; } void FolderView::setAutoSelectionDelay(int delay) { autoSelectionDelay_ = delay; } QAbstractItemView* FolderView::childView() const { return view; } ProxyFolderModel* FolderView::model() const { return model_; } void FolderView::setModel(ProxyFolderModel* model) { if(view) { view->setModel(model); QSize iconSize = iconSize_[mode - FirstViewMode]; model->setThumbnailSize(iconSize.width()); if(view->selectionModel()) connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FolderView::onSelectionChanged); } if(model_) delete model_; model_ = model; } bool FolderView::event(QEvent* event) { switch(event->type()) { case QEvent::StyleChange: break; case QEvent::FontChange: updateGridSize(); break; default: break; } return QWidget::event(event); } void FolderView::contextMenuEvent(QContextMenuEvent* event) { QWidget::contextMenuEvent(event); QPoint pos = event->pos(); QPoint view_pos = view->mapFromParent(pos); QPoint viewport_pos = view->viewport()->mapFromParent(view_pos); emitClickedAt(ContextMenuClick, viewport_pos); } void FolderView::childMousePressEvent(QMouseEvent* event) { // called from mousePressEvent() of child view Qt::MouseButton button = event->button(); if(button == Qt::MiddleButton) { emitClickedAt(MiddleClick, event->pos()); } else if (button == Qt::BackButton) { Q_EMIT clickedBack(); } else if (button == Qt::ForwardButton) { Q_EMIT clickedForward(); } } void FolderView::emitClickedAt(ClickType type, const QPoint& pos) { // indexAt() needs a point in "viewport" coordinates. QModelIndex index = view->indexAt(pos); if(index.isValid()) { QVariant data = index.data(FolderModel::FileInfoRole); FmFileInfo* info = reinterpret_cast(data.value()); Q_EMIT clicked(type, info); } else { // FIXME: should we show popup menu for the selected files instead // if there are selected files? if(type == ContextMenuClick) { // clear current selection if clicked outside selected files view->clearSelection(); Q_EMIT clicked(type, nullptr); } } } QModelIndexList FolderView::selectedRows(int column) const { QItemSelectionModel* selModel = selectionModel(); if(selModel) { return selModel->selectedRows(column); } return QModelIndexList(); } // This returns all selected "cells", which means all cells of the same row are returned. QModelIndexList FolderView::selectedIndexes() const { QItemSelectionModel* selModel = selectionModel(); if(selModel) { return selModel->selectedIndexes(); } return QModelIndexList(); } QItemSelectionModel* FolderView::selectionModel() const { return view ? view->selectionModel() : nullptr; } Fm::PathList FolderView::selectedFilePaths() const { if(model_) { QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes(); if(!selIndexes.isEmpty()) { PathList paths; QModelIndexList::const_iterator it; for(it = selIndexes.begin(); it != selIndexes.end(); ++it) { FmFileInfo* file = model_->fileInfoFromIndex(*it); paths.pushTail(fm_file_info_get_path(file)); } return paths; } } return nullptr; } QModelIndex FolderView::indexFromFolderPath(FmPath* folderPath) const { if(!model_ || !folderPath) return QModelIndex(); QModelIndex index; int count = model_->rowCount(); for(int row = 0; row < count; ++row) { index = model_->index(row, 0); FmFileInfo* info = model_->fileInfoFromIndex(index); if(info && fm_file_info_is_dir(info) && fm_path_equal(folderPath,fm_file_info_get_path(info))) return index; } return QModelIndex(); } Fm::FileInfoList FolderView::selectedFiles() const { if(model_) { QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes(); if(!selIndexes.isEmpty()) { FileInfoList files; QModelIndexList::const_iterator it; for(it = selIndexes.constBegin(); it != selIndexes.constEnd(); ++it) { FmFileInfo* file = model_->fileInfoFromIndex(*it); files.pushTail(file); } return files; } } return nullptr; } void FolderView::selectAll() { if(mode == DetailedListMode) view->selectAll(); else { // NOTE: By default QListView::selectAll() selects all columns in the model. // However, QListView only show the first column. Normal selection by mouse // can only select the first column of every row. I consider this discripancy yet // another design flaw of Qt. To make them consistent, we do it ourselves by only // selecting the first column of every row and do not select all columns as Qt does. // This will trigger one selectionChanged event per row, which is very inefficient, // but we have no other choices to workaround the Qt bug. // I'll report a Qt bug for this later. if(model_) { int rowCount = model_->rowCount(); for(int row = 0; row < rowCount; ++row) { QModelIndex index = model_->index(row, 0); selectionModel()->select(index, QItemSelectionModel::Select); } } } } void FolderView::invertSelection() { if(model_) { QItemSelectionModel* selModel = view->selectionModel(); int rows = model_->rowCount(); QItemSelectionModel::SelectionFlags flags = QItemSelectionModel::Toggle; if(mode == DetailedListMode) flags |= QItemSelectionModel::Rows; for(int row = 0; row < rows; ++row) { QModelIndex index = model_->index(row, 0); selModel->select(index, flags); } } } void FolderView::childDragEnterEvent(QDragEnterEvent* event) { qDebug("drag enter"); if(event->mimeData()->hasFormat("text/uri-list")) { event->accept(); } else event->ignore(); } void FolderView::childDragLeaveEvent(QDragLeaveEvent* e) { qDebug("drag leave"); e->accept(); } void FolderView::childDragMoveEvent(QDragMoveEvent* e) { qDebug("drag move"); } void FolderView::childDropEvent(QDropEvent* e) { // qDebug("drop"); // Try to support XDS // NOTE: in theory, it's not possible to implement XDS with pure Qt. // We achieved this with some dirty XCB/XDND workarounds. // Please refer to XdndWorkaround::clientMessage() in xdndworkaround.cpp for details. if(QX11Info::isPlatformX11() && e->mimeData()->hasFormat("XdndDirectSave0")) { e->setDropAction(Qt::CopyAction); const QWidget* targetWidget = childView()->viewport(); // these are dynamic QObject property set by our XDND workarounds in xworkaround.cpp. xcb_window_t dndSource = xcb_window_t(targetWidget->property("xdnd::lastDragSource").toUInt()); //xcb_timestamp_t dropTimestamp = (xcb_timestamp_t)targetWidget->property("xdnd::lastDropTime").toUInt(); // qDebug() << "XDS: source window" << dndSource << dropTimestamp; if(dndSource != 0) { xcb_atom_t XdndDirectSaveAtom = XdndWorkaround::internAtom("XdndDirectSave0", 15); xcb_atom_t textAtom = XdndWorkaround::internAtom("text/plain", 10); // 1. get the filename from XdndDirectSave property of the source window QByteArray basename = XdndWorkaround::windowProperty(dndSource, XdndDirectSaveAtom, textAtom, 1024); // 2. construct the fill URI for the file, and update the source window property. Path filePath = Path(path()).newChild(basename); QByteArray fileUri = filePath.toUri(); XdndWorkaround::setWindowProperty(dndSource, XdndDirectSaveAtom, textAtom, (void*)fileUri.constData(), fileUri.length()); // 3. send to XDS selection data request with type "XdndDirectSave" to the source window and // receive result from the source window. (S: success, E: error, or F: failure) QByteArray result = e->mimeData()->data("XdndDirectSave0"); // NOTE: there seems to be some bugs in file-roller so it always replies with "E" even if the // file extraction is finished successfully. Anyways, we ignore any error at the moment. } e->accept(); // yeah! we've done with XDS so stop Qt from further event propagation. return; } if(e->keyboardModifiers() == Qt::NoModifier) { // if no key modifiers are used, popup a menu // to ask the user for the action he/she wants to perform. Qt::DropAction action = DndActionMenu::askUser(e->possibleActions(), QCursor::pos()); e->setDropAction(action); } } bool FolderView::eventFilter(QObject* watched, QEvent* event) { // NOTE: Instead of simply filtering the drag and drop events of the child view in // the event filter, we overrided each event handler virtual methods in // both QListView and QTreeView and added some childXXXEvent() callbacks. // We did this because of a design flaw of Qt. // All QAbstractScrollArea derived widgets, including QAbstractItemView // contains an internal child widget, which is called a viewport. // The events actually comes from the child viewport, not the parent view itself. // Qt redirects the events of viewport to the viewportEvent() method of // QAbstractScrollArea and let the parent widget handle the events. // Qt implemented this using a event filter installed on the child viewport widget. // That means, when we try to install an event filter on the viewport, // there is already a filter installed by Qt which will be called before ours. // So we can never intercept the event handling of QAbstractItemView by using a filter. // That's why we override respective virtual methods for different events. if(view && watched == view->viewport()) { switch(event->type()) { case QEvent::HoverMove: // activate items on single click if(style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) { QHoverEvent* hoverEvent = static_cast(event); QModelIndex index = view->indexAt(hoverEvent->pos()); // find out the hovered item if(index.isValid()) { // change the cursor to a hand when hovering on an item setCursor(Qt::PointingHandCursor); if(!selectionModel()->hasSelection()) selectionModel()->setCurrentIndex(index, QItemSelectionModel::Current); } else setCursor(Qt::ArrowCursor); // turn on auto-selection for hovered item when single click mode is used. if(autoSelectionDelay_ > 0 && model_) { if(!autoSelectionTimer_) { autoSelectionTimer_ = new QTimer(this); connect(autoSelectionTimer_, &QTimer::timeout, this, &FolderView::onAutoSelectionTimeout); lastAutoSelectionIndex_ = QModelIndex(); } autoSelectionTimer_->start(autoSelectionDelay_); } break; } case QEvent::HoverLeave: if(style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) setCursor(Qt::ArrowCursor); break; case QEvent::Wheel: // This is to fix #85: Scrolling doesn't work in compact view // Actually, I think it's the bug of Qt, not ours. // When in compact mode, only the horizontal scroll bar is used and the vertical one is hidden. // So, when a user scroll his mouse wheel, it's reasonable to scroll the horizontal scollbar. // Qt does not implement such a simple feature, unfortunately. // We do it by forwarding the scroll event in the viewport to the horizontal scrollbar. // FIXME: if someday Qt supports this, we have to disable the workaround. if(mode == CompactMode) { QScrollBar* scroll = view->horizontalScrollBar(); if(scroll) { QApplication::sendEvent(scroll, event); return true; } } break; default: break; } } return QObject::eventFilter(watched, event); } // this slot handles auto-selection of items. void FolderView::onAutoSelectionTimeout() { if(QApplication::mouseButtons() != Qt::NoButton) return; Qt::KeyboardModifiers mods = QApplication::keyboardModifiers(); QPoint pos = view->viewport()->mapFromGlobal(QCursor::pos()); // convert to viewport coordinates QModelIndex index = view->indexAt(pos); // find out the hovered item QItemSelectionModel::SelectionFlags flags = (mode == DetailedListMode ? QItemSelectionModel::Rows : QItemSelectionModel::NoUpdate); QItemSelectionModel* selModel = view->selectionModel(); if(mods & Qt::ControlModifier) { // Ctrl key is pressed if(selModel->isSelected(index) && index != lastAutoSelectionIndex_) { // unselect a previously selected item selModel->select(index, flags|QItemSelectionModel::Deselect); lastAutoSelectionIndex_ = QModelIndex(); } else { // select an unselected item selModel->select(index, flags|QItemSelectionModel::Select); lastAutoSelectionIndex_ = index; } selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); // move the cursor } else if(mods & Qt::ShiftModifier) { // Shift key is pressed // select all items between current index and the hovered index. QModelIndex current = selModel->currentIndex(); if(selModel->hasSelection() && current.isValid()) { selModel->clear(); // clear old selection selModel->setCurrentIndex(current, QItemSelectionModel::NoUpdate); int begin = current.row(); int end = index.row(); if(begin > end) qSwap(begin, end); for(int row = begin; row <= end; ++row) { QModelIndex sel = model_->index(row, 0); selModel->select(sel, flags|QItemSelectionModel::Select); } } else { // no items are selected, select the hovered item. if(index.isValid()) { selModel->select(index, flags|QItemSelectionModel::SelectCurrent); selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); } } lastAutoSelectionIndex_ = index; } else if(mods == Qt::NoModifier) { // no modifier keys are pressed. if(index.isValid()) { // select the hovered item view->clearSelection(); selModel->select(index, flags|QItemSelectionModel::SelectCurrent); selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); } lastAutoSelectionIndex_ = index; } autoSelectionTimer_->deleteLater(); autoSelectionTimer_ = nullptr; } void FolderView::onFileClicked(int type, FmFileInfo* fileInfo) { if(type == ActivatedClick) { if(fileLauncher_) { GList* files = g_list_append(nullptr, fileInfo); fileLauncher_->launchFiles(nullptr, files); g_list_free(files); } } else if(type == ContextMenuClick) { FmPath* folderPath = nullptr; FileInfoList files = selectedFiles(); if (!files.isNull()) { FmFileInfo* first = files.peekHead(); if (files.getLength() == 1 && fm_file_info_is_dir(first)) folderPath = fm_file_info_get_path(first); } if (!folderPath) folderPath = path(); QMenu* menu = nullptr; if(fileInfo) { // show context menu FileInfoList files = selectedFiles(); if (!files.isNull()) { Fm::FileMenu* fileMenu = new Fm::FileMenu(files.dataPtr(), fileInfo, folderPath); fileMenu->setFileLauncher(fileLauncher_); prepareFileMenu(fileMenu); menu = fileMenu; } } else { Fm::FolderMenu* folderMenu = new Fm::FolderMenu(this); prepareFolderMenu(folderMenu); menu = folderMenu; } if (menu) { menu->exec(QCursor::pos()); delete menu; } } } void FolderView::prepareFileMenu(FileMenu* menu) { } void FolderView::prepareFolderMenu(FolderMenu* menu) { }