1 /*
2     SPDX-FileCopyrightText: 2005 Enrico Ros <eros.kde@email.it>
3     SPDX-FileCopyrightText: 2006 Albert Astals Cid <aacid@kde.org>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "minibar.h"
9 
10 // qt / kde includes
11 #include <KLocalizedString>
12 #include <QIcon>
13 #include <QToolButton>
14 #include <kacceleratormanager.h>
15 #include <kicontheme.h>
16 #include <klineedit.h>
17 #include <qapplication.h>
18 #include <qevent.h>
19 #include <qframe.h>
20 #include <qlabel.h>
21 #include <qlayout.h>
22 #include <qpainter.h>
23 #include <qpushbutton.h>
24 #include <qtoolbar.h>
25 #include <qvalidator.h>
26 
27 // local includes
28 #include "core/document.h"
29 #include "core/page.h"
30 
31 // [private widget] a flat qpushbutton that enlights on hover
32 class HoverButton : public QToolButton
33 {
34     Q_OBJECT
35 public:
36     explicit HoverButton(QWidget *parent);
37 };
38 
MiniBarLogic(QObject * parent,Okular::Document * document)39 MiniBarLogic::MiniBarLogic(QObject *parent, Okular::Document *document)
40     : QObject(parent)
41     , m_document(document)
42 {
43 }
44 
~MiniBarLogic()45 MiniBarLogic::~MiniBarLogic()
46 {
47     m_document->removeObserver(this);
48 }
49 
addMiniBar(MiniBar * miniBar)50 void MiniBarLogic::addMiniBar(MiniBar *miniBar)
51 {
52     m_miniBars.insert(miniBar);
53 }
54 
removeMiniBar(MiniBar * miniBar)55 void MiniBarLogic::removeMiniBar(MiniBar *miniBar)
56 {
57     m_miniBars.remove(miniBar);
58 }
59 
document() const60 Okular::Document *MiniBarLogic::document() const
61 {
62     return m_document;
63 }
64 
currentPage() const65 int MiniBarLogic::currentPage() const
66 {
67     return m_document->currentPage();
68 }
69 
notifySetup(const QVector<Okular::Page * > & pageVector,int setupFlags)70 void MiniBarLogic::notifySetup(const QVector<Okular::Page *> &pageVector, int setupFlags)
71 {
72     // only process data when document changes
73     if (!(setupFlags & Okular::DocumentObserver::DocumentChanged))
74         return;
75 
76     // if document is closed or has no pages, hide widget
77     const int pages = pageVector.count();
78     if (pages < 1) {
79         for (MiniBar *miniBar : qAsConst(m_miniBars)) {
80             miniBar->setEnabled(false);
81         }
82         return;
83     }
84 
85     bool labelsDiffer = false;
86     for (const Okular::Page *page : pageVector) {
87         if (!page->label().isEmpty()) {
88             if (page->label().toInt() != (page->number() + 1)) {
89                 labelsDiffer = true;
90             }
91         }
92     }
93 
94     const QString pagesString = QString::number(pages);
95 
96     // In some documents, there may be labels which are longer than pagesString. Here, we check all the page labels, and if any of the labels are longer than pagesString, we use that string for sizing m_pageLabelEdit
97     QString pagesOrLabelString = pagesString;
98     if (labelsDiffer) {
99         for (const Okular::Page *page : pageVector) {
100             if (!page->label().isEmpty()) {
101                 MiniBar *miniBar = *m_miniBars.constBegin(); // We assume all the minibars have the same font, font size etc, so we just take one minibar for the purpose of calculating the displayed length of the page labels.
102                 if (miniBar->fontMetrics().horizontalAdvance(page->label()) > miniBar->fontMetrics().horizontalAdvance(pagesOrLabelString)) {
103                     pagesOrLabelString = page->label();
104                 }
105             }
106         }
107     }
108 
109     for (MiniBar *miniBar : qAsConst(m_miniBars)) {
110         // resize width of widgets
111         miniBar->resizeForPage(pages, pagesOrLabelString);
112 
113         // update child widgets
114         miniBar->m_pageLabelEdit->setPageLabels(pageVector);
115         miniBar->m_pageNumberEdit->setPagesNumber(pages);
116         miniBar->m_pagesButton->setText(pagesString);
117         miniBar->m_prevButton->setEnabled(false);
118         miniBar->m_nextButton->setEnabled(false);
119         miniBar->m_pageLabelEdit->setVisible(labelsDiffer);
120         miniBar->m_pageNumberLabel->setVisible(labelsDiffer);
121         miniBar->m_pageNumberEdit->setVisible(!labelsDiffer);
122 
123         miniBar->adjustSize();
124 
125         miniBar->setEnabled(true);
126     }
127 }
128 
notifyCurrentPageChanged(int previousPage,int currentPage)129 void MiniBarLogic::notifyCurrentPageChanged(int previousPage, int currentPage)
130 {
131     Q_UNUSED(previousPage)
132 
133     // get current page number
134     const int pages = m_document->pages();
135 
136     // if the document is opened and page is changed
137     if (pages > 0) {
138         const QString pageNumber = QString::number(currentPage + 1);
139         const QString pageLabel = m_document->page(currentPage)->label();
140 
141         for (MiniBar *miniBar : qAsConst(m_miniBars)) {
142             // update prev/next button state
143             miniBar->m_prevButton->setEnabled(currentPage > 0);
144             miniBar->m_nextButton->setEnabled(currentPage < (pages - 1));
145             // update text on widgets
146             miniBar->m_pageNumberEdit->setText(pageNumber);
147             miniBar->m_pageNumberLabel->setText(pageNumber);
148             miniBar->m_pageLabelEdit->setText(pageLabel);
149         }
150     }
151 }
152 
153 /** MiniBar **/
154 
MiniBar(QWidget * parent,MiniBarLogic * miniBarLogic)155 MiniBar::MiniBar(QWidget *parent, MiniBarLogic *miniBarLogic)
156     : QWidget(parent)
157     , m_miniBarLogic(miniBarLogic)
158     , m_oldToolbarParent(nullptr)
159 {
160     setObjectName(QStringLiteral("miniBar"));
161 
162     m_miniBarLogic->addMiniBar(this);
163 
164     QHBoxLayout *horLayout = new QHBoxLayout(this);
165 
166     horLayout->setContentsMargins(0, 0, 0, 0);
167     horLayout->setSpacing(3);
168 
169     QSize buttonSize(KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium);
170     // bottom: left prev_page button
171     m_prevButton = new HoverButton(this);
172     m_prevButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up")));
173     m_prevButton->setIconSize(buttonSize);
174     horLayout->addWidget(m_prevButton);
175     // bottom: left lineEdit (current page box)
176     m_pageNumberEdit = new PageNumberEdit(this);
177     horLayout->addWidget(m_pageNumberEdit);
178     m_pageNumberEdit->installEventFilter(this);
179     // bottom: left labelWidget (current page label)
180     m_pageLabelEdit = new PageLabelEdit(this);
181     horLayout->addWidget(m_pageLabelEdit);
182     m_pageLabelEdit->installEventFilter(this);
183     // bottom: left labelWidget (current page label)
184     m_pageNumberLabel = new QLabel(this);
185     m_pageNumberLabel->setAlignment(Qt::AlignCenter);
186     horLayout->addWidget(m_pageNumberLabel);
187     // bottom: central 'of' label
188     horLayout->addSpacing(5);
189     horLayout->addWidget(new QLabel(i18nc("Layouted like: '5 [pages] of 10'", "of"), this));
190     // bottom: right button
191     m_pagesButton = new HoverButton(this);
192     horLayout->addWidget(m_pagesButton);
193     // bottom: right next_page button
194     m_nextButton = new HoverButton(this);
195     m_nextButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down")));
196     m_nextButton->setIconSize(buttonSize);
197     horLayout->addWidget(m_nextButton);
198 
199     QSizePolicy sp = sizePolicy();
200     sp.setHorizontalPolicy(QSizePolicy::Fixed);
201     sp.setVerticalPolicy(QSizePolicy::Fixed);
202     setSizePolicy(sp);
203 
204     // resize width of widgets
205     resizeForPage(0, QString());
206 
207     // connect signals from child widgets to internal handlers / signals bouncers
208     connect(m_pageNumberEdit, &PageNumberEdit::returnPressed, this, &MiniBar::slotChangePageFromReturn);
209     connect(m_pageLabelEdit, &PageLabelEdit::pageNumberChosen, this, &MiniBar::slotChangePage);
210     connect(m_pagesButton, &QAbstractButton::clicked, this, &MiniBar::gotoPage);
211     connect(m_prevButton, &QAbstractButton::clicked, this, &MiniBar::prevPage);
212     connect(m_nextButton, &QAbstractButton::clicked, this, &MiniBar::nextPage);
213 
214     adjustSize();
215 
216     // widget starts disabled (will be enabled after opening a document)
217     setEnabled(false);
218 }
219 
~MiniBar()220 MiniBar::~MiniBar()
221 {
222     m_miniBarLogic->removeMiniBar(this);
223 }
224 
changeEvent(QEvent * event)225 void MiniBar::changeEvent(QEvent *event)
226 {
227     if (event->type() == QEvent::ParentChange) {
228         QToolBar *tb = dynamic_cast<QToolBar *>(parent());
229         if (tb != m_oldToolbarParent) {
230             if (m_oldToolbarParent) {
231                 disconnect(m_oldToolbarParent, &QToolBar::iconSizeChanged, this, &MiniBar::slotToolBarIconSizeChanged);
232             }
233             m_oldToolbarParent = tb;
234             if (tb) {
235                 connect(tb, &QToolBar::iconSizeChanged, this, &MiniBar::slotToolBarIconSizeChanged);
236                 slotToolBarIconSizeChanged();
237             }
238         }
239     }
240 }
241 
eventFilter(QObject * target,QEvent * event)242 bool MiniBar::eventFilter(QObject *target, QEvent *event)
243 {
244     if (target == m_pageNumberEdit || target == m_pageLabelEdit) {
245         if (event->type() == QEvent::KeyPress) {
246             QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
247             int key = keyEvent->key();
248             if (key == Qt::Key_PageUp || key == Qt::Key_PageDown || key == Qt::Key_Up || key == Qt::Key_Down) {
249                 emit forwardKeyPressEvent(keyEvent);
250                 return true;
251             }
252         }
253     }
254     return false;
255 }
256 
slotChangePageFromReturn()257 void MiniBar::slotChangePageFromReturn()
258 {
259     // get text from the lineEdit
260     const QString pageNumber = m_pageNumberEdit->text();
261 
262     // convert it to page number and go to that page
263     bool ok;
264     int number = pageNumber.toInt(&ok) - 1;
265     if (ok && number >= 0 && number < (int)m_miniBarLogic->document()->pages() && number != m_miniBarLogic->currentPage()) {
266         slotChangePage(number);
267     }
268 }
269 
slotChangePage(int pageNumber)270 void MiniBar::slotChangePage(int pageNumber)
271 {
272     m_miniBarLogic->document()->setViewportPage(pageNumber);
273     m_pageNumberEdit->clearFocus();
274     m_pageLabelEdit->clearFocus();
275 }
276 
slotEmitNextPage()277 void MiniBar::slotEmitNextPage()
278 {
279     // emit signal
280     emit nextPage();
281 }
282 
slotEmitPrevPage()283 void MiniBar::slotEmitPrevPage()
284 {
285     // emit signal
286     emit prevPage();
287 }
288 
slotToolBarIconSizeChanged()289 void MiniBar::slotToolBarIconSizeChanged()
290 {
291     const QSize buttonSize = m_oldToolbarParent->iconSize();
292     m_prevButton->setIconSize(buttonSize);
293     m_nextButton->setIconSize(buttonSize);
294 }
295 
resizeForPage(int pages,const QString & pagesOrLabelString)296 void MiniBar::resizeForPage(int pages, const QString &pagesOrLabelString)
297 {
298     const int numberWidth = 10 + fontMetrics().horizontalAdvance(QString::number(pages));
299     const int labelWidth = 10 + fontMetrics().horizontalAdvance(pagesOrLabelString);
300     m_pageNumberEdit->setMinimumWidth(numberWidth);
301     m_pageNumberEdit->setMaximumWidth(2 * numberWidth);
302     m_pageLabelEdit->setMinimumWidth(labelWidth);
303     m_pageLabelEdit->setMaximumWidth(2 * labelWidth);
304     m_pageNumberLabel->setMinimumWidth(numberWidth);
305     m_pageNumberLabel->setMaximumWidth(2 * numberWidth);
306     m_pagesButton->setMinimumWidth(numberWidth);
307     m_pagesButton->setMaximumWidth(2 * numberWidth);
308 }
309 
310 /** ProgressWidget **/
311 
ProgressWidget(QWidget * parent,Okular::Document * document)312 ProgressWidget::ProgressWidget(QWidget *parent, Okular::Document *document)
313     : QWidget(parent)
314     , m_document(document)
315     , m_progressPercentage(-1)
316 {
317     setObjectName(QStringLiteral("progress"));
318     setAttribute(Qt::WA_OpaquePaintEvent, true);
319     setFixedHeight(4);
320     setMouseTracking(true);
321 }
322 
~ProgressWidget()323 ProgressWidget::~ProgressWidget()
324 {
325     m_document->removeObserver(this);
326 }
327 
notifyCurrentPageChanged(int previousPage,int currentPage)328 void ProgressWidget::notifyCurrentPageChanged(int previousPage, int currentPage)
329 {
330     Q_UNUSED(previousPage)
331 
332     // get current page number
333     int pages = m_document->pages();
334 
335     // if the document is opened and page is changed
336     if (pages > 0) {
337         // update percentage
338         const float percentage = pages < 2 ? 1.0 : (float)currentPage / (float)(pages - 1);
339         setProgress(percentage);
340     }
341 }
342 
setProgress(float percentage)343 void ProgressWidget::setProgress(float percentage)
344 {
345     m_progressPercentage = percentage;
346     update();
347 }
348 
slotGotoNormalizedPage(float index)349 void ProgressWidget::slotGotoNormalizedPage(float index)
350 {
351     // figure out page number and go to that page
352     int number = (int)(index * (float)m_document->pages());
353     if (number >= 0 && number < (int)m_document->pages() && number != (int)m_document->currentPage())
354         m_document->setViewportPage(number);
355 }
356 
mouseMoveEvent(QMouseEvent * e)357 void ProgressWidget::mouseMoveEvent(QMouseEvent *e)
358 {
359     if ((QApplication::mouseButtons() & Qt::LeftButton) && width() > 0)
360         slotGotoNormalizedPage((float)(QApplication::isRightToLeft() ? width() - e->x() : e->x()) / (float)width());
361 }
362 
mousePressEvent(QMouseEvent * e)363 void ProgressWidget::mousePressEvent(QMouseEvent *e)
364 {
365     if (e->button() == Qt::LeftButton && width() > 0)
366         slotGotoNormalizedPage((float)(QApplication::isRightToLeft() ? width() - e->x() : e->x()) / (float)width());
367 }
368 
wheelEvent(QWheelEvent * e)369 void ProgressWidget::wheelEvent(QWheelEvent *e)
370 {
371     if (e->angleDelta().y() > 0)
372         emit nextPage();
373     else
374         emit prevPage();
375 }
376 
paintEvent(QPaintEvent * e)377 void ProgressWidget::paintEvent(QPaintEvent *e)
378 {
379     QPainter p(this);
380 
381     if (m_progressPercentage < 0.0) {
382         p.fillRect(rect(), palette().color(QPalette::Active, QPalette::HighlightedText));
383         return;
384     }
385 
386     // find out the 'fill' and the 'clear' rectangles
387     int w = width(), h = height(), l = (int)((float)w * m_progressPercentage);
388     QRect cRect = (QApplication::isRightToLeft() ? QRect(0, 0, w - l, h) : QRect(l, 0, w - l, h)).intersected(e->rect());
389     QRect fRect = (QApplication::isRightToLeft() ? QRect(w - l, 0, l, h) : QRect(0, 0, l, h)).intersected(e->rect());
390 
391     QPalette pal = palette();
392     // paint clear rect
393     if (cRect.isValid())
394         p.fillRect(cRect, pal.color(QPalette::Active, QPalette::HighlightedText));
395     // draw a frame-like outline
396     // p.setPen( palette().active().mid() );
397     // p.drawRect( 0,0, w, h );
398     // paint fill rect
399     if (fRect.isValid())
400         p.fillRect(fRect, pal.color(QPalette::Active, QPalette::Highlight));
401     if (l && l != w) {
402         p.setPen(pal.color(QPalette::Active, QPalette::Highlight).darker(120));
403         int delta = QApplication::isRightToLeft() ? w - l : l;
404         p.drawLine(delta, 0, delta, h);
405     }
406 }
407 
408 /** PageLabelEdit **/
409 
PageLabelEdit(MiniBar * parent)410 PageLabelEdit::PageLabelEdit(MiniBar *parent)
411     : PagesEdit(parent)
412 {
413     setVisible(false);
414     connect(this, &PageLabelEdit::returnPressed, this, &PageLabelEdit::pageChosen);
415 }
416 
setText(const QString & newText)417 void PageLabelEdit::setText(const QString &newText)
418 {
419     m_lastLabel = newText;
420     PagesEdit::setText(newText);
421 }
422 
setPageLabels(const QVector<Okular::Page * > & pageVector)423 void PageLabelEdit::setPageLabels(const QVector<Okular::Page *> &pageVector)
424 {
425     m_labelPageMap.clear();
426     completionObject()->clear();
427     for (const Okular::Page *page : pageVector) {
428         if (!page->label().isEmpty()) {
429             m_labelPageMap.insert(page->label(), page->number());
430             bool ok;
431             page->label().toInt(&ok);
432             if (!ok) {
433                 // Only add to the completion objects labels that are not numbers
434                 completionObject()->addItem(page->label());
435             }
436         }
437     }
438 }
439 
pageChosen()440 void PageLabelEdit::pageChosen()
441 {
442     const QString newInput = text();
443     const int pageNumber = m_labelPageMap.value(newInput, -1);
444     if (pageNumber != -1) {
445         emit pageNumberChosen(pageNumber);
446     } else {
447         setText(m_lastLabel);
448     }
449 }
450 
451 /** PageNumberEdit **/
452 
PageNumberEdit(MiniBar * miniBar)453 PageNumberEdit::PageNumberEdit(MiniBar *miniBar)
454     : PagesEdit(miniBar)
455 {
456     // use an integer validator
457     m_validator = new QIntValidator(1, 1, this);
458     setValidator(m_validator);
459 }
460 
setPagesNumber(int pages)461 void PageNumberEdit::setPagesNumber(int pages)
462 {
463     m_validator->setTop(pages);
464 }
465 
466 /** PagesEdit **/
467 
PagesEdit(MiniBar * parent)468 PagesEdit::PagesEdit(MiniBar *parent)
469     : KLineEdit(parent)
470     , m_miniBar(parent)
471     , m_eatClick(false)
472 {
473     // customize text properties
474     setAlignment(Qt::AlignCenter);
475 
476     // send a focus out event
477     QFocusEvent fe(QEvent::FocusOut);
478     QApplication::sendEvent(this, &fe);
479 
480     connect(qApp, &QGuiApplication::paletteChanged, this, &PagesEdit::updatePalette);
481 }
482 
setText(const QString & newText)483 void PagesEdit::setText(const QString &newText)
484 {
485     // call default handler if hasn't focus
486     if (!hasFocus()) {
487         KLineEdit::setText(newText);
488     }
489     // else preserve existing selection
490     else {
491         // save selection and adapt it to the new text length
492         int selectionLength = selectedText().length();
493         const bool allSelected = (selectionLength == text().length());
494         if (allSelected) {
495             KLineEdit::setText(newText);
496             selectAll();
497         } else {
498             int newSelectionStart = newText.length() - text().length() + selectionStart();
499             if (newSelectionStart < 0) {
500                 // the new text is shorter than the old one, and the front part, which is "cut off", is selected
501                 // shorten the selection accordingly
502                 selectionLength += newSelectionStart;
503                 newSelectionStart = 0;
504             }
505             KLineEdit::setText(newText);
506             setSelection(newSelectionStart, selectionLength);
507         }
508     }
509 }
510 
updatePalette()511 void PagesEdit::updatePalette()
512 {
513     QPalette pal;
514 
515     if (hasFocus())
516         pal.setColor(QPalette::Active, QPalette::Base, QApplication::palette().color(QPalette::Active, QPalette::Base));
517     else
518         pal.setColor(QPalette::Base, QApplication::palette().color(QPalette::Base).darker(102));
519 
520     setPalette(pal);
521 }
522 
focusInEvent(QFocusEvent * e)523 void PagesEdit::focusInEvent(QFocusEvent *e)
524 {
525     // select all text
526     selectAll();
527     if (e->reason() == Qt::MouseFocusReason)
528         m_eatClick = true;
529     // change background color to the default 'edit' color
530     updatePalette();
531     // call default handler
532     KLineEdit::focusInEvent(e);
533 }
534 
focusOutEvent(QFocusEvent * e)535 void PagesEdit::focusOutEvent(QFocusEvent *e)
536 {
537     // change background color to a dark tone
538     updatePalette();
539     // call default handler
540     KLineEdit::focusOutEvent(e);
541 }
542 
mousePressEvent(QMouseEvent * e)543 void PagesEdit::mousePressEvent(QMouseEvent *e)
544 {
545     // if this click got the focus in, don't process the event
546     if (!m_eatClick)
547         KLineEdit::mousePressEvent(e);
548     m_eatClick = false;
549 }
550 
wheelEvent(QWheelEvent * e)551 void PagesEdit::wheelEvent(QWheelEvent *e)
552 {
553     if (e->angleDelta().y() > 0)
554         m_miniBar->slotEmitNextPage();
555     else
556         m_miniBar->slotEmitPrevPage();
557 }
558 
559 /** HoverButton **/
560 
HoverButton(QWidget * parent)561 HoverButton::HoverButton(QWidget *parent)
562     : QToolButton(parent)
563 {
564     setAutoRaise(true);
565     setFocusPolicy(Qt::NoFocus);
566     setToolButtonStyle(Qt::ToolButtonIconOnly);
567     KAcceleratorManager::setNoAccel(this);
568 }
569 
570 #include "minibar.moc"
571 
572 /* kate: replace-tabs on; indent-width 4; */
573