markdowninterface.cpp 12.2 KB
Newer Older
Laurent Montel's avatar
Laurent Montel committed
1
/*
Laurent Montel's avatar
Laurent Montel committed
2
   Copyright (C) 2018-2020 Laurent Montel <montel@kde.org>
Laurent Montel's avatar
Laurent Montel committed
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; see the file COPYING.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301, USA.
*/

#include "markdowninterface.h"
Laurent Montel's avatar
Laurent Montel committed
21
#include "markdownpreviewdialog.h"
Laurent Montel's avatar
Laurent Montel committed
22
#include "markdownplugin_debug.h"
Laurent Montel's avatar
Laurent Montel committed
23
#include "markdownconverter.h"
Laurent Montel's avatar
Laurent Montel committed
24
#include "markdowncreatelinkdialog.h"
25
#include "markdownutil.h"
26
#include "markdowncreateimagedialog.h"
Laurent Montel's avatar
Laurent Montel committed
27
#include <KPIMTextEdit/RichTextComposer>
28
#include <KPIMTextEdit/RichTextComposerControler>
Laurent Montel's avatar
Fix api    
Laurent Montel committed
29
30
31
#include <KLocalizedString>
#include <QAction>
#include <KActionCollection>
Laurent Montel's avatar
Laurent Montel committed
32
33
#include <KSharedConfig>
#include <KConfigGroup>
Laurent Montel's avatar
Laurent Montel committed
34
#include <QPointer>
Laurent Montel's avatar
Laurent Montel committed
35
#include <QLabel>
Laurent Montel's avatar
Laurent Montel committed
36
#include <QMenu>
37

Laurent Montel's avatar
Laurent Montel committed
38
#include <MessageComposer/TextPart>
Laurent Montel's avatar
Laurent Montel committed
39

40
41
#include <MessageComposer/StatusBarLabelToggledState>

Laurent Montel's avatar
Laurent Montel committed
42
43
44
45
46
47
48
49
MarkdownInterface::MarkdownInterface(QObject *parent)
    : MessageComposer::PluginEditorConvertTextInterface(parent)
{
}

MarkdownInterface::~MarkdownInterface()
{
}
Laurent Montel's avatar
Fix api    
Laurent Montel committed
50
51
52

void MarkdownInterface::createAction(KActionCollection *ac)
{
Laurent Montel's avatar
Laurent Montel committed
53
    mAction = new QAction(i18n("Generate HTML from markdown language."), this);
54
55
56
57
    mAction->setCheckable(true);
    mAction->setChecked(false);
    ac->addAction(QStringLiteral("generate_markdown"), mAction);
    connect(mAction, &QAction::triggered, this, &MarkdownInterface::slotActivated);
Laurent Montel's avatar
Laurent Montel committed
58
    MessageComposer::PluginActionType type(mAction, MessageComposer::PluginActionType::Edit);
Laurent Montel's avatar
Laurent Montel committed
59
    addActionType(type);
Laurent Montel's avatar
Laurent Montel committed
60

61
    mStatusBarLabel = new MessageComposer::StatusBarLabelToggledState(parentWidget());
62
63
    connect(mStatusBarLabel, &MessageComposer::StatusBarLabelToggledState::toggleModeChanged, this, [this](bool checked) {
        mAction->setChecked(checked);
Laurent Montel's avatar
Laurent Montel committed
64
65
66
        slotActivated(checked);
    }
            );
Laurent Montel's avatar
Laurent Montel committed
67
68
69
    QFont f = mStatusBarLabel->font();
    f.setBold(true);
    mStatusBarLabel->setFont(f);
Laurent Montel's avatar
Laurent Montel committed
70
    setStatusBarWidget(mStatusBarLabel);
71
    mStatusBarLabel->setStateString(i18n("Markdown"), QString());
Laurent Montel's avatar
Laurent Montel committed
72
73
74

    mPopupMenuAction = new QAction(i18n("Markdown Action"), this);

Laurent Montel's avatar
Laurent Montel committed
75
    QMenu *mardownMenu = new QMenu(parentWidget());
Laurent Montel's avatar
Laurent Montel committed
76
    mPopupMenuAction->setMenu(mardownMenu);
Laurent Montel's avatar
Laurent Montel committed
77
    mPopupMenuAction->setEnabled(false);
78
79
80
    QMenu *titleMenu = new QMenu(i18n("Add Title"), mardownMenu);
    mardownMenu->addMenu(titleMenu);
    for (int i = 1; i < 5; ++i) {
Laurent Montel's avatar
Laurent Montel committed
81
82
83
        titleMenu->addAction(i18n("Level %1", QString::number(i)), this, [this, i]() {
            addTitle(i);
        });
84
    }
Laurent Montel's avatar
Laurent Montel committed
85
    mardownMenu->addAction(i18n("Horizontal Rule"), this, &MarkdownInterface::addHorizontalRule);
Laurent Montel's avatar
Laurent Montel committed
86
    mardownMenu->addSeparator();
87
88
89
90
91
92
    mBoldAction = mardownMenu->addAction(i18n("Change Selected Text as Bold"), this, &MarkdownInterface::addBold);
    mBoldAction->setEnabled(false);
    mItalicAction = mardownMenu->addAction(i18n("Change Selected Text as Italic"), this, &MarkdownInterface::addItalic);
    mItalicAction->setEnabled(false);
    mCodeAction = mardownMenu->addAction(i18n("Change Selected Text as Code"), this, &MarkdownInterface::addCode);
    mCodeAction->setEnabled(false);
Laurent Montel's avatar
Laurent Montel committed
93
94
    mBlockQuoteAction = mardownMenu->addAction(i18n("Change Selected Text as Block Quote"), this, &MarkdownInterface::addBlockQuote);
    mBlockQuoteAction->setEnabled(false);
Laurent Montel's avatar
Laurent Montel committed
95
    mardownMenu->addSeparator();
Laurent Montel's avatar
Laurent Montel committed
96
    mardownMenu->addAction(i18n("Add Link"), this, &MarkdownInterface::addLink);
97
    mardownMenu->addAction(i18n("Add Image"), this, &MarkdownInterface::addImage);
Laurent Montel's avatar
Laurent Montel committed
98
99
    MessageComposer::PluginActionType typePopup(mPopupMenuAction, MessageComposer::PluginActionType::PopupMenu);
    addActionType(typePopup);
100
101
102
103
104
105
106
107
108
    connect(richTextEditor(), &KPIMTextEdit::RichTextComposer::selectionChanged, this, &MarkdownInterface::slotSelectionChanged);
}

void MarkdownInterface::slotSelectionChanged()
{
    const bool enabled = richTextEditor()->textCursor().hasSelection();
    mBoldAction->setEnabled(enabled);
    mItalicAction->setEnabled(enabled);
    mCodeAction->setEnabled(enabled);
Laurent Montel's avatar
Laurent Montel committed
109
    mBlockQuoteAction->setEnabled(enabled);
Laurent Montel's avatar
Laurent Montel committed
110
111
}

Laurent Montel's avatar
Laurent Montel committed
112
113
void MarkdownInterface::addHorizontalRule()
{
Laurent Montel's avatar
Laurent Montel committed
114
    richTextEditor()->insertPlainText(QStringLiteral("\n---"));
Laurent Montel's avatar
Laurent Montel committed
115
116
}

Laurent Montel's avatar
Laurent Montel committed
117
118
119
120
121
void MarkdownInterface::addBold()
{
    const QString selectedText = richTextEditor()->textCursor().selectedText();
    if (!selectedText.isEmpty()) {
        richTextEditor()->textCursor().insertText(QStringLiteral("**%1**").arg(selectedText));
Laurent Montel's avatar
Laurent Montel committed
122
123
    } else {
        qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
Laurent Montel's avatar
Laurent Montel committed
124
125
126
127
128
129
130
131
132
133
    }
}

void MarkdownInterface::addBlockQuote()
{
    const QString selectedText = richTextEditor()->textCursor().selectedText();
    if (!selectedText.isEmpty()) {
        richTextEditor()->composerControler()->addQuotes(QStringLiteral(">"));
    } else {
        qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
Laurent Montel's avatar
Laurent Montel committed
134
135
136
    }
}

137
138
139
140
141
void MarkdownInterface::addCode()
{
    const QString selectedText = richTextEditor()->textCursor().selectedText();
    if (!selectedText.isEmpty()) {
        richTextEditor()->textCursor().insertText(QStringLiteral("`%1`").arg(selectedText));
Laurent Montel's avatar
Laurent Montel committed
142
143
    } else {
        qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
144
145
146
    }
}

Laurent Montel's avatar
Laurent Montel committed
147
148
149
150
151
void MarkdownInterface::addItalic()
{
    const QString selectedText = richTextEditor()->textCursor().selectedText();
    if (!selectedText.isEmpty()) {
        richTextEditor()->textCursor().insertText(QStringLiteral("_%1_").arg(selectedText));
Laurent Montel's avatar
Laurent Montel committed
152
153
    } else {
        qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Any text selected";
Laurent Montel's avatar
Laurent Montel committed
154
155
156
157
158
    }
}

void MarkdownInterface::addLink()
{
Laurent Montel's avatar
Laurent Montel committed
159
160
161
162
163
164
165
166
    QPointer<MarkdownCreateLinkDialog> dlg = new MarkdownCreateLinkDialog(parentWidget());
    if (dlg->exec()) {
        const QString str = dlg->linkStr();
        if (!str.isEmpty()) {
            richTextEditor()->textCursor().insertText(str);
        }
    }
    delete dlg;
Laurent Montel's avatar
Laurent Montel committed
167
168
}

169
170
171
172
173
174
175
176
177
178
179
180
void MarkdownInterface::addImage()
{
    QPointer<MarkdownCreateImageDialog> dlg = new MarkdownCreateImageDialog(parentWidget());
    if (dlg->exec()) {
        const QString str = dlg->linkStr();
        if (!str.isEmpty()) {
            richTextEditor()->textCursor().insertText(str);
        }
    }
    delete dlg;
}

181
void MarkdownInterface::addTitle(int index)
Laurent Montel's avatar
Laurent Montel committed
182
{
183
184
185
186
    QString tag = QStringLiteral("#");
    for (int i = 1; i < index; ++i) {
        tag += QLatin1Char('#');
    }
Laurent Montel's avatar
Laurent Montel committed
187
188
    const QString selectedText = richTextEditor()->textCursor().selectedText();
    if (!selectedText.trimmed().isEmpty()) {
189
        richTextEditor()->textCursor().insertText(QStringLiteral("%1 %2").arg(tag, selectedText));
Laurent Montel's avatar
Laurent Montel committed
190
    } else {
191
        richTextEditor()->textCursor().insertText(QStringLiteral("%1 ").arg(tag));
192
    }
Laurent Montel's avatar
Fix api    
Laurent Montel committed
193
194
195
196
197
198
199
}

bool MarkdownInterface::reformatText()
{
    return false;
}

Laurent Montel's avatar
Laurent Montel committed
200
201
202
void MarkdownInterface::addEmbeddedImages(MessageComposer::TextPart *textPart, QString &textVersion, QString &htmlVersion) const
{
    QStringList listImage = MarkdownUtil::imagePaths(textVersion);
203
    QVector< QSharedPointer<KPIMTextEdit::EmbeddedImage> > lstEmbeddedImages;
Laurent Montel's avatar
Laurent Montel committed
204
205
206
    if (!listImage.isEmpty()) {
        listImage.removeDuplicates();
        QStringList imageNameAdded;
Laurent Montel's avatar
Laurent Montel committed
207
        for (const QString &urlImage : qAsConst(listImage)) {
Laurent Montel's avatar
Laurent Montel committed
208
209
210
211
212
            const QUrl url = QUrl::fromUserInput(urlImage);
            if (!url.isLocalFile()) {
                qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Url is not a local file " << url;
                continue;
            }
Laurent Montel's avatar
Laurent Montel committed
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
            QImage image;
            if (!image.load(urlImage)) {
                qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Impossible to load " << urlImage;
                continue;
            }
            const QFileInfo fi(urlImage);
            const QString imageName
                = fi.baseName().isEmpty()
                  ? QStringLiteral("image.png")
                  : QString(fi.baseName() + QLatin1String(".png"));

            QString imageNameToAdd = imageName;
            int imageNumber = 1;
            while (imageNameAdded.contains(imageNameToAdd)) {
                const int firstDot = imageName.indexOf(QLatin1Char('.'));
                if (firstDot == -1) {
                    imageNameToAdd = imageName + QString::number(imageNumber++);
                } else {
                    imageNameToAdd = imageName.left(firstDot) + QString::number(imageNumber++)
Laurent Montel's avatar
Laurent Montel committed
232
                                     +imageName.mid(firstDot);
Laurent Montel's avatar
Laurent Montel committed
233
234
235
236
237
238
239
240
241
                }
            }

            QSharedPointer<KPIMTextEdit::EmbeddedImage> embeddedImage = richTextEditor()->composerControler()->composerImages()->createEmbeddedImage(image, imageNameToAdd);
            lstEmbeddedImages.append(embeddedImage);

            const QString newImageName = QLatin1String("cid:") + embeddedImage->contentID;
            const QString quote(QStringLiteral("\""));
            htmlVersion.replace(QString(quote + urlImage + quote),
Laurent Montel's avatar
Laurent Montel committed
242
                                QString(quote + newImageName + quote));
Laurent Montel's avatar
Laurent Montel committed
243
244
245
246
247
248
249
250
251
            textVersion.replace(urlImage, newImageName);
            imageNameAdded << imageNameToAdd;
        }
        if (!lstEmbeddedImages.isEmpty()) {
            textPart->setEmbeddedImages(lstEmbeddedImages);
        }
    }
}

Laurent Montel's avatar
Laurent Montel committed
252
MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus MarkdownInterface::convertTextToFormat(MessageComposer::TextPart *textPart)
Laurent Montel's avatar
Fix api    
Laurent Montel committed
253
{
254
255
    //It can't work on html email
    if (richTextEditor()->composerControler()->isFormattingUsed()) {
Laurent Montel's avatar
Laurent Montel committed
256
        qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "We can't convert html email";
257
258
        return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
    }
259
    if (mAction->isChecked()) {
Laurent Montel's avatar
Laurent Montel committed
260
261
        QString textVersion = richTextEditor()->composerControler()->toCleanPlainText();
        if (!textVersion.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
262
263
264
            MarkdownConverter converter;
            converter.setEnableEmbeddedLabel(mEnableEmbeddedLabel);
            converter.setEnableExtraDefinitionLists(mEnableExtraDefinitionLists);
Laurent Montel's avatar
Laurent Montel committed
265
            QString result = converter.convertTextToMarkdown(textVersion);
Laurent Montel's avatar
Laurent Montel committed
266
            if (!result.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
267
268
269
270
                addEmbeddedImages(textPart, textVersion, result);
                textPart->setCleanPlainText(textVersion);

                textPart->setWrappedPlainText(textVersion);
Laurent Montel's avatar
Laurent Montel committed
271
272
273
274
275
                textPart->setCleanHtml(result);
                return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Converted;
            } else {
                qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "Impossible to convert text";
                return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Error;
Laurent Montel's avatar
Laurent Montel committed
276
            }
Laurent Montel's avatar
Laurent Montel committed
277
278
        } else {
            qCWarning(KMAIL_EDITOR_MARKDOWN_PLUGIN_LOG) << "empty text! Bug ?";
Laurent Montel's avatar
Laurent Montel committed
279
            return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
Laurent Montel's avatar
Laurent Montel committed
280
        }
281
    }
Laurent Montel's avatar
Laurent Montel committed
282
    return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
Laurent Montel's avatar
Fix api    
Laurent Montel committed
283
284
}

Laurent Montel's avatar
Laurent Montel committed
285
286
287
288
void MarkdownInterface::enableDisablePluginActions(bool richText)
{
    if (mAction) {
        mAction->setEnabled(!richText);
Laurent Montel's avatar
Laurent Montel committed
289
        mPopupMenuAction->setEnabled(!richText && mAction->isChecked());
Laurent Montel's avatar
Laurent Montel committed
290
291
292
    }
}

Laurent Montel's avatar
Fix api    
Laurent Montel committed
293
294
void MarkdownInterface::reloadConfig()
{
Laurent Montel's avatar
Laurent Montel committed
295
    KConfigGroup grp(KSharedConfig::openConfig(), "Markdown");
Laurent Montel's avatar
Laurent Montel committed
296
297
298

    mEnableEmbeddedLabel = grp.readEntry("Enable Embedded Latex", false);
    mEnableExtraDefinitionLists = grp.readEntry("Enable Extra Definition Lists", false);
Laurent Montel's avatar
Fix api    
Laurent Montel committed
299
300
}

Laurent Montel's avatar
Laurent Montel committed
301
void MarkdownInterface::slotActivated(bool checked)
Laurent Montel's avatar
Fix api    
Laurent Montel committed
302
{
Laurent Montel's avatar
Laurent Montel committed
303
304
305
    if (mDialog.isNull()) {
        mDialog = new MarkdownPreviewDialog(parentWidget());
        mDialog->setText(richTextEditor()->toPlainText());
Laurent Montel's avatar
Laurent Montel committed
306
        connect(richTextEditor(), &KPIMTextEdit::RichTextEditor::textChanged, this, [this]() {
307
308
309
            if (mDialog) {
                mDialog->setText(richTextEditor()->toPlainText());
            }
Laurent Montel's avatar
Laurent Montel committed
310
        });
Laurent Montel's avatar
Laurent Montel committed
311
    }
312
    mStatusBarLabel->setToggleMode(checked);
Laurent Montel's avatar
Laurent Montel committed
313
314
315
316
317
    if (checked) {
        mDialog->show();
    } else {
        mDialog->hide();
    }
Laurent Montel's avatar
Laurent Montel committed
318
    mPopupMenuAction->setEnabled(checked);
Laurent Montel's avatar
Fix api    
Laurent Montel committed
319
}