Browse Source

support copy and paste notes

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

+ 11 - 0
src/resources/icons/copy.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<polygon points="304,96 288,96 288,176 368,176 368,160 304,160 	"/>
+	<path d="M325.3,64H160v48h-48v336h240v-48h48V139L325.3,64z M336,432H128V128h32v272h176V432z M384,384H176V80h142.7l65.3,65.6V384
+		z"/>
+</g>
+</svg>

+ 23 - 0
src/resources/icons/cut.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<path d="M405.178,115.667c13.314-32.667,17.309-64-5.326-83.667L255.726,224l-16.976,23c0,0-27.627,40.011-37.28,58.667
+		s-19.306,39.333-27.294,54c-7.01,12.871-10.438,15.221-14.322,11.548c-0.506-0.591-1.026-1.168-1.553-1.736
+		c-0.037-0.047-0.073-0.09-0.11-0.138c-1.143-1.472-2.75-3.002-4.635-4.467C144.195,356.795,132.548,352,119.92,352
+		C89.037,352,64,380.653,64,416s25.037,64,55.92,64c25.282,0,46.635-19.205,53.553-45.561l-0.004,0.043
+		c0,0,13.355-41.482,32.661-71.482c19.306-30,49.596-43,49.596-43l31.954-32C287.68,288,391.863,148.334,405.178,115.667z
+		 M119.92,448c-15.418,0-27.918-14.353-27.918-32s12.5-32,27.918-32c15.419,0,27.918,14.353,27.918,32S135.339,448,119.92,448z
+		 M256,288c-8.836,0-16-7.163-16-16c0-8.836,7.164-16,16-16c8.837,0,16,7.164,16,16C272,280.837,264.837,288,256,288z"/>
+	<path d="M207.28,265.255c9.18-14.114,17.671-26.43,18.304-27.346l0.143-0.208l0.15-0.203l16.976-23l0.038-0.052l0.039-0.052
+		l2.941-3.918L111.896,32c-22.634,19.667-18.64,51-5.326,83.667C116.523,140.087,177.249,224.29,207.28,265.255z"/>
+	<path d="M391.828,352c-12.628,0-24.275,4.795-33.637,12.874c-1.885,1.465-3.492,2.995-4.635,4.467
+		c-0.037,0.048-0.072,0.091-0.109,0.138c-0.526,0.568-1.047,1.146-1.553,1.736c-3.884,3.673-7.312,1.323-14.322-11.548
+		c-7.988-14.667-17.641-35.344-27.294-54c-1.77-3.421-4.146-7.561-6.843-12.038c-1.272,1.712-2.264,3.043-2.932,3.938l-0.688,0.924
+		l-0.813,0.815l-28.688,28.729c10.433,6.855,24.565,18.276,35.306,34.965c19.305,30,32.66,71.482,32.66,71.482l-0.004-0.043
+		C345.193,460.795,366.546,480,391.828,480C422.711,480,448,451.347,448,416S422.711,352,391.828,352z M391.828,448
+		c-15.42,0-27.918-14.353-27.918-32s12.498-32,27.918-32c15.418,0,27.918,14.353,27.918,32S407.246,448,391.828,448z"/>
+</g>
+</svg>

+ 14 - 0
src/resources/icons/paste.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<path d="M255.988,32C160.473,32,78.934,91.804,46.727,176h34.639c9.396-20.484,22.457-39.35,38.868-55.762
+		C156.497,83.973,204.709,64,255.988,64c51.286,0,99.504,19.973,135.771,56.239C428.027,156.505,448,204.719,448,256
+		c0,51.285-19.973,99.501-56.239,135.765C355.494,428.029,307.275,448,255.988,448c-51.281,0-99.493-19.971-135.755-56.234
+		C103.821,375.354,90.76,356.486,81.362,336H46.725c32.206,84.201,113.746,144,209.264,144C379.703,480,480,379.715,480,256
+		C480,132.298,379.703,32,255.988,32z"/>
+	<polygon points="206.863,323.883 229.49,346.51 320,256 229.49,165.49 206.862,188.118 258.745,240 32,240 32,272 258.745,272 	"/>
+</g>
+</svg>

+ 4 - 2
src/src.pro

@@ -42,7 +42,8 @@ SOURCES += main.cpp\
     veditwindow.cpp \
     vedittab.cpp \
     voutline.cpp \
-    vtoc.cpp
+    vtoc.cpp \
+    vfilelocation.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -73,7 +74,8 @@ HEADERS  += vmainwindow.h \
     veditwindow.h \
     vedittab.h \
     voutline.h \
-    vtoc.h
+    vtoc.h \
+    vfilelocation.h
 
 RESOURCES += \
     vnote.qrc

+ 44 - 0
src/utils/vutils.cpp

@@ -4,6 +4,11 @@
 #include <QDebug>
 #include <QRegularExpression>
 #include <QRegExp>
+#include <QClipboard>
+#include <QApplication>
+#include <QMimeData>
+#include <QJsonObject>
+#include <QJsonDocument>
 
 VUtils::VUtils()
 {
@@ -159,3 +164,42 @@ void VUtils::makeDirectory(const QString &path)
         qDebug() << "mkdir" << path;
     }
 }
+
+ClipboardOpType VUtils::opTypeInClipboard()
+{
+    QClipboard *clipboard = QApplication::clipboard();
+    const QMimeData *mimeData = clipboard->mimeData();
+
+    if (mimeData->hasText()) {
+        QString text = mimeData->text();
+        QJsonObject clip = QJsonDocument::fromJson(text.toLocal8Bit()).object();
+        if (clip.contains("operation")) {
+            return (ClipboardOpType)clip["operation"].toInt();
+        }
+    }
+    return ClipboardOpType::Invalid;
+}
+
+bool VUtils::copyFile(const QString &p_srcFilePath, const QString &p_destFilePath, bool p_isCut)
+{
+    QString srcPath = QDir::cleanPath(p_srcFilePath);
+    QString destPath = QDir::cleanPath(p_destFilePath);
+
+    if (srcPath == destPath) {
+        return true;
+    }
+
+    if (p_isCut) {
+        QFile file(srcPath);
+        if (!file.rename(destPath)) {
+            qWarning() << "error: fail to copy file" << srcPath << destPath;
+            return false;
+        }
+    } else {
+        if (!QFile::copy(srcPath, destPath)) {
+            qWarning() << "error: fail to copy file" << srcPath << destPath;
+            return false;
+        }
+    }
+    return true;
+}

+ 3 - 0
src/utils/vutils.h

@@ -6,6 +6,7 @@
 #include <QVector>
 #include <QPair>
 #include "vconfigmanager.h"
+#include "vconstants.h"
 
 class VUtils
 {
@@ -25,6 +26,8 @@ public:
     static QString basePathFromPath(const QString &path);
     static QVector<QString> imagesFromMarkdownFile(const QString &filePath);
     static void makeDirectory(const QString &path);
+    static ClipboardOpType opTypeInClipboard();
+    static bool copyFile(const QString &p_srcFilePath, const QString &p_destFilePath, bool p_isCut);
 private:
     static inline void addQssVarToMap(QVector<QPair<QString, QString> > &map,
                                       const QString &key, const QString &value);

+ 1 - 0
src/vconstants.h

@@ -2,5 +2,6 @@
 #define VCONSTANTS_H
 
 enum class DocType { Html, Markdown };
+enum class ClipboardOpType { Invalid, CopyFile, CopyDir };
 
 #endif

+ 4 - 3
src/veditarea.cpp

@@ -242,13 +242,14 @@ void VEditArea::handleDirectoryRenamed(const QString &notebook, const QString &o
     updateWindowStatus();
 }
 
-void VEditArea::handleFileRenamed(const QString &notebook,
-                                  const QString &oldRelativePath, const QString &newRelativePath)
+void VEditArea::handleFileRenamed(const QString &p_srcNotebook, const QString &p_srcRelativePath,
+                                  const QString &p_destNotebook, const QString &p_destRelativePath)
 {
+    qDebug() << "fileRenamed" << p_srcNotebook << p_srcRelativePath << p_destNotebook << p_destRelativePath;
     int nrWin = splitter->count();
     for (int i = 0; i < nrWin; ++i) {
         VEditWindow *win = getWindow(i);
-        win->handleFileRenamed(notebook, oldRelativePath, newRelativePath);
+        win->handleFileRenamed(p_srcNotebook, p_srcRelativePath, p_destNotebook, p_destRelativePath);
     }
     updateWindowStatus();
 }

+ 2 - 2
src/veditarea.h

@@ -44,8 +44,8 @@ public slots:
     void handleOutlineItemActivated(const VAnchor &anchor);
     void handleDirectoryRenamed(const QString &notebook,
                                 const QString &oldRelativePath, const QString &newRelativePath);
-    void handleFileRenamed(const QString &notebook,
-                           const QString &oldRelativePath, const QString &newRelativePath);
+    void handleFileRenamed(const QString &p_srcNotebook, const QString &p_srcRelativePath,
+                           const QString &p_destNotebook, const QString &p_destRelativePath);
 
 private slots:
     void handleSplitWindowRequest(VEditWindow *curWindow);

+ 8 - 8
src/veditwindow.cpp

@@ -280,23 +280,23 @@ void VEditWindow::handleDirectoryRenamed(const QString &notebook, const QString
     updateTabListMenu();
 }
 
-void VEditWindow::handleFileRenamed(const QString &notebook, const QString &oldRelativePath,
-                                    const QString &newRelativePath)
+void VEditWindow::handleFileRenamed(const QString &p_srcNotebook, const QString &p_srcRelativePath,
+                                    const QString &p_destNotebook, const QString &p_destRelativePath)
 {
     QTabBar *tabs = tabBar();
     int nrTabs = tabs->count();
     for (int i = 0; i < nrTabs; ++i) {
         QJsonObject tabJson = tabs->tabData(i).toJsonObject();
-        if (tabJson["notebook"].toString() == notebook) {
+        if (tabJson["notebook"].toString() == p_srcNotebook) {
             QString relativePath = tabJson["relative_path"].toString();
-            if (relativePath == oldRelativePath) {
+            if (relativePath == p_srcRelativePath) {
                 VEditTab *tab = getTab(i);
-                relativePath = newRelativePath;
-                tabJson["relative_path"] = relativePath;
+                tabJson["notebook"] = p_destNotebook;
+                tabJson["relative_path"] = p_destRelativePath;
                 tabs->setTabData(i, tabJson);
                 tabs->setTabToolTip(i, generateTooltip(tabJson));
-                tabs->setTabText(i, generateTabText(VUtils::fileNameFromPath(relativePath), tab->isModified()));
-                QString path = QDir::cleanPath(QDir(vnote->getNotebookPath(notebook)).filePath(relativePath));
+                tabs->setTabText(i, generateTabText(VUtils::fileNameFromPath(p_destRelativePath), tab->isModified()));
+                QString path = QDir::cleanPath(QDir(vnote->getNotebookPath(p_destNotebook)).filePath(p_destRelativePath));
                 tab->updatePath(path);
             }
         }

+ 2 - 2
src/veditwindow.h

@@ -39,8 +39,8 @@ public:
     void scrollCurTab(const VAnchor &anchor);
     void handleDirectoryRenamed(const QString &notebook,
                                 const QString &oldRelativePath, const QString &newRelativePath);
-    void handleFileRenamed(const QString &notebook,
-                           const QString &oldRelativePath, const QString &newRelativePath);
+    void handleFileRenamed(const QString &p_srcNotebook, const QString &p_srcRelativePath,
+                           const QString &p_destNotebook, const QString &p_destRelativePath);
 
 protected:
     void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE;

+ 280 - 112
src/vfilelist.cpp

@@ -51,6 +51,24 @@ void VFileList::initActions()
     fileInfoAct->setStatusTip(tr("View and edit current note's information"));
     connect(fileInfoAct, &QAction::triggered,
             this, &VFileList::curFileInfo);
+
+    copyAct = new QAction(QIcon(":/resources/icons/copy.svg"),
+                          tr("&Copy"), this);
+    copyAct->setStatusTip(tr("Copy selected notes"));
+    connect(copyAct, &QAction::triggered,
+            this, &VFileList::copySelectedFiles);
+
+    cutAct = new QAction(QIcon(":/resources/icons/cut.svg"),
+                          tr("&Cut"), this);
+    cutAct->setStatusTip(tr("Cut selected notes"));
+    connect(cutAct, &QAction::triggered,
+            this, &VFileList::cutSelectedFiles);
+
+    pasteAct = new QAction(QIcon(":/resources/icons/paste.svg"),
+                          tr("&Paste"), this);
+    pasteAct->setStatusTip(tr("Paste notes"));
+    connect(pasteAct, &QAction::triggered,
+            this, &VFileList::pasteFilesInCurDir);
 }
 
 void VFileList::setDirectory(QJsonObject dirJson)
@@ -88,6 +106,7 @@ void VFileList::updateFileList()
 {
     QString path = QDir(rootPath).filePath(relativePath);
 
+    fileList->clear();
     if (!QDir(path).exists()) {
         qDebug() << "invalid notebook directory:" << path;
         QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), tr("Invalid notebook directory."),
@@ -144,7 +163,8 @@ void VFileList::fileInfo(const QString &p_notebook, const QString &p_relativePat
                 defaultName = name;
                 continue;
             }
-            renameFile(p_notebook, p_relativePath, name);
+            copyFile(p_notebook, p_relativePath, p_notebook,
+                     QDir(VUtils::basePathFromPath(p_relativePath)).filePath(name), true);
         }
         break;
     } while (true);
@@ -252,8 +272,13 @@ void VFileList::contextMenuRequested(QPoint pos)
     if (item) {
         menu.addAction(deleteFileAct);
         menu.addAction(fileInfoAct);
+        menu.addAction(copyAct);
+        menu.addAction(cutAct);
     }
 
+    if (VUtils::opTypeInClipboard() == ClipboardOpType::CopyFile) {
+        menu.addAction(pasteAct);
+    }
     menu.exec(fileList->mapToGlobal(pos));
 }
 
@@ -306,52 +331,20 @@ QListWidgetItem* VFileList::createFileAndUpdateList(const QString &name)
     file.close();
     qDebug() << "create file:" << filePath;
 
-    // Update current directory's config file to include this new file
-    QJsonObject dirJson = VConfigManager::readDirectoryConfig(path);
-    Q_ASSERT(!dirJson.isEmpty());
-    QJsonObject fileJson;
-    fileJson["name"] = name;
-    QJsonArray fileArray = dirJson["files"].toArray();
-    fileArray.push_front(fileJson);
-    dirJson["files"] = fileArray;
-    if (!VConfigManager::writeDirectoryConfig(path, dirJson)) {
-        qWarning() << "error: fail to update directory's configuration file to add a new file"
-                   << name;
+    if (!addFileInConfig(filePath, 0)) {
         file.remove();
         return NULL;
     }
 
-    return insertFileListItem(fileJson, true);
+    return insertFileListItem(readFileInConfig(filePath), true);
 }
 
 void VFileList::deleteFileAndUpdateList(const QString &p_notebook,
                                         const QString &p_relativePath)
 {
-    QString path = QDir(vnote->getNotebookPath(p_notebook)).filePath(VUtils::basePathFromPath(p_relativePath));
-    QString fileName = VUtils::fileNameFromPath(p_relativePath);
-    QString filePath = QDir(path).filePath(fileName);
+    QString filePath = QDir(vnote->getNotebookPath(p_notebook)).filePath(p_relativePath);
 
-    // Update current directory's config file to exclude this file
-    QJsonObject dirJson = VConfigManager::readDirectoryConfig(path);
-    Q_ASSERT(!dirJson.isEmpty());
-    QJsonArray fileArray = dirJson["files"].toArray();
-    bool deleted = false;
-    for (int i = 0; i < fileArray.size(); ++i) {
-        QJsonObject ele = fileArray[i].toObject();
-        if (ele["name"].toString() == fileName) {
-            fileArray.removeAt(i);
-            deleted = true;
-            break;
-        }
-    }
-    if (!deleted) {
-        qWarning() << "error: fail to find" << fileName << "to delete";
-        return;
-    }
-    dirJson["files"] = fileArray;
-    if (!VConfigManager::writeDirectoryConfig(path, dirJson)) {
-        qWarning() << "error: fail to update directory's configuration file to delete"
-                   << fileName;
+    if (!removeFileInConfig(filePath)) {
         return;
     }
 
@@ -446,116 +439,291 @@ void VFileList::handleDirectoryRenamed(const QString &notebook,
     }
 }
 
-// @p_relativePath contains the flie name
-void VFileList::renameFile(const QString &p_notebook,
-                           const QString &p_relativePath, const QString &p_newName)
+void VFileList::convertFileType(const QString &notebook, const QString &fileRelativePath,
+                                DocType oldType, DocType newType)
 {
-    QString name = VUtils::fileNameFromPath(p_relativePath);
+    Q_ASSERT(oldType != newType);
+    QString filePath = QDir(vnote->getNotebookPath(notebook)).filePath(fileRelativePath);
+    QString fileText = VUtils::readFileFromDisk(filePath);
+    QTextEdit editor;
+    if (oldType == DocType::Markdown) {
+        editor.setPlainText(fileText);
+        fileText = editor.toHtml();
+    } else {
+        editor.setHtml(fileText);
+        fileText = editor.toPlainText();
+    }
+    VUtils::writeFileToDisk(filePath, fileText);
+}
+
+void VFileList::deleteLocalImages(const QString &filePath)
+{
+    if (!VUtils::isMarkdown(filePath)) {
+        return;
+    }
+
+    QVector<QString> images = VUtils::imagesFromMarkdownFile(filePath);
+    int deleted = 0;
+    for (int i = 0; i < images.size(); ++i) {
+        QFile file(images[i]);
+        if (file.remove()) {
+            ++deleted;
+        }
+    }
+    qDebug() << "delete" << deleted << "images for" << filePath;
+}
+
+void VFileList::copySelectedFiles(bool p_isCut)
+{
+    QList<QListWidgetItem *> items = fileList->selectedItems();
+    if (items.isEmpty()) {
+        return;
+    }
+    QJsonArray files;
+    QDir dir(relativePath);
+    for (int i = 0; i < items.size(); ++i) {
+        QJsonObject itemJson = items[i]->data(Qt::UserRole).toJsonObject();
+        QString itemName = itemJson["name"].toString();
+        QJsonObject fileJson;
+        fileJson["notebook"] = notebook;
+        fileJson["relative_path"] = dir.filePath(itemName);
+        files.append(fileJson);
+    }
+
+    copyFileInfoToClipboard(files, p_isCut);
+}
+
+void VFileList::cutSelectedFiles()
+{
+    copySelectedFiles(true);
+}
+
+void VFileList::copyFileInfoToClipboard(const QJsonArray &p_files, bool p_isCut)
+{
+    QJsonObject clip;
+    clip["operation"] = (int)ClipboardOpType::CopyFile;
+    clip["is_cut"] = p_isCut;
+    clip["sources"] = p_files;
+
+    QClipboard *clipboard = QApplication::clipboard();
+    clipboard->setText(QJsonDocument(clip).toJson(QJsonDocument::Compact));
+}
+
+void VFileList::pasteFilesInCurDir()
+{
+    pasteFiles(notebook, relativePath);
+}
+
+void VFileList::pasteFiles(const QString &p_notebook, const QString &p_dirRelativePath)
+{
+    qDebug() << "paste files to" << p_notebook << p_dirRelativePath;
+    QClipboard *clipboard = QApplication::clipboard();
+    QString text = clipboard->text();
+    QJsonObject clip = QJsonDocument::fromJson(text.toLocal8Bit()).object();
+    Q_ASSERT(!clip.isEmpty() && clip["operation"] == (int)ClipboardOpType::CopyFile);
+
+    bool isCut = clip["is_cut"].toBool();
+    QJsonArray sources = clip["sources"].toArray();
+
+    int nrFiles = sources.size();
+    QDir destDir(p_dirRelativePath);
+    int nrPasted = 0;
+    for (int i = 0; i < nrFiles; ++i) {
+        QJsonObject file = sources[i].toObject();
+        QString srcNotebook = file["notebook"].toString();
+        QString srcRelativePath = file["relative_path"].toString();
+        bool ret = copyFile(srcNotebook, srcRelativePath, p_notebook,
+                            destDir.filePath(VUtils::fileNameFromPath(srcRelativePath)), isCut);
+        if (ret) {
+            nrPasted++;
+        }
+    }
+    qDebug() << "pasted" << nrPasted << "files sucessfully";
+    clipboard->clear();
+}
+
+bool VFileList::copyFile(const QString &p_srcNotebook, const QString &p_srcRelativePath,
+                         const QString &p_destNotebook, const QString &p_destRelativePath,
+                         bool p_isCut)
+{
+    QString srcPath = QDir(vnote->getNotebookPath(p_srcNotebook)).filePath(p_srcRelativePath);
+    srcPath = QDir::cleanPath(srcPath);
+    QString destPath = QDir(vnote->getNotebookPath(p_destNotebook)).filePath(p_destRelativePath);
+    destPath = QDir::cleanPath(destPath);
+    if (srcPath == destPath) {
+        return true;
+    }
 
     // If change the file type, we need to convert it
-    DocType docType = VUtils::isMarkdown(name) ? DocType::Markdown : DocType::Html;
-    DocType newDocType = VUtils::isMarkdown(p_newName) ? DocType::Markdown : DocType::Html;
+    bool needConversion = false;
+    DocType docType = VUtils::isMarkdown(srcPath) ? DocType::Markdown : DocType::Html;
+    DocType newDocType = VUtils::isMarkdown(destPath) ? DocType::Markdown : DocType::Html;
     if (docType != newDocType) {
-        if (editArea->isFileOpened(p_notebook, p_relativePath)) {
+        if (editArea->isFileOpened(p_srcNotebook, p_srcRelativePath)) {
             QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), QString("Rename will change the note type"),
                                QMessageBox::Ok | QMessageBox::Cancel, this);
             msgBox.setDefaultButton(QMessageBox::Ok);
-            msgBox.setInformativeText(QString("You should close the note %1 before continue").arg(name));
+            msgBox.setInformativeText(QString("You should close the note %1 before continue")
+                                      .arg(VUtils::fileNameFromPath(p_srcRelativePath)));
             if (QMessageBox::Ok == msgBox.exec()) {
                 QJsonObject curItemJson;
-                curItemJson["notebook"] = p_notebook;
-                curItemJson["relative_path"] = p_relativePath;
+                curItemJson["notebook"] = p_srcNotebook;
+                curItemJson["relative_path"] = p_srcRelativePath;
                 curItemJson["is_forced"] = false;
                 if (!editArea->closeFile(curItemJson)) {
-                    return;
+                    return false;
                 }
             } else {
-                return;
+                return false;
             }
         }
-        convertFileType(p_notebook, p_relativePath, docType, newDocType);
+        // Convert it later
+        needConversion = true;
     }
 
-    QString path = QDir(vnote->getNotebookPath(p_notebook)).filePath(VUtils::basePathFromPath(p_relativePath));
-    QFile file(QDir(path).filePath(name));
-    QString newFilePath(QDir(path).filePath(p_newName));
-    Q_ASSERT(file.exists());
-    if (!file.rename(newFilePath)) {
-        qWarning() << "error: fail to rename file" << name << "under" << path;
-        QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), QString("Could not rename note \"%1\" under \"%2\".")
-                           .arg(name).arg(path), QMessageBox::Ok, this);
-        msgBox.setInformativeText(QString("Please check if there already exists a file named \"%1\".").arg(p_newName));
+    QVector<QString> images;
+    if (docType == DocType::Markdown) {
+        images = VUtils::imagesFromMarkdownFile(srcPath);
+    }
+
+    // Copy the file
+    if (!VUtils::copyFile(srcPath, destPath, p_isCut)) {
+        QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), QString("Fail to copy %1 from %2.")
+                           .arg(p_srcRelativePath).arg(p_srcNotebook), QMessageBox::Ok, this);
+        msgBox.setInformativeText(QString("Please check if there already exists a file with the same name"));
         msgBox.exec();
-        return;
+        return false;
+    }
+
+    if (needConversion) {
+        convertFileType(p_destNotebook, p_destRelativePath, docType, newDocType);
+    }
+
+    // We need to copy images when it is still markdown
+    if (!images.isEmpty()) {
+        if (newDocType == DocType::Markdown) {
+            QString dirPath = QDir(VUtils::basePathFromPath(destPath)).filePath("images");
+            VUtils::makeDirectory(dirPath);
+            int nrPasted = 0;
+            for (int i = 0; i < images.size(); ++i) {
+                if (!QFile(images[i]).exists()) {
+                    continue;
+                }
+
+                QString destImagePath = QDir(dirPath).filePath(VUtils::fileNameFromPath(images[i]));
+                if (VUtils::copyFile(images[i], destImagePath, p_isCut)) {
+                    nrPasted++;
+                } else {
+                    QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), QString("Fail to copy image %1.")
+                                       .arg(images[i]), QMessageBox::Ok, this);
+                    msgBox.setInformativeText(QString("Please check if there already exists a file with the same name and manually copy it"));
+                    msgBox.exec();
+                }
+            }
+            qDebug() << "pasted" << nrPasted << "images sucessfully";
+        } else {
+            // Delete the images
+            for (int i = 0; i < images.size(); ++i) {
+                QFile file(images[i]);
+                file.remove();
+            }
+        }
+    }
+
+    int idx = -1;
+    if (p_isCut) {
+        // Remove src in the config
+        idx = removeFileInConfig(srcPath);
+        if (VUtils::basePathFromPath(srcPath) != VUtils::basePathFromPath(destPath)) {
+            idx = -1;
+        }
+    }
+
+    // Add dest in the config
+    addFileInConfig(destPath, idx);
+
+    updateFileList();
+
+    if (p_isCut) {
+        emit fileRenamed(p_srcNotebook, p_srcRelativePath,
+                         p_destNotebook, p_destRelativePath);
     }
+    return true;
+}
 
-    // Update directory's config file
-    QJsonObject dirJson = VConfigManager::readDirectoryConfig(path);
+int VFileList::removeFileInConfig(const QString &p_filePath)
+{
+    QString dirPath = VUtils::basePathFromPath(p_filePath);
+    QString fileName = VUtils::fileNameFromPath(p_filePath);
+    // Update current directory's config file to exclude this file
+    QJsonObject dirJson = VConfigManager::readDirectoryConfig(dirPath);
     Q_ASSERT(!dirJson.isEmpty());
     QJsonArray fileArray = dirJson["files"].toArray();
-    int index = 0;
-    for (index = 0; index < fileArray.size(); ++index) {
-        QJsonObject tmp = fileArray[index].toObject();
-        if (tmp["name"].toString() == name) {
-            tmp["name"] = p_newName;
-            fileArray[index] = tmp;
+    bool deleted = false;
+    int idx = -1;
+    for (int i = 0; i < fileArray.size(); ++i) {
+        QJsonObject ele = fileArray[i].toObject();
+        if (ele["name"].toString() == fileName) {
+            fileArray.removeAt(i);
+            deleted = true;
+            idx = i;
             break;
         }
     }
-    Q_ASSERT(index != fileArray.size());
-    dirJson["files"] = fileArray;
-    if (!VConfigManager::writeDirectoryConfig(path, dirJson)) {
-        qWarning() << "error: fail to rename file"
-                   << name << "to" << p_newName;
-        file.rename(name);
-        return;
+    if (!deleted) {
+        qWarning() << "error: fail to find" << fileName << "to delete";
+        return idx;
     }
-
-    // Update item
-    QListWidgetItem *item = findItem(p_notebook, p_relativePath);
-    if (item) {
-        QJsonObject itemJson = item->data(Qt::UserRole).toJsonObject();
-        itemJson["name"] = p_newName;
-        item->setData(Qt::UserRole, itemJson);
-        item->setText(p_newName);
+    dirJson["files"] = fileArray;
+    if (!VConfigManager::writeDirectoryConfig(dirPath, dirJson)) {
+        qWarning() << "error: fail to update directory's configuration file to delete"
+                   << fileName;
+        return idx;
     }
-
-    QString oldPath = QDir::cleanPath(p_relativePath);
-    QString newPath = QDir::cleanPath(QDir(VUtils::basePathFromPath(p_relativePath)).filePath(p_newName));
-    qDebug() << "file renamed" << oldPath << "to" << newPath;
-    emit fileRenamed(p_notebook, oldPath, newPath);
+    return idx;
 }
 
-void VFileList::convertFileType(const QString &notebook, const QString &fileRelativePath,
-                                DocType oldType, DocType newType)
+// @index = -1, add it to the end of the list
+bool VFileList::addFileInConfig(const QString &p_filePath, int p_index)
 {
-    Q_ASSERT(oldType != newType);
-    QString filePath = QDir(vnote->getNotebookPath(notebook)).filePath(fileRelativePath);
-    QString fileText = VUtils::readFileFromDisk(filePath);
-    QTextEdit editor;
-    if (oldType == DocType::Markdown) {
-        editor.setPlainText(fileText);
-        fileText = editor.toHtml();
-    } else {
-        editor.setHtml(fileText);
-        fileText = editor.toPlainText();
+    QString dirPath = VUtils::basePathFromPath(p_filePath);
+    QString fileName = VUtils::fileNameFromPath(p_filePath);
+
+    // Update current directory's config file to include this file
+    QJsonObject dirJson = VConfigManager::readDirectoryConfig(dirPath);
+    Q_ASSERT(!dirJson.isEmpty());
+    QJsonObject fileJson;
+    fileJson["name"] = fileName;
+    QJsonArray fileArray = dirJson["files"].toArray();
+    if (p_index == -1) {
+        p_index = fileArray.size();
     }
-    VUtils::writeFileToDisk(filePath, fileText);
+    fileArray.insert(p_index, fileJson);
+    dirJson["files"] = fileArray;
+    if (!VConfigManager::writeDirectoryConfig(dirPath, dirJson)) {
+        qWarning() << "error: fail to update directory's configuration file to add a new file"
+                   << fileName;
+        return false;
+    }
+
+    return true;
 }
 
-void VFileList::deleteLocalImages(const QString &filePath)
+QJsonObject VFileList::readFileInConfig(const QString &p_filePath)
 {
-    if (!VUtils::isMarkdown(filePath)) {
-        return;
-    }
+    QString dirPath = VUtils::basePathFromPath(p_filePath);
+    QString fileName = VUtils::fileNameFromPath(p_filePath);
 
-    QVector<QString> images = VUtils::imagesFromMarkdownFile(filePath);
-    int deleted = 0;
-    for (int i = 0; i < images.size(); ++i) {
-        QFile file(images[i]);
-        if (file.remove()) {
-            ++deleted;
+    QJsonObject dirJson = VConfigManager::readDirectoryConfig(dirPath);
+    Q_ASSERT(!dirJson.isEmpty());
+
+    qDebug() << "config" << p_filePath;
+    QJsonArray fileArray = dirJson["files"].toArray();
+    for (int i = 0; i < fileArray.size(); ++i) {
+        QJsonObject ele = fileArray[i].toObject();
+        if (ele["name"].toString() == fileName) {
+            return ele;
         }
     }
-    qDebug() << "delete" << deleted << "images for" << filePath;
+    return QJsonObject();
 }

+ 16 - 4
src/vfilelist.h

@@ -29,8 +29,8 @@ signals:
     void fileClicked(QJsonObject fileJson);
     void fileDeleted(QJsonObject fileJson);
     void fileCreated(QJsonObject fileJson);
-    void fileRenamed(const QString &notebook, const QString &oldPath,
-                     const QString &newPath);
+    void fileRenamed(const QString &p_srcNotebook, const QString &p_srcRelativePath,
+                     const QString &p_destNotebook, const QString &p_destRelativePath);
     void directoryChanged(const QString &notebook, const QString &relativePath);
 
 private slots:
@@ -38,6 +38,9 @@ private slots:
     void handleItemClicked(QListWidgetItem *currentItem);
     void curFileInfo();
     void deleteCurFile();
+    void copySelectedFiles(bool p_isCut = false);
+    void cutSelectedFiles();
+    void pasteFilesInCurDir();
 
 public slots:
     void setDirectory(QJsonObject dirJson);
@@ -58,12 +61,18 @@ private:
     void deleteFileAndUpdateList(const QString &p_notebook,
                                  const QString &p_relativePath);
     void clearDirectoryInfo();
-    void renameFile(const QString &p_notebook,
-                    const QString &p_relativePath, const QString &p_newName);
     void convertFileType(const QString &notebook, const QString &fileRelativePath,
                          DocType oldType, DocType newType);
     QListWidgetItem *findItem(const QString &p_notebook, const QString &p_relativePath);
     void deleteLocalImages(const QString &filePath);
+    void copyFileInfoToClipboard(const QJsonArray &p_files, bool p_isCut);
+    void pasteFiles(const QString &p_notebook, const QString &p_dirRelativePath);
+    bool copyFile(const QString &p_srcNotebook, const QString &p_srcRelativePath,
+                  const QString &p_destNotebook, const QString &p_destRelativePath,
+                  bool p_isCut);
+    int removeFileInConfig(const QString &p_filePath);
+    bool addFileInConfig(const QString &p_filePath, int p_index);
+    QJsonObject readFileInConfig(const QString &p_filePath);
 
     VNote *vnote;
     QString notebook;
@@ -80,6 +89,9 @@ private:
     QAction *newFileAct;
     QAction *deleteFileAct;
     QAction *fileInfoAct;
+    QAction *copyAct;
+    QAction *cutAct;
+    QAction *pasteAct;
 };
 
 inline void VFileList::setEditArea(VEditArea *editArea)

+ 10 - 0
src/vfilelocation.cpp

@@ -0,0 +1,10 @@
+#include "vfilelocation.h"
+
+VFileLocation::VFileLocation()
+{
+}
+
+VFileLocation::VFileLocation(const QString &p_notebook, const QString &p_relativePath)
+    : m_notebook(p_notebook), m_relativePath(p_relativePath)
+{
+}

+ 15 - 0
src/vfilelocation.h

@@ -0,0 +1,15 @@
+#ifndef VFILELOCATION_H
+#define VFILELOCATION_H
+
+#include <QString>
+
+class VFileLocation
+{
+public:
+    VFileLocation();
+    VFileLocation(const QString &p_notebook, const QString &p_relativePath);
+    QString m_notebook;
+    QString m_relativePath;
+};
+
+#endif // VFILELOCATION_H

+ 3 - 0
src/vnote.qrc

@@ -64,5 +64,8 @@
         <file>resources/vnote.qss</file>
         <file>resources/icons/note_info_tb.svg</file>
         <file>resources/icons/delete_note_tb.svg</file>
+        <file>resources/icons/copy.svg</file>
+        <file>resources/icons/cut.svg</file>
+        <file>resources/icons/paste.svg</file>
     </qresource>
 </RCC>