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