269 lines
9.1 KiB
C++
269 lines
9.1 KiB
C++
#include "denote_fileitemaction.h"
|
|
|
|
#include "denote_frontmatter.h"
|
|
#include "denote_metadata.h"
|
|
|
|
#include <KFileItem>
|
|
#include <KLocalizedString>
|
|
#include <KPluginFactory>
|
|
|
|
#include <QAction>
|
|
#include <QDialog>
|
|
#include <QDialogButtonBox>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <cstdio>
|
|
#include <QFormLayout>
|
|
#include <QComboBox>
|
|
#include <QLineEdit>
|
|
#include <QScreen>
|
|
#include <QVBoxLayout>
|
|
#include <QWidget>
|
|
|
|
K_PLUGIN_CLASS_WITH_JSON(DenoteFileItemAction, "denote_fileitemaction.json")
|
|
|
|
DenoteFileItemAction::DenoteFileItemAction(QObject *parent, const QVariantList &)
|
|
: KAbstractFileItemActionPlugin(parent)
|
|
{
|
|
}
|
|
|
|
QList<QAction *> DenoteFileItemAction::actions(const KFileItemListProperties &fileItemInfos,
|
|
QWidget *parentWidget)
|
|
{
|
|
if (fileItemInfos.items().count() != 1)
|
|
return {};
|
|
|
|
const KFileItem item = fileItemInfos.items().first();
|
|
|
|
// Directory: show "New Denote File..."
|
|
if (item.isDir()) {
|
|
auto *action = new QAction(i18n("New Denote File..."), parentWidget);
|
|
action->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
|
|
|
|
const QString dirPath = item.localPath();
|
|
connect(action, &QAction::triggered, this, [this, dirPath, parentWidget]() {
|
|
createDenoteFile(dirPath, parentWidget);
|
|
});
|
|
return {action};
|
|
}
|
|
|
|
// Show "Rename with Denote..." for any file
|
|
auto *action = new QAction(i18n("Rename with Denote..."), parentWidget);
|
|
action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename")));
|
|
|
|
const KFileItemList items = fileItemInfos.items();
|
|
connect(action, &QAction::triggered, this, [this, items, parentWidget]() {
|
|
renameWithDenote(items, parentWidget);
|
|
});
|
|
|
|
return {action};
|
|
}
|
|
|
|
void DenoteFileItemAction::renameWithDenote(const KFileItemList &items, QWidget *parentWidget)
|
|
{
|
|
if (items.isEmpty())
|
|
return;
|
|
|
|
const KFileItem item = items.first();
|
|
const QString path = item.localPath();
|
|
const QFileInfo fi(path);
|
|
|
|
// Preserve original extension (e.g. .txt, .tar.gz, or empty for no extension)
|
|
const QString suffix = fi.completeSuffix();
|
|
const QString ext = suffix.isEmpty() ? QString() : QStringLiteral(".") + suffix;
|
|
const Denote::FileType fileType = Denote::fileTypeFromExtension(ext);
|
|
|
|
// Parse existing denote filename metadata
|
|
Denote::Metadata meta = Denote::parseFilename(path);
|
|
QString defaultTitle;
|
|
QStringList defaultTags;
|
|
|
|
if (meta.isValid()) {
|
|
// File already follows denote naming
|
|
defaultTitle = meta.title;
|
|
defaultTags = meta.tags;
|
|
|
|
// For frontmatter-capable files, prefer frontmatter values
|
|
if (fileType != Denote::FileType::Unknown) {
|
|
QFile file(path);
|
|
if (file.open(QIODevice::ReadOnly)) {
|
|
QByteArray content = file.readAll();
|
|
file.close();
|
|
Denote::FrontMatter fm = Denote::parseFrontMatter(content, fileType);
|
|
if (!fm.title.isEmpty())
|
|
defaultTitle = fm.title;
|
|
if (!fm.tags.isEmpty())
|
|
defaultTags = fm.tags;
|
|
}
|
|
}
|
|
} else {
|
|
// Not a denote file — pre-fill title from filename
|
|
defaultTitle = fi.completeBaseName();
|
|
defaultTitle.replace(QLatin1Char('-'), QLatin1Char(' '));
|
|
defaultTitle.replace(QLatin1Char('_'), QLatin1Char(' '));
|
|
}
|
|
|
|
// Build dialog
|
|
QDialog dialog(parentWidget);
|
|
dialog.setWindowTitle(i18n("Rename with Denote"));
|
|
// Size to 33% of screen width (accounts for HiDPI)
|
|
if (auto *screen = dialog.screen()) {
|
|
dialog.setMinimumWidth(screen->availableGeometry().width() / 3);
|
|
}
|
|
|
|
auto *layout = new QVBoxLayout(&dialog);
|
|
auto *form = new QFormLayout();
|
|
|
|
auto *titleEdit = new QLineEdit(defaultTitle);
|
|
form->addRow(i18n("Title:"), titleEdit);
|
|
|
|
auto *tagsEdit = new QLineEdit(defaultTags.join(QStringLiteral(", ")));
|
|
tagsEdit->setPlaceholderText(i18n("tag1, tag2, tag3"));
|
|
form->addRow(i18n("Tags:"), tagsEdit);
|
|
|
|
auto *sigEdit = new QLineEdit();
|
|
sigEdit->setPlaceholderText(i18n("(optional)"));
|
|
form->addRow(i18n("Signature:"), sigEdit);
|
|
|
|
layout->addLayout(form);
|
|
|
|
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
|
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
|
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
|
layout->addWidget(buttons);
|
|
|
|
if (dialog.exec() != QDialog::Accepted)
|
|
return;
|
|
|
|
const QString title = titleEdit->text().trimmed();
|
|
if (title.isEmpty())
|
|
return;
|
|
|
|
QStringList tags;
|
|
const QString tagsText = tagsEdit->text().trimmed();
|
|
if (!tagsText.isEmpty()) {
|
|
tags = tagsText.split(QLatin1Char(','), Qt::SkipEmptyParts);
|
|
for (auto &t : tags)
|
|
t = t.trimmed().toLower();
|
|
}
|
|
|
|
const QString signature = sigEdit->text().trimmed();
|
|
|
|
// Build new filename
|
|
QString newName;
|
|
if (meta.isValid()) {
|
|
// Preserve existing identifier — reuse it with updated title/tags/signature
|
|
// Use Denote::buildFilename to get the slug and tag formatting, then swap the identifier
|
|
const QString baseName = Denote::buildFilename(title, tags, signature, ext);
|
|
// Replace the generated identifier with the existing one
|
|
newName = meta.identifier + baseName.mid(baseName.indexOf(QLatin1Char('-')));
|
|
} else {
|
|
newName = Denote::buildFilename(title, tags, signature, ext);
|
|
}
|
|
const QString newPath = fi.dir().filePath(newName);
|
|
|
|
// Rename (use ::rename for atomic, no-copy behavior)
|
|
::rename(path.toLocal8Bit().constData(), newPath.toLocal8Bit().constData());
|
|
|
|
// Update frontmatter if supported
|
|
if (fileType != Denote::FileType::Unknown) {
|
|
QFile file(newPath);
|
|
if (file.open(QIODevice::ReadWrite)) {
|
|
QByteArray content = file.readAll();
|
|
|
|
Denote::FrontMatter fm;
|
|
fm.title = title;
|
|
fm.tags = tags;
|
|
fm.identifier = Denote::parseFilename(newPath).identifier;
|
|
fm.signature = signature;
|
|
|
|
// Replace or prepend frontmatter
|
|
QString body = Denote::extractBody(content, fileType);
|
|
QByteArray newContent = Denote::marshalFrontMatter(fm, fileType);
|
|
newContent += body.toUtf8();
|
|
|
|
file.seek(0);
|
|
file.resize(0);
|
|
file.write(newContent);
|
|
file.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
void DenoteFileItemAction::createDenoteFile(const QString &dirPath, QWidget *parentWidget)
|
|
{
|
|
QDialog dialog(parentWidget);
|
|
dialog.setWindowTitle(i18n("New Denote File"));
|
|
if (auto *screen = dialog.screen()) {
|
|
dialog.setMinimumWidth(screen->availableGeometry().width() / 3);
|
|
}
|
|
|
|
auto *layout = new QVBoxLayout(&dialog);
|
|
auto *form = new QFormLayout();
|
|
|
|
auto *titleEdit = new QLineEdit();
|
|
titleEdit->setPlaceholderText(i18n("Note title"));
|
|
form->addRow(i18n("Title:"), titleEdit);
|
|
|
|
auto *tagsEdit = new QLineEdit();
|
|
tagsEdit->setPlaceholderText(i18n("tag1, tag2, tag3"));
|
|
form->addRow(i18n("Tags:"), tagsEdit);
|
|
|
|
auto *sigEdit = new QLineEdit();
|
|
sigEdit->setPlaceholderText(i18n("(optional)"));
|
|
form->addRow(i18n("Signature:"), sigEdit);
|
|
|
|
auto *extCombo = new QComboBox();
|
|
extCombo->addItem(i18n("Markdown"), QStringLiteral(".md"));
|
|
extCombo->addItem(i18n("Emacs org-mode"), QStringLiteral(".org"));
|
|
extCombo->addItem(i18n("Plain Text"), QStringLiteral(".txt"));
|
|
form->addRow(i18n("Format:"), extCombo);
|
|
|
|
layout->addLayout(form);
|
|
|
|
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
|
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
|
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
|
layout->addWidget(buttons);
|
|
|
|
if (dialog.exec() != QDialog::Accepted)
|
|
return;
|
|
|
|
const QString title = titleEdit->text().trimmed();
|
|
if (title.isEmpty())
|
|
return;
|
|
|
|
QStringList tags;
|
|
const QString tagsText = tagsEdit->text().trimmed();
|
|
if (!tagsText.isEmpty()) {
|
|
tags = tagsText.split(QLatin1Char(','), Qt::SkipEmptyParts);
|
|
for (auto &t : tags)
|
|
t = t.trimmed().toLower();
|
|
}
|
|
|
|
const QString signature = sigEdit->text().trimmed();
|
|
const QString ext = extCombo->currentData().toString();
|
|
|
|
// Build filename and full path
|
|
const QString filename = Denote::buildFilename(title, tags, signature, ext);
|
|
const QString filePath = QDir(dirPath).filePath(filename);
|
|
|
|
// Create file with frontmatter if supported
|
|
Denote::FileType fileType = Denote::fileTypeFromExtension(ext);
|
|
QFile file(filePath);
|
|
if (file.open(QIODevice::WriteOnly)) {
|
|
if (fileType != Denote::FileType::Unknown) {
|
|
Denote::FrontMatter fm;
|
|
fm.title = title;
|
|
fm.tags = tags;
|
|
fm.identifier = Denote::parseFilename(filePath).identifier;
|
|
fm.signature = signature;
|
|
file.write(Denote::marshalFrontMatter(fm, fileType));
|
|
}
|
|
file.close();
|
|
}
|
|
}
|
|
|
|
#include "denote_fileitemaction.moc"
|