Browse Source

support pasting image in markdown

Signed-off-by: Le Tan <[email protected]>
Le Tan 9 years ago
parent
commit
047c26b598

+ 120 - 0
src/dialog/vinsertimagedialog.cpp

@@ -0,0 +1,120 @@
+#include <QtWidgets>
+#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);
+}

+ 49 - 0
src/dialog/vinsertimagedialog.h

@@ -0,0 +1,49 @@
+#ifndef VINSERTIMAGEDIALOG_H
+#define VINSERTIMAGEDIALOG_H
+
+#include <QDialog>
+#include <QImage>
+#include <QString>
+
+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

+ 8 - 2
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

+ 22 - 0
src/utils/vutils.cpp

@@ -1,6 +1,8 @@
 #include "vutils.h"
 #include <QFile>
+#include <QDir>
 #include <QDebug>
+#include <QRegularExpression>
 
 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;
+}

+ 2 - 0
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

+ 39 - 0
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);
+}

+ 5 - 0
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;
 };
 
 

+ 16 - 0
src/veditoperations.cpp

@@ -0,0 +1,16 @@
+#include <QTextCursor>
+#include <QTextDocument>
+#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);
+}

+ 21 - 0
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

+ 3 - 11
src/vmainwindow.cpp

@@ -88,17 +88,9 @@ void VMainWindow::setupUI()
     mainSplitter->addWidget(nbContainer);
     mainSplitter->addWidget(fileList);
     mainSplitter->addWidget(tabs);
-    QList<int> 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,

+ 102 - 0
src/vmdeditoperations.cpp

@@ -0,0 +1,102 @@
+#include <QtDebug>
+#include <QImage>
+#include <QVariant>
+#include <QMimeData>
+#include <QObject>
+#include <QWidget>
+#include <QImageReader>
+#include <QDir>
+#include <QMessageBox>
+#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<QImage>(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;
+}

+ 17 - 0
src/vmdeditoperations.h

@@ -0,0 +1,17 @@
+#ifndef VMDEDITOPERATIONS_H
+#define VMDEDITOPERATIONS_H
+
+#include <QObject>
+#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

+ 1 - 3
src/vtabwidget.cpp

@@ -69,9 +69,7 @@ void VTabWidget::openFile(QJsonObject fileJson)
 out:
     setCurrentIndex(idx);
     if (mode == OpenFileMode::Edit) {
-        VEditor *editor = dynamic_cast<VEditor *>(currentWidget());
-        Q_ASSERT(editor);
-        editor->editFile();
+        editFile();
     }
 }