From d4fcd3396b4a00c529dc7adbc7571a0baebe3703 Mon Sep 17 00:00:00 2001 From: Tsu Jan Date: Fri, 7 Jul 2017 23:55:03 +0430 Subject: [PATCH] Inline Renaming Fixes https://github.com/lxde/pcmanfm-qt/issues/468 by implementing inline renaming for the icon, thumbnail and compact views (the detailed list view needs a separate handling). Inline renaming of the current item is started by the context menu-item `Rename` or `F2` when only one file/folder is selected. It is finished by Enter/Return or by removing focus from the item, and canceled by Esc. If several items are selected, renaming will be done as before, i.e. by calling the rename dialog. For user convenience, wheel scrolling is disabled for the folder view during inline renaming. Another PR will follow this one for inline renaming on desktop. --- src/filemenu.cpp | 8 +++ src/folderitemdelegate.cpp | 105 ++++++++++++++++++++++++++++++++++++- src/folderitemdelegate.h | 9 ++++ src/foldermodel.cpp | 12 ++++- src/foldermodel.h | 3 +- src/folderview.cpp | 56 +++++++++++++++++++- src/folderview.h | 1 + src/utilities.cpp | 27 +++++----- src/utilities.h | 2 + 9 files changed, 206 insertions(+), 17 deletions(-) diff --git a/src/filemenu.cpp b/src/filemenu.cpp index 4bb2cf46..a382482e 100644 --- a/src/filemenu.cpp +++ b/src/filemenu.cpp @@ -31,6 +31,7 @@ #include "customaction_p.h" #include +#include #include #include "filemenu_p.h" @@ -328,6 +329,13 @@ void FileMenu::onPasteTriggered() { } void FileMenu::onRenameTriggered() { + if (QAbstractItemView* view = qobject_cast(parentWidget())) { + // if there is a view and this is a single file, just edit the current index + if (view->currentIndex().isValid() && files_.size() == 1) { + view->edit(view->currentIndex()); + return; + } + } for(auto& info: files_) { Fm::renameFile(info, nullptr); } diff --git a/src/folderitemdelegate.cpp b/src/folderitemdelegate.cpp index 40efa943..e174078b 100644 --- a/src/folderitemdelegate.cpp +++ b/src/folderitemdelegate.cpp @@ -28,6 +28,9 @@ #include #include #include +#include +#include +#include #include namespace Fm { @@ -37,7 +40,9 @@ FolderItemDelegate::FolderItemDelegate(QAbstractItemView* view, QObject* parent) view_(view), symlinkIcon_(QIcon::fromTheme("emblem-symbolic-link")), fileInfoRole_(Fm::FolderModel::FileInfoRole), - iconInfoRole_(-1) { + iconInfoRole_(-1), + hasEditor_(false) { + connect(this, &QAbstractItemDelegate::closeEditor, [=]{hasEditor_ = false;}); } FolderItemDelegate::~FolderItemDelegate() { @@ -265,5 +270,103 @@ void FolderItemDelegate::drawText(QPainter* painter, QStyleOptionViewItem& opt, } } +/* + * The following methods are for inline renaming. + */ + +QWidget* FolderItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { + hasEditor_ = true; + if (option.decorationPosition == QStyleOptionViewItem::Top + || option.decorationPosition == QStyleOptionViewItem::Bottom) + { + // in icon view, we use QTextEdit as the editor (and not QPlainTextEdit + // because the latter always shows an empty space at the bottom) + QTextEdit *textEdit = new QTextEdit(parent); + textEdit->setAcceptRichText(false); + textEdit->ensureCursorVisible(); + textEdit->setFocusPolicy(Qt::StrongFocus); + textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + textEdit->setContentsMargins(0, 0, 0, 0); + return textEdit; + } + else { + // return the default line-edit in compact view + return QStyledItemDelegate::createEditor(parent, option, index); + } +} + +void FolderItemDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { + if (!index.isValid()) { + return; + } + const QString currentName = index.data(Qt::EditRole).toString(); + + if (QTextEdit* textEdit = qobject_cast(editor)) { + textEdit->setPlainText(currentName); + textEdit->setUndoRedoEnabled(false); + textEdit->setAlignment(Qt::AlignCenter); + textEdit->setUndoRedoEnabled(true); + // select text appropriately + QTextCursor cur = textEdit->textCursor(); + int end; + if (index.data(Fm::FolderModel::FileIsDirRole).toBool() || !currentName.contains(".")) { + end = currentName.size(); + } + else { + end = currentName.lastIndexOf("."); + } + cur.setPosition(end, QTextCursor::KeepAnchor); + textEdit->setTextCursor(cur); + } + else if (QLineEdit* lineEdit = qobject_cast(editor)) { + lineEdit->setText(currentName); + if (!index.data(Fm::FolderModel::FileIsDirRole).toBool() && currentName.contains(".")) + { + /* Qt will call QLineEdit::selectAll() after calling setEditorData() in + qabstractitemview.cpp -> QAbstractItemViewPrivate::editor(). Therefore, + we cannot select a part of the text in the usual way here. */ + QTimer::singleShot(0, [lineEdit]() { + int length = lineEdit->text().lastIndexOf("."); + lineEdit->setSelection(0, length); + }); + } + } +} + +bool FolderItemDelegate::eventFilter(QObject* object, QEvent* event) { + QWidget *editor = qobject_cast(object); + if (editor && event->type() == QEvent::KeyPress) { + int k = static_cast(event)->key(); + if (k == Qt::Key_Return || k == Qt::Key_Enter) { + Q_EMIT QAbstractItemDelegate::commitData(editor); + Q_EMIT QAbstractItemDelegate::closeEditor(editor, QAbstractItemDelegate::NoHint); + return true; + } + } + return QStyledItemDelegate::eventFilter(object, event); +} + +void FolderItemDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const { + if (gridSize_ != QSize() + && (option.decorationPosition == QStyleOptionViewItem::Top + || option.decorationPosition == QStyleOptionViewItem::Bottom)) { + // give all of the available space to the editor + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + opt.decorationAlignment = Qt::AlignHCenter|Qt::AlignTop; + opt.displayAlignment = Qt::AlignTop|Qt::AlignHCenter; + QRect textRect(opt.rect.x() - (gridSize_.width() - opt.rect.width()) / 2, + opt.rect.y() + option.decorationSize.height(), + gridSize_.width(), + gridSize_.height() - option.decorationSize.height()); + int frame = editor->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &option, editor); + editor->setGeometry(textRect.adjusted(-frame, -frame, frame, frame)); + } + else { + // use the default editor geometry in compact view + QStyledItemDelegate::updateEditorGeometry(editor, option, index); + } +} + } // namespace Fm diff --git a/src/folderitemdelegate.h b/src/folderitemdelegate.h index 3e95160d..4915c6a1 100644 --- a/src/folderitemdelegate.h +++ b/src/folderitemdelegate.h @@ -57,8 +57,16 @@ class LIBFM_QT_API FolderItemDelegate : public QStyledItemDelegate { iconInfoRole_ = role; } + bool hasEditor() const { + return hasEditor_; + } + virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; + virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const; + virtual void setEditorData(QWidget* editor, const QModelIndex& index) const; + virtual bool eventFilter(QObject* object, QEvent* event); + virtual void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const; private: void drawText(QPainter* painter, QStyleOptionViewItem& opt, QRectF& textRect) const; @@ -70,6 +78,7 @@ class LIBFM_QT_API FolderItemDelegate : public QStyledItemDelegate { QSize gridSize_; int fileInfoRole_; int iconInfoRole_; + mutable bool hasEditor_; }; } diff --git a/src/foldermodel.cpp b/src/foldermodel.cpp index 817c6771..f48eda1a 100644 --- a/src/foldermodel.cpp +++ b/src/foldermodel.cpp @@ -211,6 +211,7 @@ QVariant FolderModel::data(const QModelIndex& index, int role/* = Qt::DisplayRol case ColumnFileOwner: return item->ownerName(); } + break; } case Qt::DecorationRole: { if(index.column() == 0) { @@ -218,8 +219,16 @@ QVariant FolderModel::data(const QModelIndex& index, int role/* = Qt::DisplayRol } break; } + case Qt::EditRole: { + if(index.column() == 0) { + return QString::fromStdString(info->name()); + } + break; + } case FileInfoRole: return QVariant::fromValue(info); + case FileIsDirRole: + return QVariant(info->isDir()); } return QVariant(); } @@ -269,7 +278,8 @@ Qt::ItemFlags FolderModel::flags(const QModelIndex& index) const { if(index.isValid()) { flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; if(index.column() == ColumnFileName) { - flags |= (Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled); + flags |= (Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled + | Qt::ItemIsEditable); // inline renaming); } } else { diff --git a/src/foldermodel.h b/src/foldermodel.h index 34fc48b6..01a10608 100644 --- a/src/foldermodel.h +++ b/src/foldermodel.h @@ -42,7 +42,8 @@ class LIBFM_QT_API FolderModel : public QAbstractListModel { public: enum Role { - FileInfoRole = Qt::UserRole + FileInfoRole = Qt::UserRole, + FileIsDirRole }; enum ColumnId { diff --git a/src/folderview.cpp b/src/folderview.cpp index 63f90702..f2cb7d98 100644 --- a/src/folderview.cpp +++ b/src/folderview.cpp @@ -37,11 +37,15 @@ #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" +#include "utilities.h" Q_DECLARE_OPAQUE_POINTER(FmFileInfo*) @@ -51,6 +55,8 @@ FolderViewListView::FolderViewListView(QWidget* parent): QListView(parent), activationAllowed_(true) { connect(this, &QListView::activated, this, &FolderViewListView::activation); + // inline renaming + setEditTriggers(QAbstractItemView::NoEditTriggers); } FolderViewListView::~FolderViewListView() { @@ -227,6 +233,8 @@ FolderViewTreeView::FolderViewTreeView(QWidget* parent): setIndentation(0); connect(this, &QTreeView::activated, this, &FolderViewTreeView::activation); + // don't open editor on double clicking + setEditTriggers(QAbstractItemView::NoEditTriggers); } FolderViewTreeView::~FolderViewTreeView() { @@ -493,6 +501,40 @@ void FolderView::onSelectionChanged(const QItemSelection& /*selected*/, const QI } } +void FolderView::onClosingEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint) { + if (hint != QAbstractItemDelegate::NoHint) { + // we set the hint to NoHint in FolderItemDelegate::eventFilter() + return; + } + QString newName; + if (qobject_cast(editor)) { // icon and thumbnail view + newName = qobject_cast(editor)->toPlainText(); + } + else if (qobject_cast(editor)) { // compact view + newName = qobject_cast(editor)->text(); + } + if (newName.isEmpty()) { + return; + } + // the editor will be deleted by QAbstractItemDelegate::destroyEditor() when no longer needed + + QModelIndex index = view->selectionModel()->currentIndex(); + if(index.isValid() && index.model()) { + QVariant data = index.model()->data(index, FolderModel::FileInfoRole); + auto info = data.value>(); + if (info) { + auto oldName = QString::fromStdString(info->name()); + if(newName == oldName) { + return; + } + QWidget* parent = window(); + if (window() == this) { // supposedly desktop, in case it uses this + parent = nullptr; + } + changeFileName(info->path(), newName, parent); + } + } +} void FolderView::setViewMode(ViewMode _mode) { if(_mode == mode) { // if it's the same more, ignore @@ -540,6 +582,8 @@ void FolderView::setViewMode(ViewMode _mode) { // set our own custom delegate FolderItemDelegate* delegate = new FolderItemDelegate(listView); listView->setItemDelegateForColumn(FolderModel::ColumnFileName, delegate); + // inline renaming + connect(delegate, &QAbstractItemDelegate::closeEditor, this, &FolderView::onClosingEditor); // FIXME: should we expose the delegate? listView->setMovement(QListView::Static); /* If listView is already visible, setMovement() will lay out items again with delay @@ -968,6 +1012,14 @@ bool FolderView::eventFilter(QObject* watched, QEvent* event) { } break; case QEvent::Wheel: + // don't let the view scroll during an inline renaming + if (view && mode != DetailedListMode) { + FolderViewListView* listView = static_cast(view); + FolderItemDelegate* delegate = static_cast(listView->itemDelegateForColumn(FolderModel::ColumnFileName)); + if (delegate->hasEditor()) { + return true; + } + } // 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. @@ -1078,7 +1130,9 @@ void FolderView::onFileClicked(int type, const std::shared_ptrsetFileLauncher(fileLauncher_); prepareFileMenu(fileMenu); menu = fileMenu; diff --git a/src/folderview.h b/src/folderview.h index 4deeb360..980be837 100644 --- a/src/folderview.h +++ b/src/folderview.h @@ -155,6 +155,7 @@ public Q_SLOTS: private Q_SLOTS: void onAutoSelectionTimeout(); void onSelChangedTimeout(); + void onClosingEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint); Q_SIGNALS: void clicked(int type, const std::shared_ptr& file); diff --git a/src/utilities.cpp b/src/utilities.cpp index 1f945521..14eb8118 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -130,8 +130,20 @@ void cutFilesToClipboard(const Fm::FilePathList& files) { clipboard->setMimeData(data); } +void changeFileName(const Fm::FilePath& filePath, const QString& newName, QWidget* parent) { + auto dest = filePath.parent().child(newName.toLocal8Bit().constData()); + Fm::GErrorPtr err; + if(!g_file_move(filePath.gfile().get(), dest.gfile().get(), + GFileCopyFlags(G_FILE_COPY_ALL_METADATA | + G_FILE_COPY_NO_FALLBACK_FOR_MOVE | + G_FILE_COPY_NOFOLLOW_SYMLINKS), + nullptr, /* make this cancellable later. */ + nullptr, nullptr, &err)) { + QMessageBox::critical(parent, QObject::tr("Error"), err.message()); + } +} + void renameFile(std::shared_ptr file, QWidget* parent) { - auto path = file->path(); FilenameDialog dlg(parent); dlg.setWindowTitle(QObject::tr("Rename File")); dlg.setLabelText(QObject::tr("Please enter a new name:")); @@ -151,18 +163,7 @@ void renameFile(std::shared_ptr file, QWidget* parent) { if(new_name == old_name) { return; } - - auto parent_dir = path.parent(); - auto dest = path.parent().child(new_name.toLocal8Bit().constData()); - Fm::GErrorPtr err; - if(!g_file_move(path.gfile().get(), dest.gfile().get(), - GFileCopyFlags(G_FILE_COPY_ALL_METADATA | - G_FILE_COPY_NO_FALLBACK_FOR_MOVE | - G_FILE_COPY_NOFOLLOW_SYMLINKS), - nullptr, /* make this cancellable later. */ - nullptr, nullptr, &err)) { - QMessageBox::critical(parent, QObject::tr("Error"), err.message()); - } + changeFileName(file->path(), new_name, parent); } // templateFile is a file path used as a template of the new file. diff --git a/src/utilities.h b/src/utilities.h index ca3e0f2f..88c298a1 100644 --- a/src/utilities.h +++ b/src/utilities.h @@ -48,6 +48,8 @@ LIBFM_QT_API void copyFilesToClipboard(const Fm::FilePathList& files); LIBFM_QT_API void cutFilesToClipboard(const Fm::FilePathList& files); +LIBFM_QT_API void changeFileName(const Fm::FilePath& path, const QString& newName, QWidget* parent); + LIBFM_QT_API void renameFile(std::shared_ptr file, QWidget* parent = 0); enum CreateFileType {