#include "denote_fileitemaction.h" #include "denote_frontmatter.h" #include "denote_metadata.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_CLASS_WITH_JSON(DenoteFileItemAction, "denote_fileitemaction.json") DenoteFileItemAction::DenoteFileItemAction(QObject *parent, const QVariantList &) : KAbstractFileItemActionPlugin(parent) { } QList 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"