File indexing completed on 2024-04-28 15:51:41

0001 /*
0002     SPDX-FileCopyrightText: 2006 Chu Xiaodong <xiaodongchu@gmail.com>
0003     SPDX-FileCopyrightText: 2006 Pino Toscano <pino@kde.org>
0004 
0005     Work sponsored by the LiMux project of the city of Munich:
0006     SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "annotwindow.h"
0012 
0013 // qt/kde includes
0014 #include <KLocalizedString>
0015 #include <KStandardAction>
0016 #include <KTextEdit>
0017 #include <QAction>
0018 #include <QApplication>
0019 #include <QDebug>
0020 #include <QEvent>
0021 #include <QFont>
0022 #include <QFontInfo>
0023 #include <QFontMetrics>
0024 #include <QLabel>
0025 #include <QLayout>
0026 #include <QMenu>
0027 #include <QPushButton>
0028 #include <QSizeGrip>
0029 #include <QStyle>
0030 #include <QToolButton>
0031 
0032 // local includes
0033 #include "core/annotations.h"
0034 #include "core/document.h"
0035 #include "latexrenderer.h"
0036 #include <KMessageBox>
0037 #include <core/utils.h>
0038 
0039 class CloseButton : public QPushButton
0040 {
0041     Q_OBJECT
0042 
0043 public:
0044     explicit CloseButton(QWidget *parent = Q_NULLPTR)
0045         : QPushButton(parent)
0046     {
0047         setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0048         QSize size = QSize(14, 14);
0049         setFixedSize(size);
0050         setIcon(style()->standardIcon(QStyle::SP_DockWidgetCloseButton));
0051         setIconSize(size);
0052         setToolTip(i18n("Close this note"));
0053         setCursor(Qt::ArrowCursor);
0054     }
0055 };
0056 
0057 class MovableTitle : public QWidget
0058 {
0059     Q_OBJECT
0060 
0061 public:
0062     explicit MovableTitle(AnnotWindow *parent)
0063         : QWidget(parent)
0064     {
0065         QVBoxLayout *mainlay = new QVBoxLayout(this);
0066         mainlay->setContentsMargins(0, 0, 0, 0);
0067         mainlay->setSpacing(0);
0068         // close button row
0069         QHBoxLayout *buttonlay = new QHBoxLayout();
0070         mainlay->addLayout(buttonlay);
0071         titleLabel = new QLabel(this);
0072         QFont f = titleLabel->font();
0073         f.setBold(true);
0074         titleLabel->setFont(f);
0075         titleLabel->setCursor(Qt::SizeAllCursor);
0076         buttonlay->addWidget(titleLabel);
0077         dateLabel = new QLabel(this);
0078         dateLabel->setAlignment(Qt::AlignTop | Qt::AlignRight);
0079         f = dateLabel->font();
0080         f.setPointSize(QFontInfo(f).pointSize() - 2);
0081         dateLabel->setFont(f);
0082         dateLabel->setCursor(Qt::SizeAllCursor);
0083         buttonlay->addWidget(dateLabel);
0084         CloseButton *close = new CloseButton(this);
0085         connect(close, &QAbstractButton::clicked, parent, &QWidget::close);
0086         buttonlay->addWidget(close);
0087         // option button row
0088         QHBoxLayout *optionlay = new QHBoxLayout();
0089         mainlay->addLayout(optionlay);
0090         authorLabel = new QLabel(this);
0091         authorLabel->setCursor(Qt::SizeAllCursor);
0092         authorLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
0093         optionlay->addWidget(authorLabel);
0094         optionButton = new QToolButton(this);
0095         QString opttext = i18n("Options");
0096         optionButton->setText(opttext);
0097         optionButton->setAutoRaise(true);
0098         QSize s = QFontMetrics(optionButton->font()).boundingRect(opttext).size() + QSize(8, 8);
0099         optionButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0100         optionButton->setFixedSize(s);
0101         optionlay->addWidget(optionButton);
0102         // ### disabled for now
0103         optionButton->hide();
0104         latexButton = new QToolButton(this);
0105         QHBoxLayout *latexlay = new QHBoxLayout();
0106         QString latextext = i18n("This annotation may contain LaTeX code.\nClick here to render.");
0107         latexButton->setText(latextext);
0108         latexButton->setAutoRaise(true);
0109         s = QFontMetrics(latexButton->font()).boundingRect(0, 0, this->width(), this->height(), 0, latextext).size() + QSize(8, 8);
0110         latexButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0111         latexButton->setFixedSize(s);
0112         latexButton->setCheckable(true);
0113         latexButton->setVisible(false);
0114         latexlay->addSpacing(1);
0115         latexlay->addWidget(latexButton);
0116         latexlay->addSpacing(1);
0117         mainlay->addLayout(latexlay);
0118         connect(latexButton, &QToolButton::clicked, parent, &AnnotWindow::renderLatex);
0119         connect(parent, &AnnotWindow::containsLatex, latexButton, &QWidget::setVisible);
0120 
0121         titleLabel->installEventFilter(this);
0122         dateLabel->installEventFilter(this);
0123         authorLabel->installEventFilter(this);
0124     }
0125 
0126     bool eventFilter(QObject *obj, QEvent *e) override
0127     {
0128         if (obj != titleLabel && obj != authorLabel && obj != dateLabel) {
0129             return false;
0130         }
0131 
0132         QMouseEvent *me = nullptr;
0133         switch (e->type()) {
0134         case QEvent::MouseButtonPress:
0135             me = (QMouseEvent *)e;
0136             mousePressPos = me->pos();
0137             parentWidget()->raise();
0138             break;
0139         case QEvent::MouseButtonRelease:
0140             mousePressPos = QPoint();
0141             break;
0142         case QEvent::MouseMove: {
0143             me = (QMouseEvent *)e;
0144 
0145             // viewport info
0146             const QPoint topLeftPoint = parentWidget()->parentWidget()->pos();
0147             const int viewportHeight = parentWidget()->parentWidget()->height();
0148             const int viewportWidth = parentWidget()->parentWidget()->width();
0149 
0150             // annotation's popup window info
0151             QPoint newPositionPoint = me->pos() - mousePressPos + parentWidget()->pos();
0152             const int annotHeight = parentWidget()->height();
0153             const int annotWidth = parentWidget()->width();
0154 
0155             // make sure x is in range
0156             if (newPositionPoint.x() < topLeftPoint.x()) {
0157                 newPositionPoint.setX(topLeftPoint.x());
0158             } else if (newPositionPoint.x() + annotWidth > topLeftPoint.x() + viewportWidth) {
0159                 newPositionPoint.setX(topLeftPoint.x() + viewportWidth - annotWidth);
0160             }
0161 
0162             // make sure y is in range
0163             if (newPositionPoint.y() < topLeftPoint.y()) {
0164                 newPositionPoint.setY(topLeftPoint.y());
0165             } else if (newPositionPoint.y() + annotHeight > topLeftPoint.y() + viewportHeight) {
0166                 newPositionPoint.setY(topLeftPoint.y() + viewportHeight - annotHeight);
0167             }
0168 
0169             parentWidget()->move(newPositionPoint);
0170             break;
0171         }
0172         default:
0173             return false;
0174         }
0175         return true;
0176     }
0177 
0178     void setTitle(const QString &title)
0179     {
0180         titleLabel->setText(QStringLiteral(" ") + title);
0181     }
0182 
0183     void setDate(const QDateTime &dt)
0184     {
0185         dateLabel->setText(QLocale().toString(dt.toTimeSpec(Qt::LocalTime), QLocale::ShortFormat) + QLatin1Char(' '));
0186     }
0187 
0188     void setAuthor(const QString &author)
0189     {
0190         authorLabel->setText(QStringLiteral(" ") + author);
0191     }
0192 
0193     void connectOptionButton(QObject *recv, const char *method)
0194     {
0195         connect(optionButton, SIGNAL(clicked()), recv, method);
0196     }
0197 
0198     void uncheckLatexButton()
0199     {
0200         latexButton->setChecked(false);
0201     }
0202 
0203 private:
0204     QLabel *titleLabel;
0205     QLabel *dateLabel;
0206     QLabel *authorLabel;
0207     QPoint mousePressPos;
0208     QToolButton *optionButton;
0209     QToolButton *latexButton;
0210 };
0211 
0212 // Qt::SubWindow is needed to make QSizeGrip work
0213 AnnotWindow::AnnotWindow(QWidget *parent, Okular::Annotation *annot, Okular::Document *document, int page)
0214     : QFrame(parent, Qt::SubWindow)
0215     , m_annot(annot)
0216     , m_document(document)
0217     , m_page(page)
0218 {
0219     setAutoFillBackground(true);
0220     setFrameStyle(Panel | Raised);
0221     setAttribute(Qt::WA_DeleteOnClose);
0222     setObjectName(QStringLiteral("AnnotWindow"));
0223 
0224     const bool canEditAnnotation = m_document->canModifyPageAnnotation(annot);
0225 
0226     textEdit = new KTextEdit(this);
0227     textEdit->setAcceptRichText(false);
0228     textEdit->setPlainText(m_annot->contents());
0229     textEdit->installEventFilter(this);
0230     textEdit->setUndoRedoEnabled(false);
0231 
0232     m_prevCursorPos = textEdit->textCursor().position();
0233     m_prevAnchorPos = textEdit->textCursor().anchor();
0234 
0235     connect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText);
0236     connect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText);
0237     connect(textEdit, &KTextEdit::aboutToShowContextMenu, this, &AnnotWindow::slotUpdateUndoAndRedoInContextMenu);
0238     connect(m_document, &Okular::Document::annotationContentsChangedByUndoRedo, this, &AnnotWindow::slotHandleContentsChangedByUndoRedo);
0239 
0240     if (!canEditAnnotation) {
0241         textEdit->setReadOnly(true);
0242     }
0243 
0244     QVBoxLayout *mainlay = new QVBoxLayout(this);
0245     mainlay->setContentsMargins(2, 2, 2, 2);
0246     mainlay->setSpacing(0);
0247     m_title = new MovableTitle(this);
0248     mainlay->addWidget(m_title);
0249     mainlay->addWidget(textEdit);
0250     QHBoxLayout *lowerlay = new QHBoxLayout();
0251     mainlay->addLayout(lowerlay);
0252     lowerlay->addItem(new QSpacerItem(5, 5, QSizePolicy::Expanding, QSizePolicy::Fixed));
0253     QSizeGrip *sb = new QSizeGrip(this);
0254     lowerlay->addWidget(sb);
0255 
0256     m_latexRenderer = new GuiUtils::LatexRenderer();
0257     // The Q_EMIT below is not wrong even if emitting signals from the constructor it's usually wrong
0258     // in this case the signal it's connected to inside MovableTitle constructor a few lines above
0259     Q_EMIT containsLatex(GuiUtils::LatexRenderer::mightContainLatex(m_annot->contents())); // clazy:exclude=incorrect-emit
0260 
0261     m_title->setTitle(m_annot->window().summary());
0262     m_title->connectOptionButton(this, SLOT(slotOptionBtn()));
0263 
0264     setGeometry(10, 10, 300, 300);
0265 
0266     reloadInfo();
0267 }
0268 
0269 AnnotWindow::~AnnotWindow()
0270 {
0271     delete m_latexRenderer;
0272 }
0273 
0274 Okular::Annotation *AnnotWindow::annotation() const
0275 {
0276     return m_annot;
0277 }
0278 
0279 void AnnotWindow::updateAnnotation(Okular::Annotation *a)
0280 {
0281     m_annot = a;
0282 }
0283 
0284 void AnnotWindow::reloadInfo()
0285 {
0286     QColor newcolor;
0287     if (m_annot->subType() == Okular::Annotation::AText) {
0288         Okular::TextAnnotation *textAnn = static_cast<Okular::TextAnnotation *>(m_annot);
0289         if (textAnn->textType() == Okular::TextAnnotation::InPlace && textAnn->inplaceIntent() == Okular::TextAnnotation::TypeWriter) {
0290             newcolor = QColor(0xfd, 0xfd, 0x96);
0291         }
0292     }
0293     if (!newcolor.isValid()) {
0294         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;
0295     }
0296     if (newcolor != m_color) {
0297         m_color = newcolor;
0298         setPalette(QPalette(m_color));
0299         QPalette pl = textEdit->palette();
0300         pl.setColor(QPalette::Base, m_color);
0301         textEdit->setPalette(pl);
0302     }
0303     m_title->setAuthor(m_annot->author());
0304     m_title->setDate(m_annot->modificationDate());
0305 }
0306 
0307 int AnnotWindow::pageNumber() const
0308 {
0309     return m_page;
0310 }
0311 
0312 void AnnotWindow::showEvent(QShowEvent *event)
0313 {
0314     QFrame::showEvent(event);
0315 
0316     // focus the content area by default
0317     textEdit->setFocus();
0318 }
0319 
0320 bool AnnotWindow::eventFilter(QObject *o, QEvent *e)
0321 {
0322     if (e->type() == QEvent::ShortcutOverride) {
0323         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
0324         if (keyEvent->key() == Qt::Key_Escape) {
0325             e->accept();
0326             return true;
0327         }
0328     } else if (e->type() == QEvent::KeyPress) {
0329         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
0330         if (keyEvent == QKeySequence::Undo) {
0331             m_document->undo();
0332             return true;
0333         } else if (keyEvent == QKeySequence::Redo) {
0334             m_document->redo();
0335             return true;
0336         } else if (keyEvent->key() == Qt::Key_Escape) {
0337             close();
0338             return true;
0339         }
0340     } else if (e->type() == QEvent::FocusIn) {
0341         raise();
0342     }
0343     return QFrame::eventFilter(o, e);
0344 }
0345 
0346 void AnnotWindow::slotUpdateUndoAndRedoInContextMenu(QMenu *menu)
0347 {
0348     if (!menu) {
0349         return;
0350     }
0351 
0352     QList<QAction *> actionList = menu->actions();
0353     enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs };
0354 
0355     QAction *kundo = KStandardAction::create(
0356         KStandardAction::Undo,
0357         m_document,
0358         [doc = m_document] {
0359             // We need a QueuedConnection because undoing may end up destroying the menu this action is on
0360             // because it will undo the addition of the annotation. If it's not queued things gets unhappy
0361             // because the menu is destroyed in the middle of processing the click on the menu itself
0362             QMetaObject::invokeMethod(doc, &Okular::Document::undo, Qt::QueuedConnection);
0363         },
0364         menu);
0365     QAction *kredo = KStandardAction::create(KStandardAction::Redo, m_document, SLOT(redo()), menu);
0366     connect(m_document, &Okular::Document::canUndoChanged, kundo, &QAction::setEnabled);
0367     connect(m_document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled);
0368     kundo->setEnabled(m_document->canUndo());
0369     kredo->setEnabled(m_document->canRedo());
0370 
0371     QAction *oldUndo, *oldRedo;
0372     oldUndo = actionList[UndoAct];
0373     oldRedo = actionList[RedoAct];
0374 
0375     menu->insertAction(oldUndo, kundo);
0376     menu->insertAction(oldRedo, kredo);
0377 
0378     menu->removeAction(oldUndo);
0379     menu->removeAction(oldRedo);
0380 }
0381 
0382 void AnnotWindow::slotOptionBtn()
0383 {
0384     // TODO: call context menu in pageview
0385     // Q_EMIT sig...
0386 }
0387 
0388 void AnnotWindow::slotsaveWindowText()
0389 {
0390     const QString contents = textEdit->toPlainText();
0391     const int cursorPos = textEdit->textCursor().position();
0392     if (contents != m_annot->contents()) {
0393         m_document->editPageAnnotationContents(m_page, m_annot, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos);
0394         Q_EMIT containsLatex(GuiUtils::LatexRenderer::mightContainLatex(textEdit->toPlainText()));
0395     }
0396     m_prevCursorPos = cursorPos;
0397     m_prevAnchorPos = textEdit->textCursor().anchor();
0398 }
0399 
0400 void AnnotWindow::renderLatex(bool render)
0401 {
0402     if (render) {
0403         textEdit->setReadOnly(true);
0404         disconnect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText);
0405         disconnect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText);
0406         textEdit->setAcceptRichText(true);
0407         QString contents = m_annot->contents();
0408         contents = Qt::convertFromPlainText(contents);
0409         QColor fontColor = textEdit->textColor();
0410         int fontSize = textEdit->fontPointSize();
0411         QString latexOutput;
0412         GuiUtils::LatexRenderer::Error errorCode = m_latexRenderer->renderLatexInHtml(contents, fontColor, fontSize, Okular::Utils::realDpi(nullptr).width(), latexOutput);
0413         switch (errorCode) {
0414         case GuiUtils::LatexRenderer::LatexNotFound:
0415             KMessageBox::error(this, i18n("Cannot find latex executable."), i18n("LaTeX rendering failed"));
0416             m_title->uncheckLatexButton();
0417             renderLatex(false);
0418             break;
0419         case GuiUtils::LatexRenderer::DvipngNotFound:
0420             KMessageBox::error(this, i18n("Cannot find dvipng executable."), i18n("LaTeX rendering failed"));
0421             m_title->uncheckLatexButton();
0422             renderLatex(false);
0423             break;
0424         case GuiUtils::LatexRenderer::LatexFailed:
0425             KMessageBox::detailedError(this, i18n("A problem occurred during the execution of the 'latex' command."), latexOutput, i18n("LaTeX rendering failed"));
0426             m_title->uncheckLatexButton();
0427             renderLatex(false);
0428             break;
0429         case GuiUtils::LatexRenderer::DvipngFailed:
0430             KMessageBox::error(this, i18n("A problem occurred during the execution of the 'dvipng' command."), i18n("LaTeX rendering failed"));
0431             m_title->uncheckLatexButton();
0432             renderLatex(false);
0433             break;
0434         case GuiUtils::LatexRenderer::NoError:
0435         default:
0436             textEdit->setHtml(contents);
0437             break;
0438         }
0439     } else {
0440         textEdit->setAcceptRichText(false);
0441         textEdit->setPlainText(m_annot->contents());
0442         connect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText);
0443         connect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText);
0444         textEdit->setReadOnly(false);
0445     }
0446 }
0447 
0448 void AnnotWindow::slotHandleContentsChangedByUndoRedo(Okular::Annotation *annot, const QString &contents, int cursorPos, int anchorPos)
0449 {
0450     if (annot != m_annot) {
0451         return;
0452     }
0453 
0454     textEdit->setPlainText(contents);
0455     QTextCursor c = textEdit->textCursor();
0456     c.setPosition(anchorPos);
0457     c.setPosition(cursorPos, QTextCursor::KeepAnchor);
0458     m_prevCursorPos = cursorPos;
0459     m_prevAnchorPos = anchorPos;
0460     textEdit->setTextCursor(c);
0461     textEdit->setFocus();
0462     Q_EMIT containsLatex(GuiUtils::LatexRenderer::mightContainLatex(m_annot->contents()));
0463 }
0464 
0465 #include "annotwindow.moc"