mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-05 13:59:52 +08:00

- Add shortcut Ctrl+E A to show attachment list; - Add Vim-like navigation shortcut to attachment list; - Support drag-and-drop to add attachments; - Add bubble to indicate the number of attachments at the right top corner;
567 lines
18 KiB
C++
567 lines
18 KiB
C++
#include "vattachmentlist.h"
|
|
|
|
#include <QtWidgets>
|
|
|
|
#include "vconfigmanager.h"
|
|
#include "utils/vutils.h"
|
|
#include "vbuttonwithwidget.h"
|
|
#include "vnote.h"
|
|
#include "vmainwindow.h"
|
|
#include "dialog/vconfirmdeletiondialog.h"
|
|
#include "dialog/vsortdialog.h"
|
|
|
|
extern VConfigManager *g_config;
|
|
extern VNote *g_vnote;
|
|
|
|
VAttachmentList::VAttachmentList(QWidget *p_parent)
|
|
: QWidget(p_parent), m_file(NULL)
|
|
{
|
|
setupUI();
|
|
|
|
initActions();
|
|
|
|
updateContent();
|
|
}
|
|
|
|
void VAttachmentList::setupUI()
|
|
{
|
|
m_addBtn = new QPushButton(QIcon(":/resources/icons/add_attachment.svg"), "");
|
|
m_addBtn->setToolTip(tr("Add"));
|
|
m_addBtn->setProperty("FlatBtn", true);
|
|
m_addBtn->setDefault(true);
|
|
connect(m_addBtn, &QPushButton::clicked,
|
|
this, &VAttachmentList::addAttachment);
|
|
|
|
m_clearBtn = new QPushButton(QIcon(":/resources/icons/clear_attachment.svg"), "");
|
|
m_clearBtn->setToolTip(tr("Clear"));
|
|
m_clearBtn->setProperty("FlatBtn", true);
|
|
connect(m_clearBtn, &QPushButton::clicked,
|
|
this, [this]() {
|
|
if (m_file && m_attachmentList->count() > 0) {
|
|
int ret = VUtils::showMessage(QMessageBox::Warning, tr("Warning"),
|
|
tr("Are you sure to clear attachments of note "
|
|
"<span style=\"%1\">%2</span>?")
|
|
.arg(g_config->c_dataTextStyle)
|
|
.arg(m_file->getName()),
|
|
tr("<span style=\"%1\">WARNING</span>: "
|
|
"VNote will delete all the files in directory "
|
|
"<span style=\"%2\">%3</span>."
|
|
"You could find deleted files in the recycle bin "
|
|
"of this notebook.<br>The operation is IRREVERSIBLE!")
|
|
.arg(g_config->c_warningTextStyle)
|
|
.arg(g_config->c_dataTextStyle)
|
|
.arg(m_file->fetchAttachmentFolderPath()),
|
|
QMessageBox::Ok | QMessageBox::Cancel,
|
|
QMessageBox::Ok,
|
|
g_vnote->getMainWindow(),
|
|
MessageBoxType::Danger);
|
|
if (ret == QMessageBox::Ok) {
|
|
if (!m_file->deleteAttachments()) {
|
|
VUtils::showMessage(QMessageBox::Warning,
|
|
tr("Warning"),
|
|
tr("Fail to clear attachments of note <span style=\"%1\">%2</span>.")
|
|
.arg(g_config->c_dataTextStyle)
|
|
.arg(m_file->getName()),
|
|
tr("Please maintain the configureation file manually."),
|
|
QMessageBox::Ok,
|
|
QMessageBox::Ok,
|
|
g_vnote->getMainWindow());
|
|
}
|
|
|
|
m_attachmentList->clear();
|
|
|
|
updateButtonState();
|
|
}
|
|
}
|
|
});
|
|
|
|
m_locateBtn = new QPushButton(QIcon(":/resources/icons/locate_attachment.svg"), "");
|
|
m_locateBtn->setToolTip(tr("Open Folder"));
|
|
m_locateBtn->setProperty("FlatBtn", true);
|
|
connect(m_locateBtn, &QPushButton::clicked,
|
|
this, [this]() {
|
|
if (m_file && !m_file->getAttachmentFolder().isEmpty()) {
|
|
QUrl url = QUrl::fromLocalFile(m_file->fetchAttachmentFolderPath());
|
|
QDesktopServices::openUrl(url);
|
|
}
|
|
});
|
|
|
|
m_numLabel = new QLabel();
|
|
|
|
QHBoxLayout *btnLayout = new QHBoxLayout;
|
|
btnLayout->addWidget(m_addBtn);
|
|
btnLayout->addWidget(m_clearBtn);
|
|
btnLayout->addWidget(m_locateBtn);
|
|
btnLayout->addStretch();
|
|
btnLayout->addWidget(m_numLabel);
|
|
|
|
m_attachmentList = new QListWidget;
|
|
m_attachmentList->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
m_attachmentList->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
|
m_attachmentList->setEditTriggers(QAbstractItemView::SelectedClicked);
|
|
connect(m_attachmentList, &QListWidget::customContextMenuRequested,
|
|
this, &VAttachmentList::handleContextMenuRequested);
|
|
connect(m_attachmentList, &QListWidget::itemActivated,
|
|
this, &VAttachmentList::handleItemActivated);
|
|
connect(m_attachmentList->itemDelegate(), &QAbstractItemDelegate::commitData,
|
|
this, &VAttachmentList::handleListItemCommitData);
|
|
|
|
QVBoxLayout *mainLayout = new QVBoxLayout();
|
|
mainLayout->addLayout(btnLayout);
|
|
mainLayout->addWidget(m_attachmentList);
|
|
|
|
setLayout(mainLayout);
|
|
}
|
|
|
|
void VAttachmentList::initActions()
|
|
{
|
|
m_openAct = new QAction(tr("&Open"), this);
|
|
m_openAct->setToolTip(tr("Open current attachment file"));
|
|
connect(m_openAct, &QAction::triggered,
|
|
this, [this]() {
|
|
QListWidgetItem *item = m_attachmentList->currentItem();
|
|
handleItemActivated(item);
|
|
});
|
|
|
|
m_deleteAct = new QAction(QIcon(":/resources/icons/delete_attachment.svg"),
|
|
tr("&Delete"),
|
|
this);
|
|
m_deleteAct->setToolTip(tr("Delete selected attachments"));
|
|
connect(m_deleteAct, &QAction::triggered,
|
|
this, &VAttachmentList::deleteSelectedItems);
|
|
|
|
m_sortAct = new QAction(QIcon(":/resources/icons/sort.svg"),
|
|
tr("&Sort"),
|
|
this);
|
|
m_sortAct->setToolTip(tr("Sort attachments manually"));
|
|
connect(m_sortAct, &QAction::triggered,
|
|
this, &VAttachmentList::sortItems);
|
|
}
|
|
|
|
void VAttachmentList::setFile(VNoteFile *p_file)
|
|
{
|
|
m_file = p_file;
|
|
|
|
updateButtonState();
|
|
}
|
|
|
|
void VAttachmentList::updateContent()
|
|
{
|
|
bool enableAdd = true, enableDelete = true, enableClear = true, enableLocate = true;
|
|
m_attachmentList->clear();
|
|
|
|
if (!m_file) {
|
|
enableAdd = enableDelete = enableClear = enableLocate = false;
|
|
} else {
|
|
QString folder = m_file->getAttachmentFolder();
|
|
const QVector<VAttachment> &attas = m_file->getAttachments();
|
|
|
|
if (folder.isEmpty()) {
|
|
Q_ASSERT(attas.isEmpty());
|
|
enableDelete = enableClear = enableLocate = false;
|
|
} else if (attas.isEmpty()) {
|
|
enableDelete = enableClear = false;
|
|
} else {
|
|
fillAttachmentList(attas);
|
|
}
|
|
}
|
|
|
|
m_addBtn->setEnabled(enableAdd);
|
|
m_clearBtn->setEnabled(enableClear);
|
|
m_locateBtn->setEnabled(enableLocate);
|
|
|
|
int cnt = m_attachmentList->count();
|
|
if (cnt > 0) {
|
|
m_numLabel->setText(tr("%1 %2").arg(cnt).arg(cnt > 1 ? tr("Files") : tr("File")));
|
|
m_attachmentList->setFocus();
|
|
} else {
|
|
m_numLabel->setText("");
|
|
|
|
if (m_file) {
|
|
m_addBtn->setFocus();
|
|
}
|
|
}
|
|
}
|
|
|
|
void VAttachmentList::fillAttachmentList(const QVector<VAttachment> &p_attachments)
|
|
{
|
|
Q_ASSERT(m_attachmentList->count() == 0);
|
|
for (int i = 0; i < p_attachments.size(); ++i) {
|
|
const VAttachment &atta = p_attachments[i];
|
|
QListWidgetItem *item = new QListWidgetItem(atta.m_name);
|
|
item->setFlags(item->flags() | Qt::ItemIsEditable);
|
|
item->setData(Qt::UserRole, atta.m_name);
|
|
|
|
m_attachmentList->addItem(item);
|
|
}
|
|
}
|
|
|
|
void VAttachmentList::addAttachment()
|
|
{
|
|
if (!m_file) {
|
|
return;
|
|
}
|
|
|
|
static QString lastPath = QDir::homePath();
|
|
QStringList files = QFileDialog::getOpenFileNames(g_vnote->getMainWindow(),
|
|
tr("Select Files As Attachments"),
|
|
lastPath);
|
|
if (files.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Update lastPath
|
|
lastPath = QFileInfo(files[0]).path();
|
|
|
|
addAttachments(files);
|
|
|
|
updateButtonState();
|
|
|
|
updateContent();
|
|
}
|
|
|
|
void VAttachmentList::addAttachments(const QStringList &p_files)
|
|
{
|
|
Q_ASSERT(m_file);
|
|
int addedFiles = 0;
|
|
for (int i = 0; i < p_files.size(); ++i) {
|
|
if (!m_file->addAttachment(p_files[i])) {
|
|
VUtils::showMessage(QMessageBox::Warning,
|
|
tr("Warning"),
|
|
tr("Fail to add attachment %1 for note <span style=\"%2\">%3</span>.")
|
|
.arg(p_files[i])
|
|
.arg(g_config->c_dataTextStyle)
|
|
.arg(m_file->getName()),
|
|
"",
|
|
QMessageBox::Ok,
|
|
QMessageBox::Ok,
|
|
g_vnote->getMainWindow());
|
|
} else {
|
|
++addedFiles;
|
|
}
|
|
}
|
|
|
|
if (addedFiles > 0) {
|
|
g_vnote->getMainWindow()->showStatusMessage(tr("Added %1 %2 as attachments")
|
|
.arg(addedFiles)
|
|
.arg(addedFiles > 1 ? tr("files") : tr("file")));
|
|
}
|
|
}
|
|
|
|
void VAttachmentList::handleContextMenuRequested(QPoint p_pos)
|
|
{
|
|
// @p_pos is the position in the coordinate of VAttachmentList, no m_attachmentList.
|
|
QListWidgetItem *item = m_attachmentList->itemAt(m_attachmentList->mapFromParent(p_pos));
|
|
QMenu menu(this);
|
|
menu.setToolTipsVisible(true);
|
|
|
|
if (!m_file) {
|
|
return;
|
|
}
|
|
|
|
if (item) {
|
|
if (!item->isSelected()) {
|
|
m_attachmentList->setCurrentItem(item, QItemSelectionModel::ClearAndSelect);
|
|
}
|
|
|
|
if (m_attachmentList->selectedItems().size() == 1) {
|
|
menu.addAction(m_openAct);
|
|
}
|
|
|
|
menu.addAction(m_deleteAct);
|
|
}
|
|
|
|
m_attachmentList->update();
|
|
|
|
if (m_file->getAttachments().size() > 1) {
|
|
if (!menu.actions().isEmpty()) {
|
|
menu.addSeparator();
|
|
}
|
|
|
|
menu.addAction(m_sortAct);
|
|
}
|
|
|
|
if (!menu.actions().isEmpty()) {
|
|
menu.exec(mapToGlobal(p_pos));
|
|
}
|
|
}
|
|
|
|
void VAttachmentList::handleItemActivated(QListWidgetItem *p_item)
|
|
{
|
|
if (p_item) {
|
|
Q_ASSERT(m_file);
|
|
|
|
QString name = p_item->text();
|
|
QString folderPath = m_file->fetchAttachmentFolderPath();
|
|
QUrl url = QUrl::fromLocalFile(QDir(folderPath).filePath(name));
|
|
QDesktopServices::openUrl(url);
|
|
}
|
|
}
|
|
|
|
void VAttachmentList::deleteSelectedItems()
|
|
{
|
|
QVector<QString> names;
|
|
const QList<QListWidgetItem *> selectedItems = m_attachmentList->selectedItems();
|
|
|
|
if (selectedItems.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
for (auto const & item : selectedItems) {
|
|
names.push_back(item->text());
|
|
}
|
|
|
|
QString info = tr("Are you sure to delete these attachments of note "
|
|
"<span style=\"%1\">%2</span>? "
|
|
"You could find deleted files in the recycle "
|
|
"bin of this notebook.<br>"
|
|
"Click \"Cancel\" to leave them untouched.")
|
|
.arg(g_config->c_dataTextStyle).arg(m_file->getName());
|
|
VConfirmDeletionDialog dialog(tr("Confirm Deleting Attachments"),
|
|
info,
|
|
names,
|
|
false,
|
|
false,
|
|
false,
|
|
g_vnote->getMainWindow());
|
|
if (dialog.exec()) {
|
|
names = dialog.getConfirmedFiles();
|
|
|
|
if (!m_file->deleteAttachments(names)) {
|
|
VUtils::showMessage(QMessageBox::Warning,
|
|
tr("Warning"),
|
|
tr("Fail to delete attachments of note <span style=\"%1\">%2</span>.")
|
|
.arg(g_config->c_dataTextStyle)
|
|
.arg(m_file->getName()),
|
|
tr("Please maintain the configureation file manually."),
|
|
QMessageBox::Ok,
|
|
QMessageBox::Ok,
|
|
g_vnote->getMainWindow());
|
|
}
|
|
|
|
updateButtonState();
|
|
|
|
updateContent();
|
|
}
|
|
}
|
|
|
|
void VAttachmentList::sortItems()
|
|
{
|
|
const QVector<VAttachment> &attas = m_file->getAttachments();
|
|
if (attas.size() < 2) {
|
|
return;
|
|
}
|
|
|
|
VSortDialog dialog(tr("Sort Attachments"),
|
|
tr("Sort attachments in the configuration file."),
|
|
g_vnote->getMainWindow());
|
|
QTreeWidget *tree = dialog.getTreeWidget();
|
|
tree->clear();
|
|
tree->setColumnCount(1);
|
|
tree->header()->setStretchLastSection(true);
|
|
QStringList headers;
|
|
headers << tr("Name");
|
|
tree->setHeaderLabels(headers);
|
|
|
|
for (int i = 0; i < attas.size(); ++i) {
|
|
QTreeWidgetItem *item = new QTreeWidgetItem(tree, QStringList(attas[i].m_name));
|
|
|
|
item->setData(0, Qt::UserRole, i);
|
|
}
|
|
|
|
dialog.treeUpdated();
|
|
|
|
if (dialog.exec()) {
|
|
int cnt = tree->topLevelItemCount();
|
|
Q_ASSERT(cnt == attas.size());
|
|
QVector<int> sortedIdx(cnt, -1);
|
|
for (int i = 0; i < cnt; ++i) {
|
|
QTreeWidgetItem *item = tree->topLevelItem(i);
|
|
Q_ASSERT(item);
|
|
sortedIdx[i] = item->data(0, Qt::UserRole).toInt();
|
|
}
|
|
|
|
m_file->sortAttachments(sortedIdx);
|
|
}
|
|
}
|
|
|
|
void VAttachmentList::handleListItemCommitData(QWidget *p_itemEdit)
|
|
{
|
|
QString text = reinterpret_cast<QLineEdit *>(p_itemEdit)->text();
|
|
QListWidgetItem *item = m_attachmentList->currentItem();
|
|
Q_ASSERT(item && item->text() == text);
|
|
|
|
QString oldText = item->data(Qt::UserRole).toString();
|
|
|
|
if (oldText == text) {
|
|
return;
|
|
}
|
|
|
|
bool legalName = true;
|
|
if (text.isEmpty()) {
|
|
legalName = false;
|
|
} else {
|
|
QRegExp reg(VUtils::c_fileNameRegExp);
|
|
if (!reg.exactMatch(text)) {
|
|
legalName = false;
|
|
}
|
|
}
|
|
|
|
if (!legalName) {
|
|
// Recover to old name.
|
|
item->setText(oldText);
|
|
return;
|
|
}
|
|
|
|
if (!(oldText.toLower() == text.toLower())
|
|
&& m_file->findAttachment(text, false) > -1) {
|
|
// Name conflict.
|
|
// Recover to old name.
|
|
item->setText(oldText);
|
|
} else {
|
|
if (!m_file->renameAttachment(oldText, text)) {
|
|
VUtils::showMessage(QMessageBox::Information,
|
|
tr("Rename Attachment"),
|
|
tr("Fail to rename attachment <span style=\"%1\">%2</span>.")
|
|
.arg(g_config->c_dataTextStyle)
|
|
.arg(oldText),
|
|
"",
|
|
QMessageBox::Ok,
|
|
QMessageBox::Ok,
|
|
this);
|
|
// Recover to old name.
|
|
item->setText(oldText);
|
|
} else {
|
|
// Change the data.
|
|
item->setData(Qt::UserRole, text);
|
|
}
|
|
}
|
|
}
|
|
|
|
void VAttachmentList::keyPressEvent(QKeyEvent *p_event)
|
|
{
|
|
int key = p_event->key();
|
|
int modifiers = p_event->modifiers();
|
|
switch (key) {
|
|
case Qt::Key_BracketLeft:
|
|
{
|
|
if (modifiers == Qt::ControlModifier) {
|
|
QKeyEvent *escEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Escape,
|
|
Qt::NoModifier);
|
|
QCoreApplication::postEvent(this, escEvent);
|
|
return;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case Qt::Key_J:
|
|
{
|
|
if (modifiers == Qt::ControlModifier) {
|
|
QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down,
|
|
Qt::NoModifier);
|
|
QCoreApplication::postEvent(m_attachmentList, downEvent);
|
|
return;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case Qt::Key_K:
|
|
{
|
|
if (modifiers == Qt::ControlModifier) {
|
|
QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up,
|
|
Qt::NoModifier);
|
|
QCoreApplication::postEvent(m_attachmentList, upEvent);
|
|
return;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
QWidget::keyPressEvent(p_event);
|
|
}
|
|
|
|
bool VAttachmentList::isAcceptDrops() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool VAttachmentList::handleDragEnterEvent(QDragEnterEvent *p_event)
|
|
{
|
|
if (!m_file) {
|
|
return false;
|
|
}
|
|
|
|
if (p_event->mimeData()->hasFormat("text/uri-list")) {
|
|
p_event->acceptProposedAction();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool VAttachmentList::handleDropEvent(QDropEvent *p_event)
|
|
{
|
|
if (!m_file) {
|
|
return false;
|
|
}
|
|
|
|
const QMimeData *mime = p_event->mimeData();
|
|
if (mime->hasFormat("text/uri-list") && mime->hasUrls()) {
|
|
// Add attachments.
|
|
QStringList files;
|
|
QList<QUrl> urls = mime->urls();
|
|
for (int i = 0; i < urls.size(); ++i) {
|
|
QString file;
|
|
if (urls[i].isLocalFile()) {
|
|
file = urls[i].toLocalFile();
|
|
QFileInfo fi(file);
|
|
if (fi.exists() && fi.isFile()) {
|
|
file = QDir::cleanPath(fi.absoluteFilePath());
|
|
files.append(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!files.isEmpty()) {
|
|
addAttachments(files);
|
|
|
|
updateButtonState();
|
|
}
|
|
|
|
p_event->acceptProposedAction();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void VAttachmentList::handleAboutToShow()
|
|
{
|
|
updateContent();
|
|
}
|
|
|
|
void VAttachmentList::updateButtonState() const
|
|
{
|
|
VButtonWithWidget *btn = getButton();
|
|
Q_ASSERT(btn);
|
|
if (!btn) {
|
|
return;
|
|
}
|
|
|
|
int numOfAttachments = -1;
|
|
if (m_file) {
|
|
numOfAttachments = m_file->getAttachments().size();
|
|
if (numOfAttachments == 0) {
|
|
numOfAttachments = -1;
|
|
}
|
|
}
|
|
|
|
btn->setBubbleNumber(numOfAttachments);
|
|
}
|