Skip to content

Commit

Permalink
Inline Renaming
Browse files Browse the repository at this point in the history
Fixes lxqt/pcmanfm-qt#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.
  • Loading branch information
tsujan committed Jul 17, 2017
1 parent 67c77de commit 45169fa
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 17 deletions.
8 changes: 8 additions & 0 deletions src/filemenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "customaction_p.h"

#include <QMessageBox>
#include <QAbstractItemView>
#include <QDebug>
#include "filemenu_p.h"

Expand Down Expand Up @@ -328,6 +329,13 @@ void FileMenu::onPasteTriggered() {
}

void FileMenu::onRenameTriggered() {
if (QAbstractItemView* view = qobject_cast<QAbstractItemView*>(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);
}
Expand Down
104 changes: 103 additions & 1 deletion src/folderitemdelegate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
#include <QTextLayout>
#include <QTextOption>
#include <QTextLine>
#include <QLineEdit>
#include <QTextEdit>
#include <QTimer>
#include <QDebug>

namespace Fm {
Expand All @@ -38,7 +41,9 @@ FolderItemDelegate::FolderItemDelegate(QAbstractItemView* view, QObject* parent)
symlinkIcon_(QIcon::fromTheme("emblem-symbolic-link")),
fileInfoRole_(Fm::FolderModel::FileInfoRole),
iconInfoRole_(-1),
margins_(QSize(3, 3)) {
margins_(QSize(3, 3)),
hasEditor_(false) {
connect(this, &QAbstractItemDelegate::closeEditor, [=]{hasEditor_ = false;});
}

FolderItemDelegate::~FolderItemDelegate() {
Expand Down Expand Up @@ -296,5 +301,102 @@ 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<QTextEdit*>(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<QLineEdit*>(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<QWidget*>(object);
if (editor && event->type() == QEvent::KeyPress) {
int k = static_cast<QKeyEvent *>(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 (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(),
opt.rect.y() + margins_.height() + option.decorationSize.height(),
itemSize_.width(),
itemSize_.height() - margins_.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
13 changes: 13 additions & 0 deletions src/folderitemdelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,22 @@ class LIBFM_QT_API FolderItemDelegate : public QStyledItemDelegate {
return margins_;
}

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;

QSize iconViewTextSize(const QModelIndex& index) const;

private:
Expand All @@ -104,6 +116,7 @@ class LIBFM_QT_API FolderItemDelegate : public QStyledItemDelegate {
int iconInfoRole_;
QColor shadowColor_;
QSize margins_;
mutable bool hasEditor_;
};

}
Expand Down
12 changes: 11 additions & 1 deletion src/foldermodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -211,15 +211,24 @@ QVariant FolderModel::data(const QModelIndex& index, int role/* = Qt::DisplayRol
case ColumnFileOwner:
return item->ownerName();
}
break;
}
case Qt::DecorationRole: {
if(index.column() == 0) {
return QVariant(item->icon());
}
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();
}
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/foldermodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class LIBFM_QT_API FolderModel : public QAbstractListModel {
public:

enum Role {
FileInfoRole = Qt::UserRole
FileInfoRole = Qt::UserRole,
FileIsDirRole
};

enum ColumnId {
Expand Down
56 changes: 55 additions & 1 deletion src/folderview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@
#include <QApplication>
#include <QScrollBar>
#include <QMetaType>
#include <QMessageBox>
#include <QLineEdit>
#include <QTextEdit>
#include <QX11Info> // for XDS support
#include <xcb/xcb.h> // for XDS support
#include "xdndworkaround.h" // for XDS support
#include "path.h"
#include "folderview_p.h"
#include "utilities.h"

Q_DECLARE_OPAQUE_POINTER(FmFileInfo*)

Expand All @@ -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() {
Expand Down Expand Up @@ -236,6 +242,8 @@ FolderViewTreeView::FolderViewTreeView(QWidget* parent):
setExpandsOnDoubleClick(false);

connect(this, &QTreeView::activated, this, &FolderViewTreeView::activation);
// don't open editor on double clicking
setEditTriggers(QAbstractItemView::NoEditTriggers);
}

FolderViewTreeView::~FolderViewTreeView() {
Expand Down Expand Up @@ -502,6 +510,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<QTextEdit*>(editor)) { // icon and thumbnail view
newName = qobject_cast<QTextEdit*>(editor)->toPlainText();
}
else if (qobject_cast<QLineEdit*>(editor)) { // compact view
newName = qobject_cast<QLineEdit*>(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<std::shared_ptr<const Fm::FileInfo>>();
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
Expand Down Expand Up @@ -549,6 +591,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
Expand Down Expand Up @@ -981,6 +1025,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<FolderViewListView*>(view);
FolderItemDelegate* delegate = static_cast<FolderItemDelegate*>(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.
Expand Down Expand Up @@ -1091,7 +1143,9 @@ void FolderView::onFileClicked(int type, const std::shared_ptr<const Fm::FileInf
// show context menu
auto files = selectedFiles();
if(!files.empty()) {
Fm::FileMenu* fileMenu = new Fm::FileMenu(files, fileInfo, folderPath);
Fm::FileMenu* fileMenu = (view && mode != DetailedListMode && selectedIndexes().size() == 1)
? new Fm::FileMenu(files, fileInfo, folderPath, QString(), view)
: new Fm::FileMenu(files, fileInfo, folderPath);
fileMenu->setFileLauncher(fileLauncher_);
prepareFileMenu(fileMenu);
menu = fileMenu;
Expand Down
1 change: 1 addition & 0 deletions src/folderview.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<const Fm::FileInfo>& file);
Expand Down
27 changes: 14 additions & 13 deletions src/utilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<const Fm::FileInfo> 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:"));
Expand All @@ -151,18 +163,7 @@ void renameFile(std::shared_ptr<const Fm::FileInfo> 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.
Expand Down
2 changes: 2 additions & 0 deletions src/utilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<const Fm::FileInfo> file, QWidget* parent = 0);

enum CreateFileType {
Expand Down

0 comments on commit 45169fa

Please sign in to comment.