annotwindow.cpp 15.2 KB
Newer Older
1
2
3
/*
    SPDX-FileCopyrightText: 2006 Chu Xiaodong <xiaodongchu@gmail.com>
    SPDX-FileCopyrightText: 2006 Pino Toscano <pino@kde.org>
4
5

    Work sponsored by the LiMux project of the city of Munich:
6
    SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
7
8
9

    SPDX-License-Identifier: GPL-2.0-or-later
*/
10

Albert Astals Cid's avatar
Albert Astals Cid committed
11
12
#include "annotwindow.h"

13
// qt/kde includes
Albert Astals Cid's avatar
Albert Astals Cid committed
14
15
16
17
#include <KLocalizedString>
#include <KStandardAction>
#include <KTextEdit>
#include <QAction>
18
#include <QApplication>
Albert Astals Cid's avatar
Albert Astals Cid committed
19
#include <QDebug>
20
21
22
23
24
25
#include <QEvent>
#include <QFont>
#include <QFontInfo>
#include <QFontMetrics>
#include <QLabel>
#include <QLayout>
Albert Astals Cid's avatar
Albert Astals Cid committed
26
#include <QMenu>
27
28
29
30
#include <QPushButton>
#include <QSizeGrip>
#include <QStyle>
#include <QToolButton>
31

32
// local includes
33
#include "core/annotations.h"
34
#include "core/document.h"
35
36
#include "latexrenderer.h"
#include <KMessageBox>
Albert Astals Cid's avatar
Albert Astals Cid committed
37
#include <core/utils.h>
38

Albert Astals Cid's avatar
Albert Astals Cid committed
39
class CloseButton : public QPushButton
40
{
Albert Vaca Cintora's avatar
Albert Vaca Cintora committed
41
42
    Q_OBJECT

43
public:
44
    explicit CloseButton(QWidget *parent = Q_NULLPTR)
Albert Astals Cid's avatar
Albert Astals Cid committed
45
        : QPushButton(parent)
46
    {
Albert Astals Cid's avatar
Albert Astals Cid committed
47
48
49
50
51
52
53
        setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
        QSize size = QSize(14, 14).expandedTo(QApplication::globalStrut());
        setFixedSize(size);
        setIcon(style()->standardIcon(QStyle::SP_DockWidgetCloseButton));
        setIconSize(size);
        setToolTip(i18n("Close this note"));
        setCursor(Qt::ArrowCursor);
54
55
    }
};
56

Albert Astals Cid's avatar
Albert Astals Cid committed
57
class MovableTitle : public QWidget
58
{
Albert Vaca Cintora's avatar
Albert Vaca Cintora committed
59
60
    Q_OBJECT

61
public:
62
    explicit MovableTitle(AnnotWindow *parent)
Albert Astals Cid's avatar
Albert Astals Cid committed
63
        : QWidget(parent)
64
    {
Albert Astals Cid's avatar
Albert Astals Cid committed
65
66
67
        QVBoxLayout *mainlay = new QVBoxLayout(this);
        mainlay->setContentsMargins(0, 0, 0, 0);
        mainlay->setSpacing(0);
68
        // close button row
Albert Astals Cid's avatar
Albert Astals Cid committed
69
70
71
        QHBoxLayout *buttonlay = new QHBoxLayout();
        mainlay->addLayout(buttonlay);
        titleLabel = new QLabel(this);
72
        QFont f = titleLabel->font();
Albert Astals Cid's avatar
Albert Astals Cid committed
73
74
75
76
77
78
        f.setBold(true);
        titleLabel->setFont(f);
        titleLabel->setCursor(Qt::SizeAllCursor);
        buttonlay->addWidget(titleLabel);
        dateLabel = new QLabel(this);
        dateLabel->setAlignment(Qt::AlignTop | Qt::AlignRight);
79
        f = dateLabel->font();
Albert Astals Cid's avatar
Albert Astals Cid committed
80
81
82
83
84
85
86
        f.setPointSize(QFontInfo(f).pointSize() - 2);
        dateLabel->setFont(f);
        dateLabel->setCursor(Qt::SizeAllCursor);
        buttonlay->addWidget(dateLabel);
        CloseButton *close = new CloseButton(this);
        connect(close, &QAbstractButton::clicked, parent, &QWidget::close);
        buttonlay->addWidget(close);
87
        // option button row
Albert Astals Cid's avatar
Albert Astals Cid committed
88
89
90
91
92
93
94
95
96
97
98
99
100
101
        QHBoxLayout *optionlay = new QHBoxLayout();
        mainlay->addLayout(optionlay);
        authorLabel = new QLabel(this);
        authorLabel->setCursor(Qt::SizeAllCursor);
        authorLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
        optionlay->addWidget(authorLabel);
        optionButton = new QToolButton(this);
        QString opttext = i18n("Options");
        optionButton->setText(opttext);
        optionButton->setAutoRaise(true);
        QSize s = QFontMetrics(optionButton->font()).boundingRect(opttext).size() + QSize(8, 8);
        optionButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
        optionButton->setFixedSize(s);
        optionlay->addWidget(optionButton);
102
103
        // ### disabled for now
        optionButton->hide();
Albert Astals Cid's avatar
Albert Astals Cid committed
104
105
106
107
108
109
110
111
112
113
114
115
116
117
        latexButton = new QToolButton(this);
        QHBoxLayout *latexlay = new QHBoxLayout();
        QString latextext = i18n("This annotation may contain LaTeX code.\nClick here to render.");
        latexButton->setText(latextext);
        latexButton->setAutoRaise(true);
        s = QFontMetrics(latexButton->font()).boundingRect(0, 0, this->width(), this->height(), 0, latextext).size() + QSize(8, 8);
        latexButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
        latexButton->setFixedSize(s);
        latexButton->setCheckable(true);
        latexButton->setVisible(false);
        latexlay->addSpacing(1);
        latexlay->addWidget(latexButton);
        latexlay->addSpacing(1);
        mainlay->addLayout(latexlay);
118
119
        connect(latexButton, &QToolButton::clicked, parent, &AnnotWindow::renderLatex);
        connect(parent, &AnnotWindow::containsLatex, latexButton, &QWidget::setVisible);
120

Albert Astals Cid's avatar
Albert Astals Cid committed
121
122
123
        titleLabel->installEventFilter(this);
        dateLabel->installEventFilter(this);
        authorLabel->installEventFilter(this);
124
125
    }

Albert Astals Cid's avatar
Albert Astals Cid committed
126
    bool eventFilter(QObject *obj, QEvent *e) override
127
    {
128
        if (obj != titleLabel && obj != authorLabel && obj != dateLabel) {
129
            return false;
130
        }
131

Albert Astals Cid's avatar
Albert Astals Cid committed
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
        QMouseEvent *me = nullptr;
        switch (e->type()) {
        case QEvent::MouseButtonPress:
            me = (QMouseEvent *)e;
            mousePressPos = me->pos();
            parentWidget()->raise();
            break;
        case QEvent::MouseButtonRelease:
            mousePressPos = QPoint();
            break;
        case QEvent::MouseMove:
            me = (QMouseEvent *)e;
            parentWidget()->move(me->pos() - mousePressPos + parentWidget()->pos());
            break;
        default:
            return false;
148
149
150
151
        }
        return true;
    }

Albert Astals Cid's avatar
Albert Astals Cid committed
152
    void setTitle(const QString &title)
153
    {
Albert Astals Cid's avatar
Albert Astals Cid committed
154
        titleLabel->setText(QStringLiteral(" ") + title);
155
156
    }

Albert Astals Cid's avatar
Albert Astals Cid committed
157
    void setDate(const QDateTime &dt)
158
    {
Albert Astals Cid's avatar
Albert Astals Cid committed
159
        dateLabel->setText(QLocale().toString(dt.toTimeSpec(Qt::LocalTime), QLocale::ShortFormat) + QLatin1Char(' '));
160
161
    }

Albert Astals Cid's avatar
Albert Astals Cid committed
162
    void setAuthor(const QString &author)
163
    {
Albert Astals Cid's avatar
Albert Astals Cid committed
164
        authorLabel->setText(QStringLiteral(" ") + author);
165
166
    }

Albert Astals Cid's avatar
Albert Astals Cid committed
167
    void connectOptionButton(QObject *recv, const char *method)
168
    {
Albert Astals Cid's avatar
Albert Astals Cid committed
169
        connect(optionButton, SIGNAL(clicked()), recv, method);
170
171
    }

172
173
    void uncheckLatexButton()
    {
Albert Astals Cid's avatar
Albert Astals Cid committed
174
        latexButton->setChecked(false);
175
176
    }

177
private:
Albert Astals Cid's avatar
Albert Astals Cid committed
178
179
180
    QLabel *titleLabel;
    QLabel *dateLabel;
    QLabel *authorLabel;
181
    QPoint mousePressPos;
Albert Astals Cid's avatar
Albert Astals Cid committed
182
183
    QToolButton *optionButton;
    QToolButton *latexButton;
184
185
};

186
// Qt::SubWindow is needed to make QSizeGrip work
Albert Astals Cid's avatar
Albert Astals Cid committed
187
188
189
190
191
AnnotWindow::AnnotWindow(QWidget *parent, Okular::Annotation *annot, Okular::Document *document, int page)
    : QFrame(parent, Qt::SubWindow)
    , m_annot(annot)
    , m_document(document)
    , m_page(page)
192
{
Albert Astals Cid's avatar
Albert Astals Cid committed
193
194
195
    setAutoFillBackground(true);
    setFrameStyle(Panel | Raised);
    setAttribute(Qt::WA_DeleteOnClose);
Laurent Montel's avatar
Laurent Montel committed
196
    setObjectName(QStringLiteral("AnnotWindow"));
197

Albert Astals Cid's avatar
Albert Astals Cid committed
198
    const bool canEditAnnotation = m_document->canModifyPageAnnotation(annot);
199

Albert Astals Cid's avatar
Albert Astals Cid committed
200
201
202
203
204
    textEdit = new KTextEdit(this);
    textEdit->setAcceptRichText(false);
    textEdit->setPlainText(m_annot->contents());
    textEdit->installEventFilter(this);
    textEdit->setUndoRedoEnabled(false);
205
206
207
208

    m_prevCursorPos = textEdit->textCursor().position();
    m_prevAnchorPos = textEdit->textCursor().anchor();

Laurent Montel's avatar
Laurent Montel committed
209
210
211
212
    connect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText);
    connect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText);
    connect(textEdit, &KTextEdit::aboutToShowContextMenu, this, &AnnotWindow::slotUpdateUndoAndRedoInContextMenu);
    connect(m_document, &Okular::Document::annotationContentsChangedByUndoRedo, this, &AnnotWindow::slotHandleContentsChangedByUndoRedo);
213

214
    if (!canEditAnnotation) {
215
        textEdit->setReadOnly(true);
216
    }
217

Albert Astals Cid's avatar
Albert Astals Cid committed
218
219
220
221
222
223
224
225
226
227
228
    QVBoxLayout *mainlay = new QVBoxLayout(this);
    mainlay->setContentsMargins(2, 2, 2, 2);
    mainlay->setSpacing(0);
    m_title = new MovableTitle(this);
    mainlay->addWidget(m_title);
    mainlay->addWidget(textEdit);
    QHBoxLayout *lowerlay = new QHBoxLayout();
    mainlay->addLayout(lowerlay);
    lowerlay->addItem(new QSpacerItem(5, 5, QSizePolicy::Expanding, QSizePolicy::Fixed));
    QSizeGrip *sb = new QSizeGrip(this);
    lowerlay->addWidget(sb);
229

230
    m_latexRenderer = new GuiUtils::LatexRenderer();
231
232
    // The emit below is not wrong even if emitting signals from the constructor it's usually wrong
    // in this case the signal it's connected to inside MovableTitle constructor a few lines above
Albert Astals Cid's avatar
Albert Astals Cid committed
233
    emit containsLatex(GuiUtils::LatexRenderer::mightContainLatex(m_annot->contents())); // clazy:exclude=incorrect-emit
234

Albert Astals Cid's avatar
Albert Astals Cid committed
235
236
    m_title->setTitle(m_annot->window().summary());
    m_title->connectOptionButton(this, SLOT(slotOptionBtn()));
237

Albert Astals Cid's avatar
Albert Astals Cid committed
238
    setGeometry(10, 10, 300, 300);
239
240
241
242

    reloadInfo();
}

243
244
245
246
247
AnnotWindow::~AnnotWindow()
{
    delete m_latexRenderer;
}

Albert Astals Cid's avatar
Albert Astals Cid committed
248
Okular::Annotation *AnnotWindow::annotation() const
249
250
251
252
{
    return m_annot;
}

Albert Astals Cid's avatar
Albert Astals Cid committed
253
void AnnotWindow::updateAnnotation(Okular::Annotation *a)
254
255
256
257
{
    m_annot = a;
}

258
259
void AnnotWindow::reloadInfo()
{
260
    QColor newcolor;
Albert Astals Cid's avatar
Albert Astals Cid committed
261
262
    if (m_annot->subType() == Okular::Annotation::AText) {
        Okular::TextAnnotation *textAnn = static_cast<Okular::TextAnnotation *>(m_annot);
263
        if (textAnn->textType() == Okular::TextAnnotation::InPlace && textAnn->inplaceIntent() == Okular::TextAnnotation::TypeWriter) {
264
            newcolor = QColor(0xfd, 0xfd, 0x96);
265
        }
266
    }
267
    if (!newcolor.isValid()) {
268
        newcolor = m_annot->style().color().isValid() ? QColor(m_annot->style().color().red(), m_annot->style().color().green(), m_annot->style().color().blue(), 255) : Qt::yellow;
269
    }
Albert Astals Cid's avatar
Albert Astals Cid committed
270
    if (newcolor != m_color) {
271
        m_color = newcolor;
Albert Astals Cid's avatar
Albert Astals Cid committed
272
        setPalette(QPalette(m_color));
273
        QPalette pl = textEdit->palette();
Albert Astals Cid's avatar
Albert Astals Cid committed
274
275
        pl.setColor(QPalette::Base, m_color);
        textEdit->setPalette(pl);
276
    }
Albert Astals Cid's avatar
Albert Astals Cid committed
277
278
    m_title->setAuthor(m_annot->author());
    m_title->setDate(m_annot->modificationDate());
279
280
}

281
282
283
284
285
int AnnotWindow::pageNumber() const
{
    return m_page;
}

Albert Astals Cid's avatar
Albert Astals Cid committed
286
void AnnotWindow::showEvent(QShowEvent *event)
287
{
Albert Astals Cid's avatar
Albert Astals Cid committed
288
    QFrame::showEvent(event);
289
290
291
292
293

    // focus the content area by default
    textEdit->setFocus();
}

294
bool AnnotWindow::eventFilter(QObject *o, QEvent *e)
295
{
Albert Astals Cid's avatar
Albert Astals Cid committed
296
297
298
    if (e->type() == QEvent::ShortcutOverride) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent->key() == Qt::Key_Escape) {
299
            e->accept();
300
301
            return true;
        }
Albert Astals Cid's avatar
Albert Astals Cid committed
302
303
304
    } else if (e->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent == QKeySequence::Undo) {
305
306
            m_document->undo();
            return true;
Albert Astals Cid's avatar
Albert Astals Cid committed
307
        } else if (keyEvent == QKeySequence::Redo) {
308
309
            m_document->redo();
            return true;
310
311
312
        } else if (keyEvent->key() == Qt::Key_Escape) {
            close();
            return true;
313
        }
Albert Astals Cid's avatar
Albert Astals Cid committed
314
    } else if (e->type() == QEvent::FocusIn) {
315
316
        raise();
    }
317
    return QFrame::eventFilter(o, e);
318
319
}

Albert Astals Cid's avatar
Albert Astals Cid committed
320
void AnnotWindow::slotUpdateUndoAndRedoInContextMenu(QMenu *menu)
321
{
322
    if (!menu) {
Albert Astals Cid's avatar
Albert Astals Cid committed
323
        return;
324
    }
325
326
327
328

    QList<QAction *> actionList = menu->actions();
    enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs };

329
330
331
332
333
334
335
336
337
338
    QAction *kundo = KStandardAction::create(
        KStandardAction::Undo,
        m_document,
        [doc = m_document] {
            // We need a QueuedConnection because undoing may end up destroying the menu this action is on
            // because it will undo the addition of the annotation. If it's not queued things gets unhappy
            // because the menu is destroyed in the middle of processing the click on the menu itself
            QMetaObject::invokeMethod(doc, &Okular::Document::undo, Qt::QueuedConnection);
        },
        menu);
Albert Astals Cid's avatar
Albert Astals Cid committed
339
    QAction *kredo = KStandardAction::create(KStandardAction::Redo, m_document, SLOT(redo()), menu);
Laurent Montel's avatar
Laurent Montel committed
340
341
    connect(m_document, &Okular::Document::canUndoChanged, kundo, &QAction::setEnabled);
    connect(m_document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled);
342
343
344
345
346
347
348
349
350
351
352
353
354
355
    kundo->setEnabled(m_document->canUndo());
    kredo->setEnabled(m_document->canRedo());

    QAction *oldUndo, *oldRedo;
    oldUndo = actionList[UndoAct];
    oldRedo = actionList[RedoAct];

    menu->insertAction(oldUndo, kundo);
    menu->insertAction(oldRedo, kredo);

    menu->removeAction(oldUndo);
    menu->removeAction(oldRedo);
}

356
void AnnotWindow::slotOptionBtn()
357
{
Albert Astals Cid's avatar
Albert Astals Cid committed
358
359
    // TODO: call context menu in pageview
    // emit sig...
360
}
361

362
void AnnotWindow::slotsaveWindowText()
363
{
364
365
    const QString contents = textEdit->toPlainText();
    const int cursorPos = textEdit->textCursor().position();
Albert Astals Cid's avatar
Albert Astals Cid committed
366
367
368
    if (contents != m_annot->contents()) {
        m_document->editPageAnnotationContents(m_page, m_annot, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos);
        emit containsLatex(GuiUtils::LatexRenderer::mightContainLatex(textEdit->toPlainText()));
369
    }
370
371
    m_prevCursorPos = cursorPos;
    m_prevAnchorPos = textEdit->textCursor().anchor();
372
373
}

Albert Astals Cid's avatar
Albert Astals Cid committed
374
void AnnotWindow::renderLatex(bool render)
375
{
Albert Astals Cid's avatar
Albert Astals Cid committed
376
377
    if (render) {
        textEdit->setReadOnly(true);
Laurent Montel's avatar
Laurent Montel committed
378
379
        disconnect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText);
        disconnect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText);
Albert Astals Cid's avatar
Albert Astals Cid committed
380
        textEdit->setAcceptRichText(true);
381
        QString contents = m_annot->contents();
Albert Astals Cid's avatar
Albert Astals Cid committed
382
        contents = Qt::convertFromPlainText(contents);
383
384
385
        QColor fontColor = textEdit->textColor();
        int fontSize = textEdit->fontPointSize();
        QString latexOutput;
Albert Astals Cid's avatar
Albert Astals Cid committed
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
        GuiUtils::LatexRenderer::Error errorCode = m_latexRenderer->renderLatexInHtml(contents, fontColor, fontSize, Okular::Utils::realDpi(nullptr).width(), latexOutput);
        switch (errorCode) {
        case GuiUtils::LatexRenderer::LatexNotFound:
            KMessageBox::sorry(this, i18n("Cannot find latex executable."), i18n("LaTeX rendering failed"));
            m_title->uncheckLatexButton();
            renderLatex(false);
            break;
        case GuiUtils::LatexRenderer::DvipngNotFound:
            KMessageBox::sorry(this, i18n("Cannot find dvipng executable."), i18n("LaTeX rendering failed"));
            m_title->uncheckLatexButton();
            renderLatex(false);
            break;
        case GuiUtils::LatexRenderer::LatexFailed:
            KMessageBox::detailedSorry(this, i18n("A problem occurred during the execution of the 'latex' command."), latexOutput, i18n("LaTeX rendering failed"));
            m_title->uncheckLatexButton();
            renderLatex(false);
            break;
        case GuiUtils::LatexRenderer::DvipngFailed:
            KMessageBox::sorry(this, i18n("A problem occurred during the execution of the 'dvipng' command."), i18n("LaTeX rendering failed"));
            m_title->uncheckLatexButton();
            renderLatex(false);
            break;
        case GuiUtils::LatexRenderer::NoError:
        default:
            textEdit->setHtml(contents);
            break;
412
        }
Albert Astals Cid's avatar
Albert Astals Cid committed
413
414
415
    } else {
        textEdit->setAcceptRichText(false);
        textEdit->setPlainText(m_annot->contents());
Laurent Montel's avatar
Laurent Montel committed
416
417
        connect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText);
        connect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText);
Albert Astals Cid's avatar
Albert Astals Cid committed
418
        textEdit->setReadOnly(false);
419
    }
420
}
421

Albert Astals Cid's avatar
Albert Astals Cid committed
422
void AnnotWindow::slotHandleContentsChangedByUndoRedo(Okular::Annotation *annot, const QString &contents, int cursorPos, int anchorPos)
423
{
Albert Astals Cid's avatar
Albert Astals Cid committed
424
    if (annot != m_annot) {
425
426
427
428
429
430
        return;
    }

    textEdit->setPlainText(contents);
    QTextCursor c = textEdit->textCursor();
    c.setPosition(anchorPos);
Albert Astals Cid's avatar
Albert Astals Cid committed
431
    c.setPosition(cursorPos, QTextCursor::KeepAnchor);
432
433
434
435
    m_prevCursorPos = cursorPos;
    m_prevAnchorPos = anchorPos;
    textEdit->setTextCursor(c);
    textEdit->setFocus();
Albert Astals Cid's avatar
Albert Astals Cid committed
436
    emit containsLatex(GuiUtils::LatexRenderer::mightContainLatex(m_annot->contents()));
437
438
}

Albert Vaca Cintora's avatar
Albert Vaca Cintora committed
439
#include "annotwindow.moc"