diff --git a/src/dialog/vinsertimagedialog.cpp b/src/dialog/vinsertimagedialog.cpp new file mode 100644 index 00000000..eb12598f --- /dev/null +++ b/src/dialog/vinsertimagedialog.cpp @@ -0,0 +1,120 @@ +#include +#include "vinsertimagedialog.h" + +VInsertImageDialog::VInsertImageDialog(const QString &title, const QString &defaultImageTitle, + const QString &defaultPath, QWidget *parent) + : QDialog(parent), title(title), defaultImageTitle(defaultImageTitle), defaultPath(defaultPath), + image(NULL), browseable(true) +{ + setupUI(); + + connect(imageTitleEdit, &QLineEdit::textChanged, this, &VInsertImageDialog::enableOkButton); + connect(pathEdit, &QLineEdit::textChanged, this, &VInsertImageDialog::enableOkButton); + connect(browseBtn, &QPushButton::clicked, this, &VInsertImageDialog::handleBrowseBtnClicked); + connect(okBtn, &QPushButton::clicked, this, &VInsertImageDialog::accept); + connect(cancelBtn, &QPushButton::clicked, this, &VInsertImageDialog::reject); + + enableOkButton(); +} + +VInsertImageDialog::~VInsertImageDialog() +{ + if (image) { + delete image; + image = NULL; + } +} + +void VInsertImageDialog::setupUI() +{ + pathLabel = new QLabel(tr("&From")); + pathEdit = new QLineEdit(defaultPath); + pathLabel->setBuddy(pathEdit); + browseBtn = new QPushButton(tr("&Browse")); + QHBoxLayout *pathLayout = new QHBoxLayout(); + pathLayout->addWidget(pathEdit); + pathLayout->addWidget(browseBtn); + + imageTitleLabel = new QLabel(tr("&Title")); + imageTitleEdit = new QLineEdit(defaultImageTitle); + imageTitleEdit->selectAll(); + imageTitleLabel->setBuddy(imageTitleEdit); + + okBtn = new QPushButton(tr("&OK")); + okBtn->setDefault(true); + cancelBtn = new QPushButton(tr("&Cancel")); + + QVBoxLayout *topLayout = new QVBoxLayout(); + topLayout->addWidget(pathLabel); + topLayout->addLayout(pathLayout); + topLayout->addWidget(imageTitleLabel); + topLayout->addWidget(imageTitleEdit); + + QHBoxLayout *btmLayout = new QHBoxLayout(); + btmLayout->addStretch(); + btmLayout->addWidget(okBtn); + btmLayout->addWidget(cancelBtn); + + imagePreviewLabel = new QLabel(); + imagePreviewLabel->setVisible(false); + + QVBoxLayout *mainLayout = new QVBoxLayout(); + mainLayout->addLayout(topLayout); + mainLayout->addLayout(btmLayout); + mainLayout->addWidget(imagePreviewLabel); + setLayout(mainLayout); + layout()->setSizeConstraint(QLayout::SetFixedSize); + setWindowTitle(title); +} + +void VInsertImageDialog::enableOkButton() +{ + bool enabled = true; + if (imageTitleEdit->text().isEmpty() || !image) { + enabled = false; + } + okBtn->setEnabled(enabled); +} + +QString VInsertImageDialog::getImageTitleInput() const +{ + return imageTitleEdit->text(); +} + +QString VInsertImageDialog::getPathInput() const +{ + return pathEdit->text(); +} + +void VInsertImageDialog::handleBrowseBtnClicked() +{ + static QString lastPath = QDir::homePath(); + QString filePath = QFileDialog::getOpenFileName(this, tr("Select the image to be inserted"), + lastPath, tr("Images (*.png *.xpm *.jpg *.bmp *.gif)")); + // Update lastPath + lastPath = QFileInfo(filePath).path(); + + pathEdit->setText(filePath); +} + +void VInsertImageDialog::setImage(const QImage &image) +{ + Q_ASSERT(!image.isNull()); + QSize previewSize(256, 256); + if (!this->image) { + this->image = new QImage(image); + } else { + *(this->image) = image; + } + imagePreviewLabel->setPixmap(QPixmap::fromImage(this->image->scaled(previewSize))); + imagePreviewLabel->setVisible(true); + enableOkButton(); +} + +void VInsertImageDialog::setBrowseable(bool browseable) +{ + this->browseable = browseable; + pathLabel->setVisible(browseable); + pathEdit->setVisible(browseable); + browseBtn->setVisible(browseable); +} diff --git a/src/dialog/vinsertimagedialog.h b/src/dialog/vinsertimagedialog.h new file mode 100644 index 00000000..04a9985a --- /dev/null +++ b/src/dialog/vinsertimagedialog.h @@ -0,0 +1,49 @@ +#ifndef VINSERTIMAGEDIALOG_H +#define VINSERTIMAGEDIALOG_H + +#include +#include +#include + +class QLabel; +class QLineEdit; +class QPushButton; + +class VInsertImageDialog : public QDialog +{ + Q_OBJECT +public: + VInsertImageDialog(const QString &title, const QString &defaultImageTitle, + const QString &defaultPath, + QWidget *parent = 0); + ~VInsertImageDialog(); + QString getImageTitleInput() const; + QString getPathInput() const; + + void setImage(const QImage &image); + void setBrowseable(bool browseable); + +private slots: + void enableOkButton(); + void handleBrowseBtnClicked(); + +private: + void setupUI(); + + QLabel *imageTitleLabel; + QLineEdit *imageTitleEdit; + QLabel *pathLabel; + QLineEdit *pathEdit; + QPushButton *browseBtn; + QPushButton *okBtn; + QPushButton *cancelBtn; + QLabel *imagePreviewLabel; + + QString title; + QString defaultImageTitle; + QString defaultPath; + QImage *image; + bool browseable; +}; + +#endif // VINSERTIMAGEDIALOG_H diff --git a/src/src.pro b/src/src.pro index 8ddb744f..7bc6e0f6 100644 --- a/src/src.pro +++ b/src/src.pro @@ -36,7 +36,10 @@ SOURCES += main.cpp\ vmarkdownconverter.cpp \ dialog/vnotebookinfodialog.cpp \ dialog/vdirinfodialog.cpp \ - dialog/vfileinfodialog.cpp + dialog/vfileinfodialog.cpp \ + veditoperations.cpp \ + vmdeditoperations.cpp \ + dialog/vinsertimagedialog.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -63,7 +66,10 @@ HEADERS += vmainwindow.h \ vmarkdownconverter.h \ dialog/vnotebookinfodialog.h \ dialog/vdirinfodialog.h \ - dialog/vfileinfodialog.h + dialog/vfileinfodialog.h \ + veditoperations.h \ + vmdeditoperations.h \ + dialog/vinsertimagedialog.h RESOURCES += \ vnote.qrc diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 64f85678..3f9f3817 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -1,6 +1,8 @@ #include "vutils.h" #include +#include #include +#include VUtils::VUtils() { @@ -54,3 +56,23 @@ QRgb VUtils::QRgbFromString(const QString &str) qWarning() << "error: fail to construct QRgb from string" << str; return QRgb(); } + +QString VUtils::generateImageFileName(const QString &path, const QString &title, + const QString &format) +{ + Q_ASSERT(!title.isEmpty()); + QRegularExpression regExp("[^a-zA-Z0-9_]+"); + QString baseName(title.toLower()); + baseName.replace(regExp, "_"); + baseName.prepend('_'); + QString imageName = baseName + "." + format.toLower(); + QString filePath = QDir(path).filePath(imageName); + int index = 1; + + while (QFile(filePath).exists()) { + imageName = QString("%1_%2.%3").arg(baseName).arg(index++) + .arg(format.toLower()); + filePath = QDir(path).filePath(imageName); + } + return imageName; +} diff --git a/src/utils/vutils.h b/src/utils/vutils.h index be4fef65..43f99618 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -14,6 +14,8 @@ public: static bool writeFileToDisk(const QString &filePath, const QString &text); // Transform FFFFFF string to QRgb static QRgb QRgbFromString(const QString &str); + static QString generateImageFileName(const QString &path, const QString &title, + const QString &format = "png"); }; #endif // VUTILS_H diff --git a/src/vedit.cpp b/src/vedit.cpp index d6fcf813..fcad2dec 100644 --- a/src/vedit.cpp +++ b/src/vedit.cpp @@ -3,6 +3,7 @@ #include "vnote.h" #include "vconfigmanager.h" #include "hgmarkdownhighlighter.h" +#include "vmdeditoperations.h" extern VConfigManager vconfig; @@ -13,14 +14,24 @@ VEdit::VEdit(VNoteFile *noteFile, QWidget *parent) setAcceptRichText(false); mdHighlighter = new HGMarkdownHighlighter(vconfig.getMdHighlightingStyles(), 500, document()); + editOps = new VMdEditOperations(this, noteFile); } else { setAutoFormatting(QTextEdit::AutoBulletList); + editOps = NULL; } updateTabSettings(); updateFontAndPalette(); } +VEdit::~VEdit() +{ + if (editOps) { + delete editOps; + editOps = NULL; + } +} + void VEdit::updateFontAndPalette() { switch (noteFile->docType) { @@ -125,3 +136,31 @@ void VEdit::keyPressEvent(QKeyEvent *event) } QTextEdit::keyPressEvent(event); } + +bool VEdit::canInsertFromMimeData(const QMimeData *source) const +{ + return source->hasImage() || source->hasUrls() + || QTextEdit::canInsertFromMimeData(source); +} + +void VEdit::insertFromMimeData(const QMimeData *source) +{ + if (source->hasImage()) { + // Image data in the clipboard + if (editOps) { + bool ret = editOps->insertImageFromMimeData(source); + if (ret) { + return; + } + } + } else if (source->hasUrls()) { + // Paste an image file + if (editOps) { + bool ret = editOps->insertURLFromMimeData(source); + if (ret) { + return; + } + } + } + QTextEdit::insertFromMimeData(source); +} diff --git a/src/vedit.h b/src/vedit.h index a5ebfa83..80fefb8c 100644 --- a/src/vedit.h +++ b/src/vedit.h @@ -7,12 +7,14 @@ #include "vnotefile.h" class HGMarkdownHighlighter; +class VEditOperations; class VEdit : public QTextEdit { Q_OBJECT public: VEdit(VNoteFile *noteFile, QWidget *parent = 0); + ~VEdit(); void beginEdit(); // Save buffer content to noteFile->content. @@ -25,6 +27,8 @@ public: protected: void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; + bool canInsertFromMimeData(const QMimeData *source) const Q_DECL_OVERRIDE; + void insertFromMimeData(const QMimeData *source) Q_DECL_OVERRIDE; private: void updateTabSettings(); @@ -34,6 +38,7 @@ private: QString tabSpaces; VNoteFile *noteFile; HGMarkdownHighlighter *mdHighlighter; + VEditOperations *editOps; }; diff --git a/src/veditoperations.cpp b/src/veditoperations.cpp new file mode 100644 index 00000000..c1540b3f --- /dev/null +++ b/src/veditoperations.cpp @@ -0,0 +1,16 @@ +#include +#include +#include "vedit.h" +#include "veditoperations.h" + +VEditOperations::VEditOperations(VEdit *editor, VNoteFile *noteFile) + : editor(editor), noteFile(noteFile) +{ +} + +void VEditOperations::insertTextAtCurPos(const QString &text) +{ + QTextCursor cursor(editor->document()); + cursor.setPosition(editor->textCursor().position()); + cursor.insertText(text); +} diff --git a/src/veditoperations.h b/src/veditoperations.h new file mode 100644 index 00000000..912b7e78 --- /dev/null +++ b/src/veditoperations.h @@ -0,0 +1,21 @@ +#ifndef VEDITOPERATIONS_H +#define VEDITOPERATIONS_H + +class VNoteFile; +class VEdit; +class QMimeData; + +class VEditOperations +{ +public: + VEditOperations(VEdit *editor, VNoteFile *noteFile); + virtual bool insertImageFromMimeData(const QMimeData *source) = 0; + virtual bool insertURLFromMimeData(const QMimeData *source) = 0; + +protected: + void insertTextAtCurPos(const QString &text); + VEdit *editor; + VNoteFile *noteFile; +}; + +#endif // VEDITOPERATIONS_H diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index e455bc54..15325f92 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -88,17 +88,9 @@ void VMainWindow::setupUI() mainSplitter->addWidget(nbContainer); mainSplitter->addWidget(fileList); mainSplitter->addWidget(tabs); - QList sizes; - int sa = nbContainer->minimumSizeHint().width(); - int sb = fileList->minimumSizeHint().width(); - int sc = qMax(mainSplitter->sizeHint().width() - sa - sb, sa + sb); - sizes.append(sa); - sizes.append(sb); - sizes.append(sc); - mainSplitter->setSizes(sizes); - mainSplitter->setStretchFactor(0, 1); - mainSplitter->setStretchFactor(1, 1); - mainSplitter->setStretchFactor(2, 200); + mainSplitter->setStretchFactor(0, 0); + mainSplitter->setStretchFactor(1, 0); + mainSplitter->setStretchFactor(2, 1); // Signals connect(notebookComboBox, SIGNAL(currentIndexChanged(int)), this, diff --git a/src/vmdeditoperations.cpp b/src/vmdeditoperations.cpp new file mode 100644 index 00000000..b89e6829 --- /dev/null +++ b/src/vmdeditoperations.cpp @@ -0,0 +1,102 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "vmdeditoperations.h" +#include "dialog/vinsertimagedialog.h" +#include "vnotefile.h" +#include "utils/vutils.h" +#include "vedit.h" + +VMdEditOperations::VMdEditOperations(VEdit *editor, VNoteFile *noteFile) + : VEditOperations(editor, noteFile) +{ +} + +bool VMdEditOperations::insertImageFromMimeData(const QMimeData *source) +{ + QImage image = qvariant_cast(source->imageData()); + if (image.isNull()) { + return false; + } + VInsertImageDialog dialog(QObject::tr("Insert image from clipboard"), QObject::tr("image_title"), + "", (QWidget *)editor); + dialog.setBrowseable(false); + dialog.setImage(image); + if (dialog.exec() == QDialog::Accepted) { + QString title = dialog.getImageTitleInput(); + QString path = QDir::cleanPath(QDir(noteFile->basePath).filePath("images")); + QString fileName = VUtils::generateImageFileName(path, title); + qDebug() << "insert image" << path << title << fileName; + QString filePath = QDir(path).filePath(fileName); + Q_ASSERT(!QFile(filePath).exists()); + bool ret = image.save(filePath); + if (!ret) { + QMessageBox msgBox(QMessageBox::Warning, QObject::tr("Warning"), QString("Fail to save image %1").arg(filePath), + QMessageBox::Ok, (QWidget *)editor); + msgBox.exec(); + return true; + } + + QString md = QString("![%1](images/%2)").arg(title).arg(fileName); + insertTextAtCurPos(md); + } + return true; +} + +bool VMdEditOperations::insertImageFromPath(const QString &imagePath) +{ + QImage image(imagePath); + if (image.isNull()) { + qWarning() << "error: image is null"; + return false; + } + VInsertImageDialog dialog(QObject::tr("Insert image from file"), QObject::tr("image_title"), + "", (QWidget *)editor); + dialog.setBrowseable(false); + dialog.setImage(image); + if (dialog.exec() == QDialog::Accepted) { + QString title = dialog.getImageTitleInput(); + QString path = QDir::cleanPath(QDir(noteFile->basePath).filePath("images")); + QString fileName = VUtils::generateImageFileName(path, title, QFileInfo(imagePath).suffix()); + qDebug() << "insert image" << path << title << fileName; + QString filePath = QDir(path).filePath(fileName); + Q_ASSERT(!QFile(filePath).exists()); + bool ret = QFile::copy(imagePath, filePath); + if (!ret) { + qWarning() << "error: fail to copy" << imagePath << "to" << filePath; + QMessageBox msgBox(QMessageBox::Warning, QObject::tr("Warning"), QString("Fail to save image %1").arg(filePath), + QMessageBox::Ok, (QWidget *)editor); + msgBox.exec(); + return false; + } + + QString md = QString("![%1](images/%2)").arg(title).arg(fileName); + insertTextAtCurPos(md); + } + return true; +} + +bool VMdEditOperations::insertURLFromMimeData(const QMimeData *source) +{ + foreach (QUrl url, source->urls()) { + if (url.isLocalFile()) { + QFileInfo info(url.toLocalFile()); + if (QImageReader::supportedImageFormats().contains(info.suffix().toLower().toLatin1())) { + insertImageFromPath(info.filePath()); + } else { + insertTextAtCurPos(url.toLocalFile()); + } + } else { + // TODO: download http image + // Just insert the URL for non-image + insertTextAtCurPos(url.toString()); + } + } + return true; +} diff --git a/src/vmdeditoperations.h b/src/vmdeditoperations.h new file mode 100644 index 00000000..4838e1a6 --- /dev/null +++ b/src/vmdeditoperations.h @@ -0,0 +1,17 @@ +#ifndef VMDEDITOPERATIONS_H +#define VMDEDITOPERATIONS_H + +#include +#include "veditoperations.h" + +// Editor operations for Markdown +class VMdEditOperations : public VEditOperations +{ +public: + VMdEditOperations(VEdit *editor, VNoteFile *noteFile); + bool insertImageFromMimeData(const QMimeData *source) Q_DECL_OVERRIDE; + bool insertURLFromMimeData(const QMimeData *source) Q_DECL_OVERRIDE; + bool insertImageFromPath(const QString &imagePath); +}; + +#endif // VMDEDITOPERATIONS_H diff --git a/src/vtabwidget.cpp b/src/vtabwidget.cpp index f713116b..a655ab64 100644 --- a/src/vtabwidget.cpp +++ b/src/vtabwidget.cpp @@ -69,9 +69,7 @@ void VTabWidget::openFile(QJsonObject fileJson) out: setCurrentIndex(idx); if (mode == OpenFileMode::Edit) { - VEditor *editor = dynamic_cast(currentWidget()); - Q_ASSERT(editor); - editor->editFile(); + editFile(); } }