1 /*
2     SPDX-FileCopyrightText: 2004-2005 Enrico Ros <eros.kde@email.it>
3 
4     Work sponsored by the LiMux project of the city of Munich:
5     SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 
10 #include "pageviewutils.h"
11 
12 // qt/kde includes
13 #include <QApplication>
14 #include <QMenu>
15 #include <QPainter>
16 #include <QTimer>
17 
18 // local includes
19 #include "core/form.h"
20 #include "core/page.h"
21 #include "formwidgets.h"
22 #include "settings.h"
23 #include "videowidget.h"
24 
25 /*********************/
26 /** PageViewItem     */
27 /*********************/
28 
PageViewItem(const Okular::Page * page)29 PageViewItem::PageViewItem(const Okular::Page *page)
30     : m_page(page)
31     , m_zoomFactor(1.0)
32     , m_visible(true)
33     , m_formsVisible(false)
34     , m_crop(0., 0., 1., 1.)
35 {
36 }
37 
~PageViewItem()38 PageViewItem::~PageViewItem()
39 {
40     qDeleteAll(m_formWidgets);
41     qDeleteAll(m_videoWidgets);
42 }
43 
page() const44 const Okular::Page *PageViewItem::page() const
45 {
46     return m_page;
47 }
48 
pageNumber() const49 int PageViewItem::pageNumber() const
50 {
51     return m_page->number();
52 }
53 
croppedGeometry() const54 const QRect &PageViewItem::croppedGeometry() const
55 {
56     return m_croppedGeometry;
57 }
58 
croppedWidth() const59 int PageViewItem::croppedWidth() const
60 {
61     return m_croppedGeometry.width();
62 }
63 
croppedHeight() const64 int PageViewItem::croppedHeight() const
65 {
66     return m_croppedGeometry.height();
67 }
68 
uncroppedGeometry() const69 const QRect &PageViewItem::uncroppedGeometry() const
70 {
71     return m_uncroppedGeometry;
72 }
73 
uncroppedWidth() const74 int PageViewItem::uncroppedWidth() const
75 {
76     return m_uncroppedGeometry.width();
77 }
78 
uncroppedHeight() const79 int PageViewItem::uncroppedHeight() const
80 {
81     return m_uncroppedGeometry.height();
82 }
83 
crop() const84 const Okular::NormalizedRect &PageViewItem::crop() const
85 {
86     return m_crop;
87 }
88 
zoomFactor() const89 double PageViewItem::zoomFactor() const
90 {
91     return m_zoomFactor;
92 }
93 
absToPageX(double absX) const94 double PageViewItem::absToPageX(double absX) const
95 {
96     return (absX - m_uncroppedGeometry.left()) / m_uncroppedGeometry.width();
97 }
98 
absToPageY(double absY) const99 double PageViewItem::absToPageY(double absY) const
100 {
101     return (absY - m_uncroppedGeometry.top()) / m_uncroppedGeometry.height();
102 }
103 
isVisible() const104 bool PageViewItem::isVisible() const
105 {
106     return m_visible;
107 }
108 
formWidgets()109 QSet<FormWidgetIface *> &PageViewItem::formWidgets()
110 {
111     return m_formWidgets;
112 }
113 
videoWidgets()114 QHash<Okular::Movie *, VideoWidget *> &PageViewItem::videoWidgets()
115 {
116     return m_videoWidgets;
117 }
118 
setWHZC(int w,int h,double z,const Okular::NormalizedRect & c)119 void PageViewItem::setWHZC(int w, int h, double z, const Okular::NormalizedRect &c)
120 {
121     m_croppedGeometry.setWidth(w);
122     m_croppedGeometry.setHeight(h);
123     m_zoomFactor = z;
124     m_crop = c;
125     m_uncroppedGeometry.setWidth(qRound(w / (c.right - c.left)));
126     m_uncroppedGeometry.setHeight(qRound(h / (c.bottom - c.top)));
127     for (FormWidgetIface *fwi : qAsConst(m_formWidgets)) {
128         Okular::NormalizedRect r = fwi->rect();
129         fwi->setWidthHeight(qRound(fabs(r.right - r.left) * m_uncroppedGeometry.width()), qRound(fabs(r.bottom - r.top) * m_uncroppedGeometry.height()));
130     }
131     for (VideoWidget *vw : qAsConst(m_videoWidgets)) {
132         const Okular::NormalizedRect r = vw->normGeometry();
133         vw->resize(qRound(fabs(r.right - r.left) * m_uncroppedGeometry.width()), qRound(fabs(r.bottom - r.top) * m_uncroppedGeometry.height()));
134     }
135 }
136 
moveTo(int x,int y)137 void PageViewItem::moveTo(int x, int y)
138 // Assumes setWHZC() has already been called
139 {
140     m_croppedGeometry.moveLeft(x);
141     m_croppedGeometry.moveTop(y);
142     m_uncroppedGeometry.moveLeft(qRound(x - m_crop.left * m_uncroppedGeometry.width()));
143     m_uncroppedGeometry.moveTop(qRound(y - m_crop.top * m_uncroppedGeometry.height()));
144     QSet<FormWidgetIface *>::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end();
145     for (; it != itEnd; ++it) {
146         Okular::NormalizedRect r = (*it)->rect();
147         (*it)->moveTo(qRound(x + m_uncroppedGeometry.width() * r.left) + 1, qRound(y + m_uncroppedGeometry.height() * r.top) + 1);
148     }
149     for (VideoWidget *vw : qAsConst(m_videoWidgets)) {
150         const Okular::NormalizedRect r = vw->normGeometry();
151         vw->move(qRound(x + m_uncroppedGeometry.width() * r.left) + 1, qRound(y + m_uncroppedGeometry.height() * r.top) + 1);
152     }
153 }
154 
setVisible(bool visible)155 void PageViewItem::setVisible(bool visible)
156 {
157     setFormWidgetsVisible(visible && m_formsVisible);
158     m_visible = visible;
159 }
160 
invalidate()161 void PageViewItem::invalidate()
162 {
163     m_croppedGeometry.setRect(0, 0, 0, 0);
164     m_uncroppedGeometry.setRect(0, 0, 0, 0);
165 }
166 
setFormWidgetsVisible(bool visible)167 bool PageViewItem::setFormWidgetsVisible(bool visible)
168 {
169     m_formsVisible = visible;
170 
171     if (!m_visible)
172         return false;
173 
174     bool somehadfocus = false;
175     QSet<FormWidgetIface *>::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end();
176     for (; it != itEnd; ++it) {
177         bool hadfocus = (*it)->setVisibility(visible && (*it)->formField()->isVisible() && FormWidgetsController::shouldFormWidgetBeShown((*it)->formField()));
178         somehadfocus = somehadfocus || hadfocus;
179     }
180     return somehadfocus;
181 }
182 
reloadFormWidgetsState()183 void PageViewItem::reloadFormWidgetsState()
184 {
185     for (FormWidgetIface *fwi : qAsConst(m_formWidgets)) {
186         fwi->setVisibility(fwi->formField()->isVisible() && FormWidgetsController::shouldFormWidgetBeShown(fwi->formField()));
187     }
188 }
189 
190 /*********************/
191 /** PageViewMessage  */
192 /*********************/
193 
PageViewMessage(QWidget * parent)194 PageViewMessage::PageViewMessage(QWidget *parent)
195     : QWidget(parent)
196     , m_timer(nullptr)
197     , m_lineSpacing(0)
198 {
199     setObjectName(QStringLiteral("pageViewMessage"));
200     setFocusPolicy(Qt::NoFocus);
201     QPalette pal = palette();
202     pal.setColor(QPalette::Active, QPalette::Window, QApplication::palette().color(QPalette::Active, QPalette::Window));
203     setPalette(pal);
204     // if the layout is LtR, we can safely place it in the right position
205     if (layoutDirection() == Qt::LeftToRight)
206         move(10, 10);
207     resize(0, 0);
208     hide();
209 }
210 
display(const QString & message,const QString & details,Icon icon,int durationMs)211 void PageViewMessage::display(const QString &message, const QString &details, Icon icon, int durationMs)
212 // give Caesar what belongs to Caesar: code taken from Amarok's osd.h/.cpp
213 // "redde (reddite, pl.) cesari quae sunt cesaris", just btw.  :)
214 // The code has been heavily modified since then.
215 {
216     if (!Okular::Settings::showOSD()) {
217         hide();
218         return;
219     }
220 
221     // set text
222     m_message = message;
223     m_details = details;
224     // reset vars
225     m_lineSpacing = 0;
226 
227     // load icon (if set)
228     m_symbol = QPixmap();
229     const auto symbolSize = style()->pixelMetric(QStyle::PM_SmallIconSize);
230     if (icon != None) {
231         switch (icon) {
232         case Annotation:
233             m_symbol = QIcon::fromTheme(QStringLiteral("draw-freehand")).pixmap(symbolSize);
234             break;
235         case Find:
236             m_symbol = QIcon::fromTheme(QStringLiteral("zoom-original")).pixmap(symbolSize);
237             break;
238         case Error:
239             m_symbol = QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(symbolSize);
240             break;
241         case Warning:
242             m_symbol = QIcon::fromTheme(QStringLiteral("dialog-warning")).pixmap(symbolSize);
243             break;
244         default:
245             m_symbol = QIcon::fromTheme(QStringLiteral("dialog-information")).pixmap(symbolSize);
246             break;
247         }
248     }
249 
250     computeSizeAndResize();
251     // show widget and schedule a repaint
252     show();
253     update();
254 
255     // close the message window after given mS
256     if (durationMs > 0) {
257         if (!m_timer) {
258             m_timer = new QTimer(this);
259             m_timer->setSingleShot(true);
260             connect(m_timer, &QTimer::timeout, this, &PageViewMessage::hide);
261         }
262         m_timer->start(durationMs);
263     } else if (m_timer)
264         m_timer->stop();
265 
266     qobject_cast<QAbstractScrollArea *>(parentWidget())->viewport()->installEventFilter(this);
267 }
268 
computeTextRect(const QString & message,int extra_width) const269 QRect PageViewMessage::computeTextRect(const QString &message, int extra_width) const
270 // Return the QRect which embeds the text
271 {
272     int charSize = fontMetrics().averageCharWidth();
273     /* width of the viewport, minus 20 (~ size removed by further resizing),
274        minus the extra size (usually the icon width), minus (a bit empirical)
275        twice the mean width of a character to ensure that the bounding box is
276        really smaller than the container.
277      */
278     const int boundingWidth = qobject_cast<QAbstractScrollArea *>(parentWidget())->viewport()->width() - 20 - (extra_width > 0 ? 2 + extra_width : 0) - 2 * charSize;
279     QRect textRect = fontMetrics().boundingRect(0, 0, boundingWidth, 0, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, message);
280     textRect.translate(-textRect.left(), -textRect.top());
281     textRect.adjust(0, 0, 2, 2);
282 
283     return textRect;
284 }
285 
computeSizeAndResize()286 void PageViewMessage::computeSizeAndResize()
287 {
288     // determine text rectangle
289     const QRect textRect = computeTextRect(m_message, m_symbol.width());
290     int width = textRect.width(), height = textRect.height();
291 
292     if (!m_details.isEmpty()) {
293         // determine details text rectangle
294         const QRect detailsRect = computeTextRect(m_details, m_symbol.width());
295         width = qMax(width, detailsRect.width());
296         height += detailsRect.height();
297 
298         // plus add a ~60% line spacing
299         m_lineSpacing = static_cast<int>(fontMetrics().height() * 0.6);
300         height += m_lineSpacing;
301     }
302 
303     // update geometry with icon information
304     if (!m_symbol.isNull()) {
305         width += 2 + m_symbol.width();
306         height = qMax(height, m_symbol.height());
307     }
308 
309     // resize widget
310     resize(QRect(0, 0, width + 10, height + 8).size());
311 
312     // if the layout is RtL, we can move it to the right place only after we
313     // know how much size it will take
314     if (layoutDirection() == Qt::RightToLeft)
315         move(parentWidget()->width() - geometry().width() - 10 - 1, 10);
316 }
317 
eventFilter(QObject * obj,QEvent * event)318 bool PageViewMessage::eventFilter(QObject *obj, QEvent *event)
319 {
320     /* if the parent object (scroll area) resizes, the message should
321        resize as well */
322     if (event->type() == QEvent::Resize) {
323         QResizeEvent *resizeEvent = static_cast<QResizeEvent *>(event);
324         if (resizeEvent->oldSize() != resizeEvent->size()) {
325             computeSizeAndResize();
326         }
327     }
328     // standard event processing
329     return QObject::eventFilter(obj, event);
330 }
331 
paintEvent(QPaintEvent *)332 void PageViewMessage::paintEvent(QPaintEvent * /* e */)
333 {
334     const QRect textRect = computeTextRect(m_message, m_symbol.width());
335 
336     QRect detailsRect;
337     if (!m_details.isEmpty()) {
338         detailsRect = computeTextRect(m_details, m_symbol.width());
339     }
340 
341     int textXOffset = 0,
342         // add 2 to account for the reduced drawRoundedRect later
343         textYOffset = (geometry().height() - textRect.height() - detailsRect.height() - m_lineSpacing + 2) / 2, iconXOffset = 0, iconYOffset = !m_symbol.isNull() ? (geometry().height() - m_symbol.height()) / 2 : 0, shadowOffset = 1;
344 
345     if (layoutDirection() == Qt::RightToLeft)
346         iconXOffset = 2 + textRect.width();
347     else
348         textXOffset = 2 + m_symbol.width();
349 
350     // draw background
351     QPainter painter(this);
352     painter.setRenderHint(QPainter::Antialiasing, true);
353     painter.setPen(Qt::black);
354     painter.setBrush(palette().color(QPalette::Window));
355     painter.translate(0.5, 0.5);
356     painter.drawRoundedRect(1, 1, width() - 2, height() - 2, 1600.0 / width(), 1600.0 / height());
357 
358     // draw icon if present
359     if (!m_symbol.isNull())
360         painter.drawPixmap(5 + iconXOffset, iconYOffset, m_symbol, 0, 0, m_symbol.width(), m_symbol.height());
361 
362     const int xStartPoint = 5 + textXOffset;
363     const int yStartPoint = textYOffset;
364     const int textDrawingFlags = Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap;
365 
366     // draw shadow and text
367     painter.setPen(palette().color(QPalette::Window).darker(115));
368     painter.drawText(xStartPoint + shadowOffset, yStartPoint + shadowOffset, textRect.width(), textRect.height(), textDrawingFlags, m_message);
369     if (!m_details.isEmpty())
370         painter.drawText(xStartPoint + shadowOffset, yStartPoint + textRect.height() + m_lineSpacing + shadowOffset, textRect.width(), detailsRect.height(), textDrawingFlags, m_details);
371     painter.setPen(palette().color(QPalette::WindowText));
372     painter.drawText(xStartPoint, yStartPoint, textRect.width(), textRect.height(), textDrawingFlags, m_message);
373     if (!m_details.isEmpty())
374         painter.drawText(xStartPoint + shadowOffset, yStartPoint + textRect.height() + m_lineSpacing, textRect.width(), detailsRect.height(), textDrawingFlags, m_details);
375 }
376 
mousePressEvent(QMouseEvent *)377 void PageViewMessage::mousePressEvent(QMouseEvent * /*e*/)
378 {
379     if (m_timer)
380         m_timer->stop();
381     hide();
382 }
383