Okular

document.cpp
1/*
2 SPDX-FileCopyrightText: 2004-2005 Enrico Ros <eros.kde@email.it>
3 SPDX-FileCopyrightText: 2004-2008 Albert Astals Cid <aacid@kde.org>
4
5 Work sponsored by the LiMux project of the city of Munich:
6 SPDX-FileCopyrightText: 2017, 2018 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
7
8 SPDX-License-Identifier: GPL-2.0-or-later
9*/
10
11#include "document.h"
12#include "document_p.h"
13#include "documentcommands_p.h"
14
15#include <limits.h>
16#include <memory>
17#ifdef Q_OS_WIN
18#define _WIN32_WINNT 0x0500
19#include <windows.h>
20#elif defined(Q_OS_FREEBSD)
21// clang-format off
22// FreeBSD really wants this include order
23#include <sys/types.h>
24#include <sys/sysctl.h>
25// clang-format on
26#include <vm/vm_param.h>
27#endif
28
29// qt/kde/system includes
30#include <QApplication>
31#include <QDesktopServices>
32#include <QDir>
33#include <QFile>
34#include <QFileInfo>
35#include <QLabel>
36#include <QMap>
37#include <QMimeDatabase>
38#include <QPageSize>
39#include <QPrintDialog>
40#include <QRegularExpression>
41#include <QScreen>
42#include <QStack>
43#include <QStandardPaths>
44#include <QTemporaryFile>
45#include <QTextStream>
46#include <QTimer>
47#include <QUndoCommand>
48#include <QWindow>
49#include <QtAlgorithms>
50
51#include <KApplicationTrader>
52#include <KAuthorized>
53#include <KConfigDialog>
54#include <KFormat>
55#include <KIO/Global>
56#include <KIO/JobUiDelegate>
57#include <KIO/JobUiDelegateFactory>
58#include <KIO/OpenUrlJob>
59#include <KLocalizedString>
60#include <KMacroExpander>
61#include <KPluginMetaData>
62#include <KProcess>
63#include <KShell>
64#include <kio_version.h>
65#include <kzip.h>
66
67// local includes
68#include "action.h"
69#include "annotations.h"
70#include "annotations_p.h"
71#include "audioplayer.h"
72#include "bookmarkmanager.h"
73#include "chooseenginedialog_p.h"
74#include "debug_p.h"
75#include "form.h"
76#include "generator_p.h"
77#include "interfaces/configinterface.h"
78#include "interfaces/guiinterface.h"
79#include "interfaces/printinterface.h"
80#include "interfaces/saveinterface.h"
81#include "misc.h"
82#include "observer.h"
83#include "page.h"
84#include "page_p.h"
85#include "pagecontroller_p.h"
86#include "script/event_p.h"
87#include "scripter.h"
88#include "settings_core.h"
89#include "sourcereference.h"
90#include "sourcereference_p.h"
91#include "texteditors_p.h"
92#include "tile.h"
93#include "tilesmanager_p.h"
94#include "utils.h"
95#include "utils_p.h"
96#include "view.h"
97#include "view_p.h"
98
99#include <config-okular.h>
100
101#if HAVE_MALLOC_TRIM
102#include "malloc.h"
103#endif
104
105using namespace Okular;
106
107struct AllocatedPixmap {
108 // owner of the page
109 DocumentObserver *observer;
110 int page;
111 qulonglong memory;
112 // public constructor: initialize data
113 AllocatedPixmap(DocumentObserver *o, int p, qulonglong m)
114 : observer(o)
115 , page(p)
116 , memory(m)
117 {
118 }
119};
120
121struct ArchiveData {
122 ArchiveData()
123 {
124 }
125
126 QString originalFileName;
127 QTemporaryFile document;
128 QTemporaryFile metadataFile;
129};
130
131struct RunningSearch {
132 // store search properties
133 int continueOnPage;
134 RegularAreaRect continueOnMatch;
135 QSet<int> highlightedPages;
136
137 // fields related to previous searches (used for 'continueSearch')
138 QString cachedString;
139 Document::SearchType cachedType;
140 Qt::CaseSensitivity cachedCaseSensitivity;
141 bool cachedViewportMove : 1;
142 bool isCurrentlySearching : 1;
143 QColor cachedColor;
144 int pagesDone;
145};
146
147#define foreachObserver(cmd) \
148 { \
149 QSet<DocumentObserver *>::const_iterator it = d->m_observers.constBegin(), end = d->m_observers.constEnd(); \
150 for (; it != end; ++it) { \
151 (*it)->cmd; \
152 } \
153 }
154
155#define foreachObserverD(cmd) \
156 { \
157 QSet<DocumentObserver *>::const_iterator it = m_observers.constBegin(), end = m_observers.constEnd(); \
158 for (; it != end; ++it) { \
159 (*it)->cmd; \
160 } \
161 }
162
163#define OKULAR_HISTORY_MAXSTEPS 100
164#define OKULAR_HISTORY_SAVEDSTEPS 10
165
166// how often to run slotTimedMemoryCheck
167constexpr int kMemCheckTime = 2000; // in msec
168// getFreeMemory is called every two seconds when checking to see if the system is low on memory. If this timeout was left at kMemCheckTime, half of these checks are useless (when okular is idle) since the cache is used when the cache is
169// <=2 seconds old. This means that after the system is out of memory, up to 4 seconds (instead of 2) could go by before okular starts to free memory.
170constexpr int kFreeMemCacheTimeout = kMemCheckTime - 100;
171
172/***** Document ******/
173
174QString DocumentPrivate::pagesSizeString() const
175{
176 if (m_generator) {
177 if (m_generator->pagesSizeMetric() != Generator::None) {
178 QSizeF size = m_parent->allPagesSize();
179 // Single page size
180 if (size.isValid()) {
181 return localizedSize(size);
182 }
183
184 // Multiple page sizes
185 QString sizeString;
186 QHash<QString, int> pageSizeFrequencies;
187
188 // Compute frequencies of each page size
189 for (int i = 0; i < m_pagesVector.count(); ++i) {
190 const Page *p = m_pagesVector.at(i);
191 sizeString = localizedSize(QSizeF(p->width(), p->height()));
192 pageSizeFrequencies[sizeString] = pageSizeFrequencies.value(sizeString, 0) + 1;
193 }
194
195 // Figure out which page size is most frequent
196 int largestFrequencySeen = 0;
197 QString mostCommonPageSize = QString();
198 QHash<QString, int>::const_iterator i = pageSizeFrequencies.constBegin();
199 while (i != pageSizeFrequencies.constEnd()) {
200 if (i.value() > largestFrequencySeen) {
201 largestFrequencySeen = i.value();
202 mostCommonPageSize = i.key();
203 }
204 ++i;
205 }
206 QString finalText = i18nc("@info %1 is a page size", "Most pages are %1.", mostCommonPageSize);
207
208 return finalText;
209 } else {
210 return QString();
211 }
212 } else {
213 return QString();
214 }
215}
216
217QString DocumentPrivate::namePaperSize(double inchesWidth, double inchesHeight) const
218{
219 const QPageLayout::Orientation orientation = inchesWidth > inchesHeight ? QPageLayout::Landscape : QPageLayout::Portrait;
220
221 const QSize pointsSize(inchesWidth * 72.0, inchesHeight * 72.0);
223
224 const QString paperName = QPageSize::name(paperSize);
225
226 if (orientation == QPageLayout::Portrait) {
227 return i18nc("paper type and orientation (eg: Portrait A4)", "Portrait %1", paperName);
228 } else {
229 return i18nc("paper type and orientation (eg: Portrait A4)", "Landscape %1", paperName);
230 }
231}
232
233QString DocumentPrivate::localizedSize(const QSizeF size) const
234{
235 double inchesWidth = 0, inchesHeight = 0;
236 switch (m_generator->pagesSizeMetric()) {
238 inchesWidth = size.width() / 72.0;
239 inchesHeight = size.height() / 72.0;
240 break;
241
242 case Generator::Pixels: {
243 const QSizeF dpi = m_generator->dpi();
244 inchesWidth = size.width() / dpi.width();
245 inchesHeight = size.height() / dpi.height();
246 } break;
247
248 case Generator::None:
249 break;
250 }
251 if (QLocale::system().measurementSystem() == QLocale::ImperialSystem) {
252 return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 in (%3)", inchesWidth, inchesHeight, namePaperSize(inchesWidth, inchesHeight));
253 } else {
254 return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 mm (%3)", QString::number(inchesWidth * 25.4, 'd', 0), QString::number(inchesHeight * 25.4, 'd', 0), namePaperSize(inchesWidth, inchesHeight));
255 }
256}
257
258qulonglong DocumentPrivate::calculateMemoryToFree()
259{
260 // [MEM] choose memory parameters based on configuration profile
261 qulonglong clipValue = 0;
262 qulonglong memoryToFree = 0;
263
264 switch (SettingsCore::memoryLevel()) {
265 case SettingsCore::EnumMemoryLevel::Low:
266 memoryToFree = m_allocatedPixmapsTotalMemory;
267 break;
268
269 case SettingsCore::EnumMemoryLevel::Normal: {
270 qulonglong thirdTotalMemory = getTotalMemory() / 3;
271 qulonglong freeMemory = getFreeMemory();
272 if (m_allocatedPixmapsTotalMemory > thirdTotalMemory) {
273 memoryToFree = m_allocatedPixmapsTotalMemory - thirdTotalMemory;
274 }
275 if (m_allocatedPixmapsTotalMemory > freeMemory) {
276 clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2;
277 }
278 } break;
279
280 case SettingsCore::EnumMemoryLevel::Aggressive: {
281 qulonglong freeMemory = getFreeMemory();
282 if (m_allocatedPixmapsTotalMemory > freeMemory) {
283 clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2;
284 }
285 } break;
286 case SettingsCore::EnumMemoryLevel::Greedy: {
287 qulonglong freeSwap;
288 qulonglong freeMemory = getFreeMemory(&freeSwap);
289 const qulonglong memoryLimit = qMin(qMax(freeMemory, getTotalMemory() / 2), freeMemory + freeSwap);
290 if (m_allocatedPixmapsTotalMemory > memoryLimit) {
291 clipValue = (m_allocatedPixmapsTotalMemory - memoryLimit) / 2;
292 }
293 } break;
294 }
295
296 if (clipValue > memoryToFree) {
297 memoryToFree = clipValue;
298 }
299
300 return memoryToFree;
301}
302
303void DocumentPrivate::cleanupPixmapMemory()
304{
305 cleanupPixmapMemory(calculateMemoryToFree());
306}
307
308void DocumentPrivate::cleanupPixmapMemory(qulonglong memoryToFree)
309{
310 if (memoryToFree < 1) {
311 return;
312 }
313
314 const int currentViewportPage = (*m_viewportIterator).pageNumber;
315
316 // Create a QMap of visible rects, indexed by page number
317 QMap<int, VisiblePageRect *> visibleRects;
318 QVector<Okular::VisiblePageRect *>::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd();
319 for (; vIt != vEnd; ++vIt) {
320 visibleRects.insert((*vIt)->pageNumber, (*vIt));
321 }
322
323 // Free memory starting from pages that are farthest from the current one
324 int pagesFreed = 0;
325 while (memoryToFree > 0) {
326 AllocatedPixmap *p = searchLowestPriorityPixmap(true, true);
327 if (!p) { // No pixmap to remove
328 break;
329 }
330
331 qCDebug(OkularCoreDebug).nospace() << "Evicting cache pixmap observer=" << p->observer << " page=" << p->page;
332
333 // m_allocatedPixmapsTotalMemory can't underflow because we always add or remove
334 // the memory used by the AllocatedPixmap so at most it can reach zero
335 m_allocatedPixmapsTotalMemory -= p->memory;
336 // Make sure memoryToFree does not underflow
337 if (p->memory > memoryToFree) {
338 memoryToFree = 0;
339 } else {
340 memoryToFree -= p->memory;
341 }
342 pagesFreed++;
343 // delete pixmap
344 m_pagesVector.at(p->page)->deletePixmap(p->observer);
345 // delete allocation descriptor
346 delete p;
347 }
348
349 // If we're still on low memory, try to free individual tiles
350
351 // Store pages that weren't completely removed
352
353 std::list<AllocatedPixmap *> pixmapsToKeep;
354 while (memoryToFree > 0) {
355 int clean_hits = 0;
356 for (DocumentObserver *observer : std::as_const(m_observers)) {
357 AllocatedPixmap *p = searchLowestPriorityPixmap(false, true, observer);
358 if (!p) { // No pixmap to remove
359 continue;
360 }
361
362 clean_hits++;
363
364 TilesManager *tilesManager = m_pagesVector.at(p->page)->d->tilesManager(observer);
365 if (tilesManager && tilesManager->totalMemory() > 0) {
366 qulonglong memoryDiff = p->memory;
367 NormalizedRect visibleRect;
368 if (visibleRects.contains(p->page)) {
369 visibleRect = visibleRects[p->page]->rect;
370 }
371
372 // Free non visible tiles
373 tilesManager->cleanupPixmapMemory(memoryToFree, visibleRect, currentViewportPage);
374
375 p->memory = tilesManager->totalMemory();
376 memoryDiff -= p->memory;
377 memoryToFree = (memoryDiff < memoryToFree) ? (memoryToFree - memoryDiff) : 0;
378 m_allocatedPixmapsTotalMemory -= memoryDiff;
379
380 if (p->memory > 0) {
381 pixmapsToKeep.push_back(p);
382 } else {
383 delete p;
384 }
385 } else {
386 pixmapsToKeep.push_back(p);
387 }
388 }
389
390 if (clean_hits == 0) {
391 break;
392 }
393 }
394
395 m_allocatedPixmaps.splice(m_allocatedPixmaps.end(), pixmapsToKeep);
396 Q_UNUSED(pagesFreed);
397 // p--rintf("freeMemory A:[%d -%d = %d] \n", m_allocatedPixmaps.count() + pagesFreed, pagesFreed, m_allocatedPixmaps.count() );
398}
399
400/* Returns the next pixmap to evict from cache, or NULL if no suitable pixmap
401 * if found. If unloadableOnly is set, only unloadable pixmaps are returned. If
402 * thenRemoveIt is set, the pixmap is removed from m_allocatedPixmaps before
403 * returning it
404 */
405AllocatedPixmap *DocumentPrivate::searchLowestPriorityPixmap(bool unloadableOnly, bool thenRemoveIt, DocumentObserver *observer)
406{
407 std::list<AllocatedPixmap *>::iterator pIt = m_allocatedPixmaps.begin();
408 std::list<AllocatedPixmap *>::iterator pEnd = m_allocatedPixmaps.end();
409 std::list<AllocatedPixmap *>::iterator farthestPixmap = pEnd;
410 const int currentViewportPage = (*m_viewportIterator).pageNumber;
411
412 /* Find the pixmap that is farthest from the current viewport */
413 int maxDistance = -1;
414 while (pIt != pEnd) {
415 const AllocatedPixmap *p = *pIt;
416 // Filter by observer
417 if (observer == nullptr || p->observer == observer) {
418 const int distance = qAbs(p->page - currentViewportPage);
419 if (maxDistance < distance && (!unloadableOnly || p->observer->canUnloadPixmap(p->page))) {
420 maxDistance = distance;
421 farthestPixmap = pIt;
422 }
423 }
424 ++pIt;
425 }
426
427 /* No pixmap to remove */
428 if (farthestPixmap == pEnd) {
429 return nullptr;
430 }
431
432 AllocatedPixmap *selectedPixmap = *farthestPixmap;
433 if (thenRemoveIt) {
434 m_allocatedPixmaps.erase(farthestPixmap);
435 }
436 return selectedPixmap;
437}
438
439qulonglong DocumentPrivate::getTotalMemory()
440{
441 static qulonglong cachedValue = 0;
442 if (cachedValue) {
443 return cachedValue;
444 }
445
446#if defined(Q_OS_LINUX)
447 // if /proc/meminfo doesn't exist, return 128MB
448 QFile memFile(QStringLiteral("/proc/meminfo"));
449 if (!memFile.open(QIODevice::ReadOnly)) {
450 return (cachedValue = 134217728);
451 }
452
453 QTextStream readStream(&memFile);
454 while (true) {
455 QString entry = readStream.readLine();
456 if (entry.isNull()) {
457 break;
458 }
459 if (entry.startsWith(QLatin1String("MemTotal:"))) {
460 return (cachedValue = (Q_UINT64_C(1024) * entry.section(QLatin1Char(' '), -2, -2).toULongLong()));
461 }
462 }
463#elif defined(Q_OS_FREEBSD)
464 qulonglong physmem;
465 int mib[] = {CTL_HW, HW_PHYSMEM};
466 size_t len = sizeof(physmem);
467 if (sysctl(mib, 2, &physmem, &len, NULL, 0) == 0)
468 return (cachedValue = physmem);
469#elif defined(Q_OS_WIN)
470 MEMORYSTATUSEX stat;
471 stat.dwLength = sizeof(stat);
472 GlobalMemoryStatusEx(&stat);
473
474 return (cachedValue = stat.ullTotalPhys);
475#endif
476 return (cachedValue = 134217728);
477}
478
479qulonglong DocumentPrivate::getFreeMemory(qulonglong *freeSwap)
480{
481 static QDeadlineTimer cacheTimer(0);
482 static qulonglong cachedValue = 0;
483 static qulonglong cachedFreeSwap = 0;
484
485 if (!cacheTimer.hasExpired()) {
486 if (freeSwap) {
487 *freeSwap = cachedFreeSwap;
488 }
489 return cachedValue;
490 }
491
492 /* Initialize the returned free swap value to 0. It is overwritten if the
493 * actual value is available */
494 if (freeSwap) {
495 *freeSwap = 0;
496 }
497
498#if defined(Q_OS_LINUX)
499 // if /proc/meminfo doesn't exist, return MEMORY FULL
500 QFile memFile(QStringLiteral("/proc/meminfo"));
501 if (!memFile.open(QIODevice::ReadOnly)) {
502 return 0;
503 }
504
505 // read /proc/meminfo and sum up the contents of 'MemFree', 'Buffers'
506 // and 'Cached' fields. consider swapped memory as used memory.
507 qulonglong memoryFree = 0;
508 QString entry;
509 QTextStream readStream(&memFile);
510 static const int nElems = 5;
511 QString names[nElems] = {QStringLiteral("MemFree:"), QStringLiteral("Buffers:"), QStringLiteral("Cached:"), QStringLiteral("SwapFree:"), QStringLiteral("SwapTotal:")};
512 qulonglong values[nElems] = {0, 0, 0, 0, 0};
513 bool foundValues[nElems] = {false, false, false, false, false};
514 while (true) {
515 entry = readStream.readLine();
516 if (entry.isNull()) {
517 break;
518 }
519 for (int i = 0; i < nElems; ++i) {
520 if (entry.startsWith(names[i])) {
521 values[i] = entry.section(QLatin1Char(' '), -2, -2).toULongLong(&foundValues[i]);
522 }
523 }
524 }
525 memFile.close();
526 bool found = true;
527 for (int i = 0; found && i < nElems; ++i) {
528 found = found && foundValues[i];
529 }
530 if (found) {
531 /* MemFree + Buffers + Cached - SwapUsed =
532 * = MemFree + Buffers + Cached - (SwapTotal - SwapFree) =
533 * = MemFree + Buffers + Cached + SwapFree - SwapTotal */
534 memoryFree = values[0] + values[1] + values[2] + values[3];
535 if (values[4] > memoryFree) {
536 memoryFree = 0;
537 } else {
538 memoryFree -= values[4];
539 }
540 } else {
541 return 0;
542 }
543
544 cacheTimer.setRemainingTime(kFreeMemCacheTimeout);
545
546 if (freeSwap) {
547 *freeSwap = (cachedFreeSwap = (Q_UINT64_C(1024) * values[3]));
548 }
549 return (cachedValue = (Q_UINT64_C(1024) * memoryFree));
550#elif defined(Q_OS_FREEBSD)
551 qulonglong cache, inact, free, psize;
552 size_t cachelen, inactlen, freelen, psizelen;
553 cachelen = sizeof(cache);
554 inactlen = sizeof(inact);
555 freelen = sizeof(free);
556 psizelen = sizeof(psize);
557 // sum up inactive, cached and free memory
558 if (sysctlbyname("vm.stats.vm.v_cache_count", &cache, &cachelen, NULL, 0) == 0 && sysctlbyname("vm.stats.vm.v_inactive_count", &inact, &inactlen, NULL, 0) == 0 &&
559 sysctlbyname("vm.stats.vm.v_free_count", &free, &freelen, NULL, 0) == 0 && sysctlbyname("vm.stats.vm.v_page_size", &psize, &psizelen, NULL, 0) == 0) {
560 cacheTimer.setRemainingTime(kFreeMemCacheTimeout);
561 return (cachedValue = (cache + inact + free) * psize);
562 } else {
563 return 0;
564 }
565#elif defined(Q_OS_WIN)
566 MEMORYSTATUSEX stat;
567 stat.dwLength = sizeof(stat);
568 GlobalMemoryStatusEx(&stat);
569
570 cacheTimer.setRemainingTime(kFreeMemCacheTimeout);
571
572 if (freeSwap)
573 *freeSwap = (cachedFreeSwap = stat.ullAvailPageFile);
574 return (cachedValue = stat.ullAvailPhys);
575#else
576 // tell the memory is full.. will act as in LOW profile
577 return 0;
578#endif
579}
580
581bool DocumentPrivate::loadDocumentInfo(LoadDocumentInfoFlags loadWhat)
582// note: load data and stores it internally (document or pages). observers
583// are still uninitialized at this point so don't access them
584{
585 // qCDebug(OkularCoreDebug).nospace() << "Using '" << d->m_xmlFileName << "' as document info file.";
586 if (m_xmlFileName.isEmpty()) {
587 return false;
588 }
589
590 QFile infoFile(m_xmlFileName);
591 return loadDocumentInfo(infoFile, loadWhat);
592}
593
594bool DocumentPrivate::loadDocumentInfo(QFile &infoFile, LoadDocumentInfoFlags loadWhat)
595{
596 if (!infoFile.exists() || !infoFile.open(QIODevice::ReadOnly)) {
597 return false;
598 }
599
600 // Load DOM from XML file
601 QDomDocument doc(QStringLiteral("documentInfo"));
602 if (!doc.setContent(&infoFile)) {
603 qCDebug(OkularCoreDebug) << "Can't load XML pair! Check for broken xml.";
604 infoFile.close();
605 return false;
606 }
607 infoFile.close();
608
609 QDomElement root = doc.documentElement();
610
611 if (root.tagName() != QLatin1String("documentInfo")) {
612 return false;
613 }
614
615 bool loadedAnything = false; // set if something gets actually loaded
616
617 // Parse the DOM tree
618 QDomNode topLevelNode = root.firstChild();
619 while (topLevelNode.isElement()) {
620 QString catName = topLevelNode.toElement().tagName();
621
622 // Restore page attributes (bookmark, annotations, ...) from the DOM
623 if (catName == QLatin1String("pageList") && (loadWhat & LoadPageInfo)) {
624 QDomNode pageNode = topLevelNode.firstChild();
625 while (pageNode.isElement()) {
626 QDomElement pageElement = pageNode.toElement();
627 if (pageElement.hasAttribute(QStringLiteral("number"))) {
628 // get page number (node's attribute)
629 bool ok;
630 int pageNumber = pageElement.attribute(QStringLiteral("number")).toInt(&ok);
631
632 // pass the domElement to the right page, to read config data from
633 if (ok && pageNumber >= 0 && pageNumber < (int)m_pagesVector.count()) {
634 if (m_pagesVector[pageNumber]->d->restoreLocalContents(pageElement)) {
635 loadedAnything = true;
636 }
637 }
638 }
639 pageNode = pageNode.nextSibling();
640 }
641 }
642
643 // Restore 'general info' from the DOM
644 else if (catName == QLatin1String("generalInfo") && (loadWhat & LoadGeneralInfo)) {
645 QDomNode infoNode = topLevelNode.firstChild();
646 while (infoNode.isElement()) {
647 QDomElement infoElement = infoNode.toElement();
648
649 // restore viewports history
650 if (infoElement.tagName() == QLatin1String("history")) {
651 // clear history
652 m_viewportHistory.clear();
653 // append old viewports
654 QDomNode historyNode = infoNode.firstChild();
655 while (historyNode.isElement()) {
656 QDomElement historyElement = historyNode.toElement();
657 if (historyElement.hasAttribute(QStringLiteral("viewport"))) {
658 QString vpString = historyElement.attribute(QStringLiteral("viewport"));
659 m_viewportIterator = m_viewportHistory.insert(m_viewportHistory.end(), DocumentViewport(vpString));
660 loadedAnything = true;
661 }
662 historyNode = historyNode.nextSibling();
663 }
664 // consistency check
665 if (m_viewportHistory.empty()) {
666 m_viewportIterator = m_viewportHistory.insert(m_viewportHistory.end(), DocumentViewport());
667 }
668 } else if (infoElement.tagName() == QLatin1String("rotation")) {
669 QString str = infoElement.text();
670 bool ok = true;
671 int newrotation = !str.isEmpty() ? (str.toInt(&ok) % 4) : 0;
672 if (ok && newrotation != 0) {
673 setRotationInternal(newrotation, false);
674 loadedAnything = true;
675 }
676 } else if (infoElement.tagName() == QLatin1String("views")) {
677 QDomNode viewNode = infoNode.firstChild();
678 while (viewNode.isElement()) {
679 QDomElement viewElement = viewNode.toElement();
680 if (viewElement.tagName() == QLatin1String("view")) {
681 const QString viewName = viewElement.attribute(QStringLiteral("name"));
682 for (View *view : std::as_const(m_views)) {
683 if (view->name() == viewName) {
684 loadViewsInfo(view, viewElement);
685 loadedAnything = true;
686 break;
687 }
688 }
689 }
690 viewNode = viewNode.nextSibling();
691 }
692 }
693 infoNode = infoNode.nextSibling();
694 }
695 }
696
697 topLevelNode = topLevelNode.nextSibling();
698 } // </documentInfo>
699
700 return loadedAnything;
701}
702
703void DocumentPrivate::loadViewsInfo(View *view, const QDomElement &e)
704{
705 QDomNode viewNode = e.firstChild();
706 while (viewNode.isElement()) {
707 QDomElement viewElement = viewNode.toElement();
708
709 if (viewElement.tagName() == QLatin1String("zoom")) {
710 const QString valueString = viewElement.attribute(QStringLiteral("value"));
711 bool newzoom_ok = true;
712 const double newzoom = !valueString.isEmpty() ? valueString.toDouble(&newzoom_ok) : 1.0;
713 if (newzoom_ok && newzoom != 0 && view->supportsCapability(View::Zoom) && (view->capabilityFlags(View::Zoom) & (View::CapabilityRead | View::CapabilitySerializable))) {
714 view->setCapability(View::Zoom, newzoom);
715 }
716 const QString modeString = viewElement.attribute(QStringLiteral("mode"));
717 bool newmode_ok = true;
718 const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
719 if (newmode_ok && view->supportsCapability(View::ZoomModality) && (view->capabilityFlags(View::ZoomModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
720 view->setCapability(View::ZoomModality, newmode);
721 }
722 } else if (viewElement.tagName() == QLatin1String("viewMode")) {
723 const QString modeString = viewElement.attribute(QStringLiteral("mode"));
724 bool newmode_ok = true;
725 const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
726 if (newmode_ok && view->supportsCapability(View::ViewModeModality) && (view->capabilityFlags(View::ViewModeModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
727 view->setCapability(View::ViewModeModality, newmode);
728 }
729 } else if (viewElement.tagName() == QLatin1String("continuous")) {
730 const QString modeString = viewElement.attribute(QStringLiteral("mode"));
731 bool newmode_ok = true;
732 const int newmode = !modeString.isEmpty() ? modeString.toInt(&newmode_ok) : 2;
733 if (newmode_ok && view->supportsCapability(View::Continuous) && (view->capabilityFlags(View::Continuous) & (View::CapabilityRead | View::CapabilitySerializable))) {
734 view->setCapability(View::Continuous, newmode);
735 }
736 } else if (viewElement.tagName() == QLatin1String("trimMargins")) {
737 const QString valueString = viewElement.attribute(QStringLiteral("value"));
738 bool newmode_ok = true;
739 const int newmode = !valueString.isEmpty() ? valueString.toInt(&newmode_ok) : 2;
740 if (newmode_ok && view->supportsCapability(View::TrimMargins) && (view->capabilityFlags(View::TrimMargins) & (View::CapabilityRead | View::CapabilitySerializable))) {
741 view->setCapability(View::TrimMargins, newmode);
742 }
743 }
744
745 viewNode = viewNode.nextSibling();
746 }
747}
748
749void DocumentPrivate::saveViewsInfo(View *view, QDomElement &e) const
750{
751 if (view->supportsCapability(View::Zoom) && (view->capabilityFlags(View::Zoom) & (View::CapabilityRead | View::CapabilitySerializable)) && view->supportsCapability(View::ZoomModality) &&
752 (view->capabilityFlags(View::ZoomModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
753 QDomElement zoomEl = e.ownerDocument().createElement(QStringLiteral("zoom"));
754 e.appendChild(zoomEl);
755 bool ok = true;
756 const double zoom = view->capability(View::Zoom).toDouble(&ok);
757 if (ok && zoom != 0) {
758 zoomEl.setAttribute(QStringLiteral("value"), QString::number(zoom));
759 }
760 const int mode = view->capability(View::ZoomModality).toInt(&ok);
761 if (ok) {
762 zoomEl.setAttribute(QStringLiteral("mode"), mode);
763 }
764 }
765 if (view->supportsCapability(View::Continuous) && (view->capabilityFlags(View::Continuous) & (View::CapabilityRead | View::CapabilitySerializable))) {
766 QDomElement contEl = e.ownerDocument().createElement(QStringLiteral("continuous"));
767 e.appendChild(contEl);
768 const bool mode = view->capability(View::Continuous).toBool();
769 contEl.setAttribute(QStringLiteral("mode"), mode);
770 }
771 if (view->supportsCapability(View::ViewModeModality) && (view->capabilityFlags(View::ViewModeModality) & (View::CapabilityRead | View::CapabilitySerializable))) {
772 QDomElement viewEl = e.ownerDocument().createElement(QStringLiteral("viewMode"));
773 e.appendChild(viewEl);
774 bool ok = true;
775 const int mode = view->capability(View::ViewModeModality).toInt(&ok);
776 if (ok) {
777 viewEl.setAttribute(QStringLiteral("mode"), mode);
778 }
779 }
780 if (view->supportsCapability(View::TrimMargins) && (view->capabilityFlags(View::TrimMargins) & (View::CapabilityRead | View::CapabilitySerializable))) {
781 QDomElement contEl = e.ownerDocument().createElement(QStringLiteral("trimMargins"));
782 e.appendChild(contEl);
783 const bool value = view->capability(View::TrimMargins).toBool();
784 contEl.setAttribute(QStringLiteral("value"), value);
785 }
786}
787
788QUrl DocumentPrivate::giveAbsoluteUrl(const QString &fileName) const
789{
790 if (!QDir::isRelativePath(fileName)) {
791 return QUrl::fromLocalFile(fileName);
792 }
793
794 if (!m_url.isValid()) {
795 return QUrl();
796 }
797
798 return QUrl(KIO::upUrl(m_url).toString() + fileName);
799}
800
801bool DocumentPrivate::openRelativeFile(const QString &fileName)
802{
803 const QUrl newUrl = giveAbsoluteUrl(fileName);
804 if (newUrl.isEmpty()) {
805 return false;
806 }
807
808 qCDebug(OkularCoreDebug).nospace() << "openRelativeFile: '" << newUrl << "'";
809
810 Q_EMIT m_parent->openUrl(newUrl);
811 return m_url == newUrl;
812}
813
814Generator *DocumentPrivate::loadGeneratorLibrary(const KPluginMetaData &service)
815{
816 const auto result = KPluginFactory::instantiatePlugin<Okular::Generator>(service);
817
818 if (!result) {
819 qCWarning(OkularCoreDebug).nospace() << "Failed to load plugin " << service.fileName() << ": " << result.errorText;
820 return nullptr;
821 }
822
823 GeneratorInfo info(result.plugin, service);
824 m_loadedGenerators.insert(service.pluginId(), info);
825 return result.plugin;
826}
827
828void DocumentPrivate::loadAllGeneratorLibraries()
829{
830 if (m_generatorsLoaded) {
831 return;
832 }
833
834 loadServiceList(availableGenerators());
835
836 m_generatorsLoaded = true;
837}
838
839void DocumentPrivate::loadServiceList(const QVector<KPluginMetaData> &offers)
840{
841 int count = offers.count();
842 if (count <= 0) {
843 return;
844 }
845
846 for (int i = 0; i < count; ++i) {
847 QString id = offers.at(i).pluginId();
848 // don't load already loaded generators
849 QHash<QString, GeneratorInfo>::const_iterator genIt = m_loadedGenerators.constFind(id);
850 if (!m_loadedGenerators.isEmpty() && genIt != m_loadedGenerators.constEnd()) {
851 continue;
852 }
853
854 Generator *g = loadGeneratorLibrary(offers.at(i));
855 (void)g;
856 }
857}
858
859void DocumentPrivate::unloadGenerator(const GeneratorInfo &info)
860{
861 delete info.generator;
862}
863
864void DocumentPrivate::cacheExportFormats()
865{
866 if (m_exportCached) {
867 return;
868 }
869
870 const ExportFormat::List formats = m_generator->exportFormats();
871 for (int i = 0; i < formats.count(); ++i) {
872 if (formats.at(i).mimeType().name() == QLatin1String("text/plain")) {
873 m_exportToText = formats.at(i);
874 } else {
875 m_exportFormats.append(formats.at(i));
876 }
877 }
878
879 m_exportCached = true;
880}
881
882ConfigInterface *DocumentPrivate::generatorConfig(GeneratorInfo &info)
883{
884 if (info.configChecked) {
885 return info.config;
886 }
887
888 info.config = qobject_cast<Okular::ConfigInterface *>(info.generator);
889 info.configChecked = true;
890 return info.config;
891}
892
893SaveInterface *DocumentPrivate::generatorSave(GeneratorInfo &info)
894{
895 if (info.saveChecked) {
896 return info.save;
897 }
898
899 info.save = qobject_cast<Okular::SaveInterface *>(info.generator);
900 info.saveChecked = true;
901 return info.save;
902}
903
904Document::OpenResult DocumentPrivate::openDocumentInternal(const KPluginMetaData &offer, bool isstdin, const QString &docFile, const QByteArray &filedata, const QString &password)
905{
906 QString propName = offer.pluginId();
907 QHash<QString, GeneratorInfo>::const_iterator genIt = m_loadedGenerators.constFind(propName);
908 m_walletGenerator = nullptr;
909 if (genIt != m_loadedGenerators.constEnd()) {
910 m_generator = genIt.value().generator;
911 } else {
912 m_generator = loadGeneratorLibrary(offer);
913 if (!m_generator) {
914 return Document::OpenError;
915 }
916 genIt = m_loadedGenerators.constFind(propName);
917 Q_ASSERT(genIt != m_loadedGenerators.constEnd());
918 }
919 Q_ASSERT_X(m_generator, "Document::load()", "null generator?!");
920
921 m_generator->d_func()->m_document = this;
922
923 // connect error reporting signals
924 m_openError.clear();
925 QMetaObject::Connection errorToOpenErrorConnection = QObject::connect(m_generator, &Generator::error, m_parent, [this](const QString &message) { m_openError = message; });
926 QObject::connect(m_generator, &Generator::warning, m_parent, &Document::warning);
927 QObject::connect(m_generator, &Generator::notice, m_parent, &Document::notice);
928
930
931 const QWindow *window = m_widget && m_widget->window() ? m_widget->window()->windowHandle() : nullptr;
932 const QSizeF dpi = Utils::realDpi(window);
933 qCDebug(OkularCoreDebug) << "Output DPI:" << dpi;
934 m_generator->setDPI(dpi);
935
936 Document::OpenResult openResult = Document::OpenError;
937 if (!isstdin) {
938 openResult = m_generator->loadDocumentWithPassword(docFile, m_pagesVector, password);
939 } else if (!filedata.isEmpty()) {
940 if (m_generator->hasFeature(Generator::ReadRawData)) {
941 openResult = m_generator->loadDocumentFromDataWithPassword(filedata, m_pagesVector, password);
942 } else {
943 m_tempFile = new QTemporaryFile();
944 if (!m_tempFile->open()) {
945 delete m_tempFile;
946 m_tempFile = nullptr;
947 } else {
948 m_tempFile->write(filedata);
949 QString tmpFileName = m_tempFile->fileName();
950 m_tempFile->close();
951 openResult = m_generator->loadDocumentWithPassword(tmpFileName, m_pagesVector, password);
952 }
953 }
954 }
955
957 if (openResult != Document::OpenSuccess || m_pagesVector.size() <= 0) {
958 m_generator->d_func()->m_document = nullptr;
959 QObject::disconnect(m_generator, nullptr, m_parent, nullptr);
960
961 // TODO this is a bit of a hack, since basically means that
962 // you can only call walletDataForFile after calling openDocument
963 // but since in reality it's what happens I've decided not to refactor/break API
964 // One solution is just kill walletDataForFile and make OpenResult be an object
965 // where the wallet data is also returned when OpenNeedsPassword
966 m_walletGenerator = m_generator;
967 m_generator = nullptr;
968
969 qDeleteAll(m_pagesVector);
970 m_pagesVector.clear();
971 delete m_tempFile;
972 m_tempFile = nullptr;
973
974 // TODO: Q_EMIT a message telling the document is empty
975 if (openResult == Document::OpenSuccess) {
976 openResult = Document::OpenError;
977 }
978 } else {
979 /*
980 * Now that the documen is opened, the tab (if using tabs) is visible, which mean that
981 * we can now connect the error reporting signal directly to the parent
982 */
983
984 QObject::disconnect(errorToOpenErrorConnection);
985 QObject::connect(m_generator, &Generator::error, m_parent, &Document::error);
986 }
987
988 return openResult;
989}
990
991bool DocumentPrivate::savePageDocumentInfo(QTemporaryFile *infoFile, int what) const
992{
993 if (infoFile->open()) {
994 // 1. Create DOM
995 QDomDocument doc(QStringLiteral("documentInfo"));
996 QDomProcessingInstruction xmlPi = doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
997 doc.appendChild(xmlPi);
998 QDomElement root = doc.createElement(QStringLiteral("documentInfo"));
999 doc.appendChild(root);
1000
1001 // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM
1002 QDomElement pageList = doc.createElement(QStringLiteral("pageList"));
1003 root.appendChild(pageList);
1004 // <page list><page number='x'>.... </page> save pages that hold data
1005 QVector<Page *>::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
1006 for (; pIt != pEnd; ++pIt) {
1007 (*pIt)->d->saveLocalContents(pageList, doc, PageItems(what));
1008 }
1009
1010 // 3. Save DOM to XML file
1011 QString xml = doc.toString();
1012
1013 QTextStream os(infoFile);
1014 os.setEncoding(QStringConverter::Utf8);
1015 os << xml;
1016 return true;
1017 }
1018 return false;
1019}
1020
1021DocumentViewport DocumentPrivate::nextDocumentViewport() const
1022{
1023 DocumentViewport ret = m_nextDocumentViewport;
1024 if (!m_nextDocumentDestination.isEmpty() && m_generator) {
1025 DocumentViewport vp(m_parent->metaData(QStringLiteral("NamedViewport"), m_nextDocumentDestination).toString());
1026 if (vp.isValid()) {
1027 ret = vp;
1028 }
1029 }
1030 return ret;
1031}
1032
1033void DocumentPrivate::performAddPageAnnotation(int page, Annotation *annotation)
1034{
1035 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1036 AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1037
1038 // find out the page to attach annotation
1039 Page *kp = m_pagesVector[page];
1040 if (!m_generator || !kp) {
1041 return;
1042 }
1043
1044 // the annotation belongs already to a page
1045 if (annotation->d_ptr->m_page) {
1046 return;
1047 }
1048
1049 // add annotation to the page
1050 kp->addAnnotation(annotation);
1051
1052 // tell the annotation proxy
1053 if (proxy && proxy->supports(AnnotationProxy::Addition)) {
1054 proxy->notifyAddition(annotation, page);
1055 }
1056
1057 // notify observers about the change
1058 notifyAnnotationChanges(page);
1059
1060 if (annotation->flags() & Annotation::ExternallyDrawn) {
1061 // Redraw everything, including ExternallyDrawn annotations
1062 refreshPixmaps(page);
1063 }
1064}
1065
1066void DocumentPrivate::performRemovePageAnnotation(int page, Annotation *annotation)
1067{
1068 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1069 AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1070 bool isExternallyDrawn;
1071
1072 // find out the page
1073 Page *kp = m_pagesVector[page];
1074 if (!m_generator || !kp) {
1075 return;
1076 }
1077
1078 if (annotation->flags() & Annotation::ExternallyDrawn) {
1079 isExternallyDrawn = true;
1080 } else {
1081 isExternallyDrawn = false;
1082 }
1083
1084 // try to remove the annotation
1085 if (m_parent->canRemovePageAnnotation(annotation)) {
1086 // tell the annotation proxy
1087 if (proxy && proxy->supports(AnnotationProxy::Removal)) {
1088 proxy->notifyRemoval(annotation, page);
1089 }
1090
1091 kp->removeAnnotation(annotation); // Also destroys the object
1092
1093 // in case of success, notify observers about the change
1094 notifyAnnotationChanges(page);
1095
1096 if (isExternallyDrawn) {
1097 // Redraw everything, including ExternallyDrawn annotations
1098 refreshPixmaps(page);
1099 }
1100 }
1101}
1102
1103void DocumentPrivate::performModifyPageAnnotation(int page, Annotation *annotation, bool appearanceChanged)
1104{
1105 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
1106 AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr;
1107
1108 // find out the page
1109 Page *kp = m_pagesVector[page];
1110 if (!m_generator || !kp) {
1111 return;
1112 }
1113
1114 // tell the annotation proxy
1115 if (proxy && proxy->supports(AnnotationProxy::Modification)) {
1116 proxy->notifyModification(annotation, page, appearanceChanged);
1117 }
1118
1119 // notify observers about the change
1120 notifyAnnotationChanges(page);
1121 if (appearanceChanged && (annotation->flags() & Annotation::ExternallyDrawn)) {
1122 /* When an annotation is being moved, the generator will not render it.
1123 * Therefore there's no need to refresh pixmaps after the first time */
1124 if (annotation->flags() & (Annotation::BeingMoved | Annotation::BeingResized)) {
1125 if (m_annotationBeingModified) {
1126 return;
1127 } else { // First time: take note
1128 m_annotationBeingModified = true;
1129 }
1130 } else {
1131 m_annotationBeingModified = false;
1132 }
1133
1134 // Redraw everything, including ExternallyDrawn annotations
1135 qCDebug(OkularCoreDebug) << "Refreshing Pixmaps";
1136 refreshPixmaps(page);
1137 }
1138}
1139
1140void DocumentPrivate::performSetAnnotationContents(const QString &newContents, Annotation *annot, int pageNumber)
1141{
1142 bool appearanceChanged = false;
1143
1144 // Check if appearanceChanged should be true
1145 switch (annot->subType()) {
1146 // If it's an in-place TextAnnotation, set the inplace text
1148 Okular::TextAnnotation *txtann = static_cast<Okular::TextAnnotation *>(annot);
1149 if (txtann->textType() == Okular::TextAnnotation::InPlace) {
1150 appearanceChanged = true;
1151 }
1152 break;
1153 }
1154 // If it's a LineAnnotation, check if caption text is visible
1156 Okular::LineAnnotation *lineann = static_cast<Okular::LineAnnotation *>(annot);
1157 if (lineann->showCaption()) {
1158 appearanceChanged = true;
1159 }
1160 break;
1161 }
1162 default:
1163 break;
1164 }
1165
1166 // Set contents
1167 annot->setContents(newContents);
1168
1169 // Tell the document the annotation has been modified
1170 performModifyPageAnnotation(pageNumber, annot, appearanceChanged);
1171}
1172
1173void DocumentPrivate::recalculateForms()
1174{
1175 const QVariant fco = m_parent->metaData(QStringLiteral("FormCalculateOrder"));
1176 const QVector<int> formCalculateOrder = fco.value<QVector<int>>();
1177 for (int formId : formCalculateOrder) {
1178 for (uint pageIdx = 0; pageIdx < m_parent->pages(); pageIdx++) {
1179 const Page *p = m_parent->page(pageIdx);
1180 if (p) {
1181 bool pageNeedsRefresh = false;
1182 const QList<Okular::FormField *> forms = p->formFields();
1183 for (FormField *form : forms) {
1184 if (form->id() == formId) {
1185 Action *action = form->additionalAction(FormField::CalculateField);
1186 if (action) {
1187 FormFieldText *fft = dynamic_cast<FormFieldText *>(form);
1188 std::shared_ptr<Event> event;
1189 QString oldVal;
1190 if (fft) {
1191 // Prepare text calculate event
1192 event = Event::createFormCalculateEvent(fft, m_pagesVector[pageIdx]);
1193 if (!m_scripter) {
1194 m_scripter = new Scripter(this);
1195 }
1196 m_scripter->setEvent(event.get());
1197 // The value maybe changed in javascript so save it first.
1198 oldVal = fft->text();
1199 }
1200
1201 m_parent->processAction(action);
1202 if (event && fft) {
1203 // Update text field from calculate
1204 m_scripter->setEvent(nullptr);
1205 const QString newVal = event->value().toString();
1206 if (newVal != oldVal) {
1207 fft->setText(newVal);
1208 fft->setAppearanceText(newVal);
1210 // The format action handles the refresh.
1211 m_parent->processFormatAction(action, fft);
1212 } else {
1213 Q_EMIT m_parent->refreshFormWidget(fft);
1214 pageNeedsRefresh = true;
1215 }
1216 }
1217 }
1218 } else {
1219 qWarning() << "Form that is part of calculate order doesn't have a calculate action";
1220 }
1221 }
1222 }
1223 if (pageNeedsRefresh) {
1224 refreshPixmaps(p->number());
1225 }
1226 }
1227 }
1228 }
1229}
1230
1231void DocumentPrivate::saveDocumentInfo() const
1232{
1233 if (m_xmlFileName.isEmpty()) {
1234 return;
1235 }
1236
1237 QFile infoFile(m_xmlFileName);
1238 qCDebug(OkularCoreDebug) << "About to save document info to" << m_xmlFileName;
1240 qCWarning(OkularCoreDebug) << "Failed to open docdata file" << m_xmlFileName;
1241 return;
1242 }
1243 // 1. Create DOM
1244 QDomDocument doc(QStringLiteral("documentInfo"));
1245 QDomProcessingInstruction xmlPi = doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
1246 doc.appendChild(xmlPi);
1247 QDomElement root = doc.createElement(QStringLiteral("documentInfo"));
1248 root.setAttribute(QStringLiteral("url"), m_url.toDisplayString(QUrl::PreferLocalFile));
1249 doc.appendChild(root);
1250
1251 // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM
1252 // -> do this if there are not-yet-migrated annots or forms in docdata/
1253 if (m_docdataMigrationNeeded) {
1254 QDomElement pageList = doc.createElement(QStringLiteral("pageList"));
1255 root.appendChild(pageList);
1256 // OriginalAnnotationPageItems and OriginalFormFieldPageItems tell to
1257 // store the same unmodified annotation list and form contents that we
1258 // read when we opened the file and ignore any change made by the user.
1259 // Since we don't store annotations and forms in docdata/ any more, this is
1260 // necessary to preserve annotations/forms that previous Okular version
1261 // had stored there.
1262 const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems;
1263 // <page list><page number='x'>.... </page> save pages that hold data
1264 QVector<Page *>::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
1265 for (; pIt != pEnd; ++pIt) {
1266 (*pIt)->d->saveLocalContents(pageList, doc, saveWhat);
1267 }
1268 }
1269
1270 // 2.2. Save document info (current viewport, history, ... ) to DOM
1271 QDomElement generalInfo = doc.createElement(QStringLiteral("generalInfo"));
1272 root.appendChild(generalInfo);
1273 // create rotation node
1274 if (m_rotation != Rotation0) {
1275 QDomElement rotationNode = doc.createElement(QStringLiteral("rotation"));
1276 generalInfo.appendChild(rotationNode);
1277 rotationNode.appendChild(doc.createTextNode(QString::number((int)m_rotation)));
1278 }
1279 // <general info><history> ... </history> save history up to OKULAR_HISTORY_SAVEDSTEPS viewports
1280 const auto currentViewportIterator = std::list<DocumentViewport>::const_iterator(m_viewportIterator);
1281 std::list<DocumentViewport>::const_iterator backIterator = currentViewportIterator;
1282 if (backIterator != m_viewportHistory.end()) {
1283 // go back up to OKULAR_HISTORY_SAVEDSTEPS steps from the current viewportIterator
1284 int backSteps = OKULAR_HISTORY_SAVEDSTEPS;
1285 while (backSteps-- && backIterator != m_viewportHistory.begin()) {
1286 --backIterator;
1287 }
1288
1289 // create history root node
1290 QDomElement historyNode = doc.createElement(QStringLiteral("history"));
1291 generalInfo.appendChild(historyNode);
1292
1293 // add old[backIterator] and present[viewportIterator] items
1294 std::list<DocumentViewport>::const_iterator endIt = currentViewportIterator;
1295 ++endIt;
1296 while (backIterator != endIt) {
1297 QString name = (backIterator == currentViewportIterator) ? QStringLiteral("current") : QStringLiteral("oldPage");
1298 QDomElement historyEntry = doc.createElement(name);
1299 historyEntry.setAttribute(QStringLiteral("viewport"), (*backIterator).toString());
1300 historyNode.appendChild(historyEntry);
1301 ++backIterator;
1302 }
1303 }
1304 // create views root node
1305 QDomElement viewsNode = doc.createElement(QStringLiteral("views"));
1306 generalInfo.appendChild(viewsNode);
1307 for (View *view : std::as_const(m_views)) {
1308 QDomElement viewEntry = doc.createElement(QStringLiteral("view"));
1309 viewEntry.setAttribute(QStringLiteral("name"), view->name());
1310 viewsNode.appendChild(viewEntry);
1311 saveViewsInfo(view, viewEntry);
1312 }
1313
1314 // 3. Save DOM to XML file
1315 QString xml = doc.toString();
1316 QTextStream os(&infoFile);
1317 os.setEncoding(QStringConverter::Utf8);
1318 os << xml;
1319 infoFile.close();
1320}
1321
1322void DocumentPrivate::slotTimedMemoryCheck()
1323{
1324 // [MEM] clean memory (for 'free mem dependent' profiles only)
1325 if (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Low && m_allocatedPixmapsTotalMemory > 1024 * 1024) {
1326 cleanupPixmapMemory();
1327 }
1328}
1329
1330void DocumentPrivate::sendGeneratorPixmapRequest()
1331{
1332 /* If the pixmap cache will have to be cleaned in order to make room for the
1333 * next request, get the distance from the current viewport of the page
1334 * whose pixmap will be removed. We will ignore preload requests for pages
1335 * that are at the same distance or farther */
1336 const qulonglong memoryToFree = calculateMemoryToFree();
1337 const int currentViewportPage = (*m_viewportIterator).pageNumber;
1338 int maxDistance = INT_MAX; // Default: No maximum
1339 if (memoryToFree) {
1340 AllocatedPixmap *pixmapToReplace = searchLowestPriorityPixmap(true);
1341 if (pixmapToReplace) {
1342 maxDistance = qAbs(pixmapToReplace->page - currentViewportPage);
1343 }
1344 }
1345
1346 // find a request
1347 PixmapRequest *request = nullptr;
1348 m_pixmapRequestsMutex.lock();
1349 while (!m_pixmapRequestsStack.empty() && !request) {
1350 PixmapRequest *r = m_pixmapRequestsStack.back();
1351 if (!r) {
1352 m_pixmapRequestsStack.pop_back();
1353 continue;
1354 }
1355
1356 QRect requestRect = r->isTile() ? r->normalizedRect().geometry(r->width(), r->height()) : QRect(0, 0, r->width(), r->height());
1357 TilesManager *tilesManager = r->d->tilesManager();
1358 const double normalizedArea = r->normalizedRect().width() * r->normalizedRect().height();
1359 const QScreen *screen = nullptr;
1360 if (m_widget) {
1361 const QWindow *window = m_widget->window()->windowHandle();
1362 if (window) {
1363 screen = window->screen();
1364 }
1365 }
1366 if (!screen) {
1368 }
1369 const long screenSize = screen->devicePixelRatio() * screen->size().width() * screen->devicePixelRatio() * screen->size().height();
1370
1371 // Make sure the page is the right size to receive the pixmap
1372 r->page()->setPageSize(r->observer(), r->width(), r->height());
1373
1374 // If it's a preload but the generator is not threaded no point in trying to preload
1375 if (r->preload() && !m_generator->hasFeature(Generator::Threaded)) {
1376 m_pixmapRequestsStack.pop_back();
1377 delete r;
1378 }
1379 // request only if page isn't already present and request has valid id
1380 else if ((!r->d->mForce && r->page()->hasPixmap(r->observer(), r->width(), r->height(), r->normalizedRect())) || !m_observers.contains(r->observer())) {
1381 m_pixmapRequestsStack.pop_back();
1382 delete r;
1383 } else if (!r->d->mForce && r->preload() && qAbs(r->pageNumber() - currentViewportPage) >= maxDistance) {
1384 m_pixmapRequestsStack.pop_back();
1385 // qCDebug(OkularCoreDebug) << "Ignoring request that doesn't fit in cache";
1386 delete r;
1387 }
1388 // Ignore requests for pixmaps that are already being generated
1389 else if (tilesManager && tilesManager->isRequesting(r->normalizedRect(), r->width(), r->height())) {
1390 m_pixmapRequestsStack.pop_back();
1391 delete r;
1392 }
1393 // If the requested area is above 4*screenSize pixels, and we're not rendering most of the page, switch on the tile manager
1394 else if (!tilesManager && m_generator->hasFeature(Generator::TiledRendering) && (long)r->width() * (long)r->height() > 4L * screenSize && normalizedArea < 0.75) {
1395 // if the image is too big. start using tiles
1396 qCDebug(OkularCoreDebug).nospace() << "Start using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1397
1398 // fill the tiles manager with the last rendered pixmap
1399 const QPixmap *pixmap = r->page()->_o_nearestPixmap(r->observer(), r->width(), r->height());
1400 if (pixmap) {
1401 tilesManager = new TilesManager(r->pageNumber(), pixmap->width(), pixmap->height(), r->page()->rotation());
1402 tilesManager->setPixmap(pixmap, NormalizedRect(0, 0, 1, 1), true /*isPartialPixmap*/);
1403 tilesManager->setSize(r->width(), r->height());
1404 } else {
1405 // create new tiles manager
1406 tilesManager = new TilesManager(r->pageNumber(), r->width(), r->height(), r->page()->rotation());
1407 }
1408 tilesManager->setRequest(r->normalizedRect(), r->width(), r->height());
1409 r->page()->deletePixmap(r->observer());
1410 r->page()->d->setTilesManager(r->observer(), tilesManager);
1411 r->setTile(true);
1412
1413 // Change normalizedRect to the smallest rect that contains all
1414 // visible tiles.
1415 if (!r->normalizedRect().isNull()) {
1416 NormalizedRect tilesRect;
1417 const QList<Tile> tiles = tilesManager->tilesAt(r->normalizedRect(), TilesManager::TerminalTile);
1418 QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
1419 while (tIt != tEnd) {
1420 Tile tile = *tIt;
1421 if (tilesRect.isNull()) {
1422 tilesRect = tile.rect();
1423 } else {
1424 tilesRect |= tile.rect();
1425 }
1426
1427 ++tIt;
1428 }
1429
1430 r->setNormalizedRect(tilesRect);
1431 request = r;
1432 } else {
1433 // Discard request if normalizedRect is null. This happens in
1434 // preload requests issued by PageView if the requested page is
1435 // not visible and the user has just switched from a non-tiled
1436 // zoom level to a tiled one
1437 m_pixmapRequestsStack.pop_back();
1438 delete r;
1439 }
1440 }
1441 // If the requested area is below 3*screenSize pixels, switch off the tile manager
1442 else if (tilesManager && (long)r->width() * (long)r->height() < 3L * screenSize) {
1443 qCDebug(OkularCoreDebug).nospace() << "Stop using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1444
1445 // page is too small. stop using tiles.
1446 r->page()->deletePixmap(r->observer());
1447 r->setTile(false);
1448
1449 request = r;
1450 } else if ((long)requestRect.width() * (long)requestRect.height() > 100L * screenSize && (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Greedy)) {
1451 m_pixmapRequestsStack.pop_back();
1452 if (!m_warnedOutOfMemory) {
1453 qCWarning(OkularCoreDebug).nospace() << "Running out of memory on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);";
1454 qCWarning(OkularCoreDebug) << "this message will be reported only once.";
1455 m_warnedOutOfMemory = true;
1456 }
1457 delete r;
1458 } else {
1459 request = r;
1460 }
1461 }
1462
1463 // if no request found (or already generated), return
1464 if (!request) {
1465 m_pixmapRequestsMutex.unlock();
1466 return;
1467 }
1468
1469 // [MEM] preventive memory freeing
1470 qulonglong pixmapBytes = 0;
1471 TilesManager *tm = request->d->tilesManager();
1472 if (tm) {
1473 pixmapBytes = tm->totalMemory();
1474 } else {
1475 pixmapBytes = 4 * request->width() * request->height();
1476 }
1477
1478 if (pixmapBytes > (1024 * 1024)) {
1479 cleanupPixmapMemory(memoryToFree /* previously calculated value */);
1480 }
1481
1482 // submit the request to the generator
1483 if (m_generator->canGeneratePixmap()) {
1484 QRect requestRect = !request->isTile() ? QRect(0, 0, request->width(), request->height()) : request->normalizedRect().geometry(request->width(), request->height());
1485 qCDebug(OkularCoreDebug).nospace() << "sending request observer=" << request->observer() << " " << requestRect.width() << "x" << requestRect.height() << "@" << request->pageNumber() << " async == " << request->asynchronous()
1486 << " isTile == " << request->isTile();
1487 m_pixmapRequestsStack.remove(request);
1488
1489 if (tm) {
1490 tm->setRequest(request->normalizedRect(), request->width(), request->height());
1491 }
1492
1493 if ((int)m_rotation % 2) {
1494 request->d->swap();
1495 }
1496
1497 if (m_rotation != Rotation0 && !request->normalizedRect().isNull()) {
1498 request->setNormalizedRect(TilesManager::fromRotatedRect(request->normalizedRect(), m_rotation));
1499 }
1500
1501 // If set elsewhere we already know we want it to be partial
1502 if (!request->partialUpdatesWanted()) {
1503 request->setPartialUpdatesWanted(request->asynchronous() && !request->page()->hasPixmap(request->observer()));
1504 }
1505
1506 // we always have to unlock _before_ the generatePixmap() because
1507 // a sync generation would end with requestDone() -> deadlock, and
1508 // we can not really know if the generator can do async requests
1509 m_executingPixmapRequests.push_back(request);
1510 m_pixmapRequestsMutex.unlock();
1511 m_generator->generatePixmap(request);
1512 } else {
1513 m_pixmapRequestsMutex.unlock();
1514 // pino (7/4/2006): set the polling interval from 10 to 30
1515 QTimer::singleShot(30, m_parent, [this] { sendGeneratorPixmapRequest(); });
1516 }
1517}
1518
1519void DocumentPrivate::rotationFinished(int page, Okular::Page *okularPage)
1520{
1521 Okular::Page *wantedPage = m_pagesVector.value(page, nullptr);
1522 if (!wantedPage || wantedPage != okularPage) {
1523 return;
1524 }
1525
1526 for (DocumentObserver *o : std::as_const(m_observers)) {
1527 o->notifyPageChanged(page, DocumentObserver::Pixmap | DocumentObserver::Annotations);
1528 }
1529}
1530
1531void DocumentPrivate::slotFontReadingProgress(int page)
1532{
1533 Q_EMIT m_parent->fontReadingProgress(page);
1534
1535 if (page >= (int)m_parent->pages() - 1) {
1536 Q_EMIT m_parent->fontReadingEnded();
1537 m_fontThread = nullptr;
1538 m_fontsCached = true;
1539 }
1540}
1541
1542void DocumentPrivate::fontReadingGotFont(const Okular::FontInfo &font)
1543{
1544 // Try to avoid duplicate fonts
1545 if (m_fontsCache.indexOf(font) == -1) {
1546 m_fontsCache.append(font);
1547
1548 Q_EMIT m_parent->gotFont(font);
1549 }
1550}
1551
1552void DocumentPrivate::slotGeneratorConfigChanged()
1553{
1554 if (!m_generator) {
1555 return;
1556 }
1557
1558 // reparse generator config and if something changed clear Pages
1559 bool configchanged = false;
1560 QHash<QString, GeneratorInfo>::iterator it = m_loadedGenerators.begin(), itEnd = m_loadedGenerators.end();
1561 for (; it != itEnd; ++it) {
1562 Okular::ConfigInterface *iface = generatorConfig(it.value());
1563 if (iface) {
1564 bool it_changed = iface->reparseConfig();
1565 if (it_changed && (m_generator == it.value().generator)) {
1566 configchanged = true;
1567 }
1568 }
1569 }
1570 if (configchanged) {
1571 // invalidate pixmaps
1572 QVector<Page *>::const_iterator it = m_pagesVector.constBegin(), end = m_pagesVector.constEnd();
1573 for (; it != end; ++it) {
1574 (*it)->deletePixmaps();
1575 }
1576
1577 // [MEM] remove allocation descriptors
1578 qDeleteAll(m_allocatedPixmaps);
1579 m_allocatedPixmaps.clear();
1580 m_allocatedPixmapsTotalMemory = 0;
1581
1582 // send reload signals to observers
1583 foreachObserverD(notifyContentsCleared(DocumentObserver::Pixmap));
1584 }
1585
1586 // free memory if in 'low' profile
1587 if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !m_allocatedPixmaps.empty() && !m_pagesVector.isEmpty()) {
1588 cleanupPixmapMemory();
1589 }
1590}
1591
1592void DocumentPrivate::refreshPixmaps(int pageNumber)
1593{
1594 Page *page = m_pagesVector.value(pageNumber, nullptr);
1595 if (!page) {
1596 return;
1597 }
1598
1599 QMap<DocumentObserver *, PagePrivate::PixmapObject>::ConstIterator it = page->d->m_pixmaps.constBegin(), itEnd = page->d->m_pixmaps.constEnd();
1600 QVector<Okular::PixmapRequest *> pixmapsToRequest;
1601 for (; it != itEnd; ++it) {
1602 const QSize size = (*it).m_pixmap->size();
1603 PixmapRequest *p = new PixmapRequest(it.key(), pageNumber, size.width(), size.height(), 1 /* dpr */, 1, PixmapRequest::Asynchronous);
1604 p->d->mForce = true;
1605 pixmapsToRequest << p;
1606 }
1607
1608 // Need to do this ↑↓ in two steps since requestPixmaps can end up calling cancelRenderingBecauseOf
1609 // which changes m_pixmaps and thus breaks the loop above
1610 for (PixmapRequest *pr : std::as_const(pixmapsToRequest)) {
1611 QList<Okular::PixmapRequest *> requestedPixmaps;
1612 requestedPixmaps.push_back(pr);
1613 m_parent->requestPixmaps(requestedPixmaps, Okular::Document::NoOption);
1614 }
1615
1616 for (DocumentObserver *observer : std::as_const(m_observers)) {
1617 QList<Okular::PixmapRequest *> requestedPixmaps;
1618
1619 TilesManager *tilesManager = page->d->tilesManager(observer);
1620 if (tilesManager) {
1621 tilesManager->markDirty();
1622
1623 PixmapRequest *p = new PixmapRequest(observer, pageNumber, tilesManager->width(), tilesManager->height(), 1 /* dpr */, 1, PixmapRequest::Asynchronous);
1624
1625 // Get the visible page rect
1626 NormalizedRect visibleRect;
1627 QVector<Okular::VisiblePageRect *>::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd();
1628 for (; vIt != vEnd; ++vIt) {
1629 if ((*vIt)->pageNumber == pageNumber) {
1630 visibleRect = (*vIt)->rect;
1631 break;
1632 }
1633 }
1634
1635 if (!visibleRect.isNull()) {
1636 p->setNormalizedRect(visibleRect);
1637 p->setTile(true);
1638 p->d->mForce = true;
1639 requestedPixmaps.push_back(p);
1640 } else {
1641 delete p;
1642 }
1643 }
1644
1645 m_parent->requestPixmaps(requestedPixmaps, Okular::Document::NoOption);
1646 }
1647}
1648
1649void DocumentPrivate::_o_configChanged()
1650{
1651 // free text pages if needed
1652 calculateMaxTextPages();
1653 while (m_allocatedTextPagesFifo.count() > m_maxAllocatedTextPages) {
1654 int pageToKick = m_allocatedTextPagesFifo.takeFirst();
1655 m_pagesVector.at(pageToKick)->setTextPage(nullptr); // deletes the textpage
1656 }
1657}
1658
1659void DocumentPrivate::doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct)
1660{
1661 DoContinueDirectionMatchSearchStruct *searchStruct = static_cast<DoContinueDirectionMatchSearchStruct *>(doContinueDirectionMatchSearchStruct);
1662 RunningSearch *search = m_searches.value(searchStruct->searchID);
1663
1664 if ((m_searchCancelled && !searchStruct->match) || !search) {
1665 // if the user cancelled but he just got a match, give him the match!
1667
1668 if (search) {
1669 search->isCurrentlySearching = false;
1670 }
1671
1672 Q_EMIT m_parent->searchFinished(searchStruct->searchID, Document::SearchCancelled);
1673 delete searchStruct->pagesToNotify;
1674 delete searchStruct;
1675 return;
1676 }
1677
1678 const bool forward = search->cachedType == Document::NextMatch;
1679 bool doContinue = false;
1680 // if no match found, loop through the whole doc, starting from currentPage
1681 if (!searchStruct->match) {
1682 const int pageCount = m_pagesVector.count();
1683 if (search->pagesDone < pageCount) {
1684 doContinue = true;
1685 if (searchStruct->currentPage >= pageCount) {
1686 searchStruct->currentPage = 0;
1687 Q_EMIT m_parent->notice(i18n("Continuing search from beginning"), 3000);
1688 } else if (searchStruct->currentPage < 0) {
1689 searchStruct->currentPage = pageCount - 1;
1690 Q_EMIT m_parent->notice(i18n("Continuing search from bottom"), 3000);
1691 }
1692 }
1693 }
1694
1695 if (doContinue) {
1696 // get page
1697 Page *page = m_pagesVector[searchStruct->currentPage];
1698 // request search page if needed
1699 if (!page->hasTextPage()) {
1700 m_parent->requestTextPage(page->number());
1701 }
1702
1703 // if found a match on the current page, end the loop
1704 searchStruct->match = page->findText(searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity);
1705 if (!searchStruct->match) {
1706 if (forward) {
1707 searchStruct->currentPage++;
1708 } else {
1709 searchStruct->currentPage--;
1710 }
1711 search->pagesDone++;
1712 } else {
1713 search->pagesDone = 1;
1714 }
1715
1716 // Both of the previous if branches need to call doContinueDirectionMatchSearch
1717 QTimer::singleShot(0, m_parent, [this, searchStruct] { doContinueDirectionMatchSearch(searchStruct); });
1718 } else {
1719 doProcessSearchMatch(searchStruct->match, search, searchStruct->pagesToNotify, searchStruct->currentPage, searchStruct->searchID, search->cachedViewportMove, search->cachedColor);
1720 delete searchStruct;
1721 }
1722}
1723
1724void DocumentPrivate::doProcessSearchMatch(RegularAreaRect *match, RunningSearch *search, QSet<int> *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor &color)
1725{
1726 // reset cursor to previous shape
1728
1729 bool foundAMatch = false;
1730
1731 search->isCurrentlySearching = false;
1732
1733 // if a match has been found..
1734 if (match) {
1735 // update the RunningSearch structure adding this match..
1736 foundAMatch = true;
1737 search->continueOnPage = currentPage;
1738 search->continueOnMatch = *match;
1739 search->highlightedPages.insert(currentPage);
1740 // ..add highlight to the page..
1741 m_pagesVector[currentPage]->d->setHighlight(searchID, match, color);
1742
1743 // ..queue page for notifying changes..
1744 pagesToNotify->insert(currentPage);
1745
1746 // Create a normalized rectangle around the search match that includes a 5% buffer on all sides.
1747 const Okular::NormalizedRect matchRectWithBuffer = Okular::NormalizedRect(match->first().left - 0.05, match->first().top - 0.05, match->first().right + 0.05, match->first().bottom + 0.05);
1748
1749 const bool matchRectFullyVisible = isNormalizedRectangleFullyVisible(matchRectWithBuffer, currentPage);
1750
1751 // ..move the viewport to show the first of the searched word sequence centered
1752 if (moveViewport && !matchRectFullyVisible) {
1753 DocumentViewport searchViewport(currentPage);
1754 searchViewport.rePos.enabled = true;
1755 searchViewport.rePos.normalizedX = (match->first().left + match->first().right) / 2.0;
1756 searchViewport.rePos.normalizedY = (match->first().top + match->first().bottom) / 2.0;
1757 m_parent->setViewport(searchViewport, nullptr, true);
1758 }
1759 delete match;
1760 }
1761
1762 // notify observers about highlights changes
1763 for (int pageNumber : std::as_const(*pagesToNotify)) {
1764 for (DocumentObserver *observer : std::as_const(m_observers)) {
1765 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1766 }
1767 }
1768
1769 if (foundAMatch) {
1770 Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1771 } else {
1772 Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1773 }
1774
1775 delete pagesToNotify;
1776}
1777
1778void DocumentPrivate::doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID)
1779{
1780 QMap<Page *, QVector<RegularAreaRect *>> *pageMatches = static_cast<QMap<Page *, QVector<RegularAreaRect *>> *>(pageMatchesMap);
1781 QSet<int> *pagesToNotify = static_cast<QSet<int> *>(pagesToNotifySet);
1782 RunningSearch *search = m_searches.value(searchID);
1783
1784 if (m_searchCancelled || !search) {
1785 typedef QVector<RegularAreaRect *> MatchesVector;
1786
1788
1789 if (search) {
1790 search->isCurrentlySearching = false;
1791 }
1792
1793 Q_EMIT m_parent->searchFinished(searchID, Document::SearchCancelled);
1794 for (const MatchesVector &mv : std::as_const(*pageMatches)) {
1795 qDeleteAll(mv);
1796 }
1797 delete pageMatches;
1798 delete pagesToNotify;
1799 return;
1800 }
1801
1802 if (currentPage < m_pagesVector.count()) {
1803 // get page (from the first to the last)
1804 Page *page = m_pagesVector.at(currentPage);
1805 int pageNumber = page->number(); // redundant? is it == currentPage ?
1806
1807 // request search page if needed
1808 if (!page->hasTextPage()) {
1809 m_parent->requestTextPage(pageNumber);
1810 }
1811
1812 // loop on a page adding highlights for all found items
1813 RegularAreaRect *lastMatch = nullptr;
1814 while (true) {
1815 if (lastMatch) {
1816 lastMatch = page->findText(searchID, search->cachedString, NextResult, search->cachedCaseSensitivity, lastMatch);
1817 } else {
1818 lastMatch = page->findText(searchID, search->cachedString, FromTop, search->cachedCaseSensitivity);
1819 }
1820
1821 if (!lastMatch) {
1822 break;
1823 }
1824
1825 // add highlight rect to the matches map
1826 (*pageMatches)[page].append(lastMatch);
1827 }
1828 delete lastMatch;
1829
1830 QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID] { doContinueAllDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID); });
1831 } else {
1832 // reset cursor to previous shape
1834
1835 search->isCurrentlySearching = false;
1836 bool foundAMatch = pageMatches->count() != 0;
1837 QMap<Page *, QVector<RegularAreaRect *>>::const_iterator it, itEnd;
1838 it = pageMatches->constBegin();
1839 itEnd = pageMatches->constEnd();
1840 for (; it != itEnd; ++it) {
1841 for (RegularAreaRect *match : it.value()) {
1842 it.key()->d->setHighlight(searchID, match, search->cachedColor);
1843 delete match;
1844 }
1845 search->highlightedPages.insert(it.key()->number());
1846 pagesToNotify->insert(it.key()->number());
1847 }
1848
1849 for (DocumentObserver *observer : std::as_const(m_observers)) {
1850 observer->notifySetup(m_pagesVector, 0);
1851 }
1852
1853 // notify observers about highlights changes
1854 for (int pageNumber : std::as_const(*pagesToNotify)) {
1855 for (DocumentObserver *observer : std::as_const(m_observers)) {
1856 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1857 }
1858 }
1859
1860 if (foundAMatch) {
1861 Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1862 } else {
1863 Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1864 }
1865
1866 delete pageMatches;
1867 delete pagesToNotify;
1868 }
1869}
1870
1871void DocumentPrivate::doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList &words)
1872{
1873 typedef QPair<RegularAreaRect *, QColor> MatchColor;
1874 QMap<Page *, QVector<MatchColor>> *pageMatches = static_cast<QMap<Page *, QVector<MatchColor>> *>(pageMatchesMap);
1875 QSet<int> *pagesToNotify = static_cast<QSet<int> *>(pagesToNotifySet);
1876 RunningSearch *search = m_searches.value(searchID);
1877
1878 if (m_searchCancelled || !search) {
1879 typedef QVector<MatchColor> MatchesVector;
1880
1882
1883 if (search) {
1884 search->isCurrentlySearching = false;
1885 }
1886
1887 Q_EMIT m_parent->searchFinished(searchID, Document::SearchCancelled);
1888
1889 for (const MatchesVector &mv : std::as_const(*pageMatches)) {
1890 for (const MatchColor &mc : mv) {
1891 delete mc.first;
1892 }
1893 }
1894 delete pageMatches;
1895 delete pagesToNotify;
1896 return;
1897 }
1898
1899 const int wordCount = words.count();
1900 const int hueStep = (wordCount > 1) ? (60 / (wordCount - 1)) : 60;
1901 int baseHue, baseSat, baseVal;
1902 search->cachedColor.getHsv(&baseHue, &baseSat, &baseVal);
1903
1904 if (currentPage < m_pagesVector.count()) {
1905 // get page (from the first to the last)
1906 Page *page = m_pagesVector.at(currentPage);
1907 int pageNumber = page->number(); // redundant? is it == currentPage ?
1908
1909 // request search page if needed
1910 if (!page->hasTextPage()) {
1911 m_parent->requestTextPage(pageNumber);
1912 }
1913
1914 // loop on a page adding highlights for all found items
1915 bool allMatched = wordCount > 0, anyMatched = false;
1916 for (int w = 0; w < wordCount; w++) {
1917 const QString &word = words[w];
1918 int newHue = baseHue - w * hueStep;
1919 if (newHue < 0) {
1920 newHue += 360;
1921 }
1922 QColor wordColor = QColor::fromHsv(newHue, baseSat, baseVal);
1923 RegularAreaRect *lastMatch = nullptr;
1924 // add all highlights for current word
1925 bool wordMatched = false;
1926 while (true) {
1927 if (lastMatch) {
1928 lastMatch = page->findText(searchID, word, NextResult, search->cachedCaseSensitivity, lastMatch);
1929 } else {
1930 lastMatch = page->findText(searchID, word, FromTop, search->cachedCaseSensitivity);
1931 }
1932
1933 if (!lastMatch) {
1934 break;
1935 }
1936
1937 // add highligh rect to the matches map
1938 (*pageMatches)[page].append(MatchColor(lastMatch, wordColor));
1939 wordMatched = true;
1940 }
1941 allMatched = allMatched && wordMatched;
1942 anyMatched = anyMatched || wordMatched;
1943 }
1944
1945 // if not all words are present in page, remove partial highlights
1946 const bool matchAll = search->cachedType == Document::GoogleAll;
1947 if (!allMatched && matchAll) {
1948 const QVector<MatchColor> &matches = (*pageMatches)[page];
1949 for (const MatchColor &mc : matches) {
1950 delete mc.first;
1951 }
1952 pageMatches->remove(page);
1953 }
1954
1955 QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID, words] { doContinueGooglesDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID, words); });
1956 } else {
1957 // reset cursor to previous shape
1959
1960 search->isCurrentlySearching = false;
1961 bool foundAMatch = pageMatches->count() != 0;
1962 QMap<Page *, QVector<MatchColor>>::const_iterator it, itEnd;
1963 it = pageMatches->constBegin();
1964 itEnd = pageMatches->constEnd();
1965 for (; it != itEnd; ++it) {
1966 for (const MatchColor &mc : it.value()) {
1967 it.key()->d->setHighlight(searchID, mc.first, mc.second);
1968 delete mc.first;
1969 }
1970 search->highlightedPages.insert(it.key()->number());
1971 pagesToNotify->insert(it.key()->number());
1972 }
1973
1974 // send page lists to update observers (since some filter on bookmarks)
1975 for (DocumentObserver *observer : std::as_const(m_observers)) {
1976 observer->notifySetup(m_pagesVector, 0);
1977 }
1978
1979 // notify observers about highlights changes
1980 for (int pageNumber : std::as_const(*pagesToNotify)) {
1981 for (DocumentObserver *observer : std::as_const(m_observers)) {
1982 observer->notifyPageChanged(pageNumber, DocumentObserver::Highlights);
1983 }
1984 }
1985
1986 if (foundAMatch) {
1987 Q_EMIT m_parent->searchFinished(searchID, Document::MatchFound);
1988 } else {
1989 Q_EMIT m_parent->searchFinished(searchID, Document::NoMatchFound);
1990 }
1991
1992 delete pageMatches;
1993 delete pagesToNotify;
1994 }
1995}
1996
1997QVariant DocumentPrivate::documentMetaData(const Generator::DocumentMetaDataKey key, const QVariant &option) const
1998{
1999 switch (key) {
2001 bool giveDefault = option.toBool();
2002 QColor color;
2003 if ((SettingsCore::renderMode() == SettingsCore::EnumRenderMode::Paper) && SettingsCore::changeColors()) {
2004 color = SettingsCore::paperColor();
2005 } else if (giveDefault) {
2006 color = Qt::white;
2007 }
2008 return color;
2009 } break;
2010
2012 switch (SettingsCore::textAntialias()) {
2013 case SettingsCore::EnumTextAntialias::Enabled:
2014 return true;
2015 break;
2016 case SettingsCore::EnumTextAntialias::Disabled:
2017 return false;
2018 break;
2019 }
2020 break;
2021
2023 switch (SettingsCore::graphicsAntialias()) {
2024 case SettingsCore::EnumGraphicsAntialias::Enabled:
2025 return true;
2026 break;
2027 case SettingsCore::EnumGraphicsAntialias::Disabled:
2028 return false;
2029 break;
2030 }
2031 break;
2032
2034 switch (SettingsCore::textHinting()) {
2035 case SettingsCore::EnumTextHinting::Enabled:
2036 return true;
2037 break;
2038 case SettingsCore::EnumTextHinting::Disabled:
2039 return false;
2040 break;
2041 }
2042 break;
2043 }
2044 return QVariant();
2045}
2046
2047bool DocumentPrivate::isNormalizedRectangleFullyVisible(const Okular::NormalizedRect &rectOfInterest, int rectPage)
2048{
2049 bool rectFullyVisible = false;
2050 const QVector<Okular::VisiblePageRect *> &visibleRects = m_parent->visiblePageRects();
2053
2054 for (; (vIt != vEnd) && !rectFullyVisible; ++vIt) {
2055 if ((*vIt)->pageNumber == rectPage && (*vIt)->rect.contains(rectOfInterest.left, rectOfInterest.top) && (*vIt)->rect.contains(rectOfInterest.right, rectOfInterest.bottom)) {
2056 rectFullyVisible = true;
2057 }
2058 }
2059 return rectFullyVisible;
2060}
2061
2062struct pdfsyncpoint {
2063 QString file;
2064 qlonglong x;
2065 qlonglong y;
2066 int row;
2067 int column;
2068 int page;
2069};
2070
2071void DocumentPrivate::loadSyncFile(const QString &filePath)
2072{
2073 QFile f(filePath + QLatin1String("sync"));
2074 if (!f.open(QIODevice::ReadOnly)) {
2075 return;
2076 }
2077
2078 QTextStream ts(&f);
2079 // first row: core name of the pdf output
2080 const QString coreName = ts.readLine();
2081 // second row: version string, in the form 'Version %u'
2082 const QString versionstr = ts.readLine();
2083 // anchor the pattern with \A and \z to match the entire subject string
2084 // TODO: with Qt 5.12 QRegularExpression::anchoredPattern() can be used instead
2085 static QRegularExpression versionre(QStringLiteral("\\AVersion \\d+\\z"), QRegularExpression::CaseInsensitiveOption);
2086 QRegularExpressionMatch match = versionre.match(versionstr);
2087 if (!match.hasMatch()) {
2088 return;
2089 }
2090
2092 QStack<QString> fileStack;
2093 int currentpage = -1;
2094 const QLatin1String texStr(".tex");
2095 const QChar spaceChar = QChar::fromLatin1(' ');
2096
2097 fileStack.push(coreName + texStr);
2098
2099 const QSizeF dpi = m_generator->dpi();
2100
2101 QString line;
2102 while (!ts.atEnd()) {
2103 line = ts.readLine();
2104 const QStringList tokens = line.split(spaceChar, Qt::SkipEmptyParts);
2105 const int tokenSize = tokens.count();
2106 if (tokenSize < 1) {
2107 continue;
2108 }
2109 if (tokens.first() == QLatin1String("l") && tokenSize >= 3) {
2110 int id = tokens.at(1).toInt();
2112 if (it == points.constEnd()) {
2113 pdfsyncpoint pt;
2114 pt.x = 0;
2115 pt.y = 0;
2116 pt.row = tokens.at(2).toInt();
2117 pt.column = 0; // TODO
2118 pt.page = -1;
2119 pt.file = fileStack.top();
2120 points[id] = pt;
2121 }
2122 } else if (tokens.first() == QLatin1String("s") && tokenSize >= 2) {
2123 currentpage = tokens.at(1).toInt() - 1;
2124 } else if (tokens.first() == QLatin1String("p*") && tokenSize >= 4) {
2125 // TODO
2126 qCDebug(OkularCoreDebug) << "PdfSync: 'p*' line ignored";
2127 } else if (tokens.first() == QLatin1String("p") && tokenSize >= 4) {
2128 int id = tokens.at(1).toInt();
2130 if (it != points.end()) {
2131 it->x = tokens.at(2).toInt();
2132 it->y = tokens.at(3).toInt();
2133 it->page = currentpage;
2134 }
2135 } else if (line.startsWith(QLatin1Char('(')) && tokenSize == 1) {
2136 QString newfile = line;
2137 // chop the leading '('
2138 newfile.remove(0, 1);
2139 if (!newfile.endsWith(texStr)) {
2140 newfile += texStr;
2141 }
2142 fileStack.push(newfile);
2143 } else if (line == QLatin1String(")")) {
2144 if (!fileStack.isEmpty()) {
2145 fileStack.pop();
2146 } else {
2147 qCDebug(OkularCoreDebug) << "PdfSync: going one level down too much";
2148 }
2149 } else {
2150 qCDebug(OkularCoreDebug).nospace() << "PdfSync: unknown line format: '" << line << "'";
2151 }
2152 }
2153
2154 QVector<QList<Okular::SourceRefObjectRect *>> refRects(m_pagesVector.size());
2155 for (const pdfsyncpoint &pt : std::as_const(points)) {
2156 // drop pdfsync points not completely valid
2157 if (pt.page < 0 || pt.page >= m_pagesVector.size()) {
2158 continue;
2159 }
2160
2161 // magic numbers for TeX's RSU's (Ridiculously Small Units) conversion to pixels
2162 Okular::NormalizedPoint p((pt.x * dpi.width()) / (72.27 * 65536.0 * m_pagesVector[pt.page]->width()), (pt.y * dpi.height()) / (72.27 * 65536.0 * m_pagesVector[pt.page]->height()));
2163 QString file = pt.file;
2164 Okular::SourceReference *sourceRef = new Okular::SourceReference(file, pt.row, pt.column);
2165 refRects[pt.page].append(new Okular::SourceRefObjectRect(p, sourceRef));
2166 }
2167 for (int i = 0; i < refRects.size(); ++i) {
2168 if (!refRects.at(i).isEmpty()) {
2169 m_pagesVector[i]->setSourceReferences(refRects.at(i));
2170 }
2171 }
2172}
2173
2174void DocumentPrivate::clearAndWaitForRequests()
2175{
2176 m_pixmapRequestsMutex.lock();
2177 std::list<PixmapRequest *>::const_iterator sIt = m_pixmapRequestsStack.begin();
2178 std::list<PixmapRequest *>::const_iterator sEnd = m_pixmapRequestsStack.end();
2179 for (; sIt != sEnd; ++sIt) {
2180 delete *sIt;
2181 }
2182 m_pixmapRequestsStack.clear();
2183 m_pixmapRequestsMutex.unlock();
2184
2185 QEventLoop loop;
2186 bool startEventLoop = false;
2187 do {
2188 m_pixmapRequestsMutex.lock();
2189 startEventLoop = !m_executingPixmapRequests.empty();
2190
2191 if (m_generator->hasFeature(Generator::SupportsCancelling)) {
2192 for (PixmapRequest *executingRequest : std::as_const(m_executingPixmapRequests)) {
2193 executingRequest->d->mShouldAbortRender = 1;
2194 }
2195
2196 if (m_generator->d_ptr->mTextPageGenerationThread) {
2197 m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
2198 }
2199 }
2200
2201 m_pixmapRequestsMutex.unlock();
2202 if (startEventLoop) {
2203 m_closingLoop = &loop;
2204 loop.exec();
2205 m_closingLoop = nullptr;
2206 }
2207 } while (startEventLoop);
2208}
2209
2210int DocumentPrivate::findFieldPageNumber(Okular::FormField *field)
2211{
2212 // Lookup the page of the FormField
2213 int foundPage = -1;
2214 for (uint pageIdx = 0, nPages = m_parent->pages(); pageIdx < nPages; pageIdx++) {
2215 const Page *p = m_parent->page(pageIdx);
2216 if (p && p->formFields().contains(field)) {
2217 foundPage = static_cast<int>(pageIdx);
2218 break;
2219 }
2220 }
2221 return foundPage;
2222}
2223
2224void DocumentPrivate::executeScriptEvent(const std::shared_ptr<Event> &event, const Okular::ScriptAction *linkscript)
2225{
2226 if (!m_scripter) {
2227 m_scripter = new Scripter(this);
2228 }
2229 m_scripter->setEvent(event.get());
2230 m_scripter->execute(linkscript->scriptType(), linkscript->script());
2231
2232 // Clear out the event after execution
2233 m_scripter->setEvent(nullptr);
2234}
2235
2236Document::Document(QWidget *widget)
2237 : QObject(nullptr)
2238 , d(new DocumentPrivate(this))
2239{
2240 d->m_widget = widget;
2241 d->m_bookmarkManager = new BookmarkManager(d);
2242 d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), DocumentViewport());
2243 d->m_undoStack = new QUndoStack(this);
2244
2245 connect(SettingsCore::self(), &SettingsCore::configChanged, this, [this] { d->_o_configChanged(); });
2249
2250 qRegisterMetaType<Okular::FontInfo>();
2251}
2252
2254{
2255 // delete generator, pages, and related stuff
2256 closeDocument();
2257
2258 QSet<View *>::const_iterator viewIt = d->m_views.constBegin(), viewEnd = d->m_views.constEnd();
2259 for (; viewIt != viewEnd; ++viewIt) {
2260 View *v = *viewIt;
2261 v->d_func()->document = nullptr;
2262 }
2263
2264 // delete the bookmark manager
2265 delete d->m_bookmarkManager;
2266
2267 // delete the loaded generators
2268 QHash<QString, GeneratorInfo>::const_iterator it = d->m_loadedGenerators.constBegin(), itEnd = d->m_loadedGenerators.constEnd();
2269 for (; it != itEnd; ++it) {
2270 d->unloadGenerator(it.value());
2271 }
2272 d->m_loadedGenerators.clear();
2273
2274 // delete the private structure
2275 delete d;
2276}
2277
2278QString DocumentPrivate::docDataFileName(const QUrl &url, qint64 document_size)
2279{
2280 QString fn = url.fileName();
2281 fn = QString::number(document_size) + QLatin1Char('.') + fn + QStringLiteral(".xml");
2282 QString docdataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/okular/docdata");
2283 // make sure that the okular/docdata/ directory exists (probably this used to be handled by KStandardDirs)
2284 if (!QFileInfo::exists(docdataDir)) {
2285 qCDebug(OkularCoreDebug) << "creating docdata folder" << docdataDir;
2286 QDir().mkpath(docdataDir);
2287 }
2288 QString newokularfile = docdataDir + QLatin1Char('/') + fn;
2289
2290 return newokularfile;
2291}
2292
2293QVector<KPluginMetaData> DocumentPrivate::availableGenerators()
2294{
2295 static QVector<KPluginMetaData> result;
2296 if (result.isEmpty()) {
2297 result = KPluginMetaData::findPlugins(QStringLiteral("okular_generators"));
2298 }
2299 return result;
2300}
2301
2302KPluginMetaData DocumentPrivate::generatorForMimeType(const QMimeType &type, QWidget *widget, const QVector<KPluginMetaData> &triedOffers)
2303{
2304 // First try to find an exact match, and then look for more general ones (e. g. the plain text one)
2305 // Ideally we would rank these by "closeness", but that might be overdoing it
2306
2307 const QVector<KPluginMetaData> available = availableGenerators();
2309 QVector<KPluginMetaData> exactMatches;
2310
2311 QMimeDatabase mimeDatabase;
2312
2313 for (const KPluginMetaData &md : available) {
2314 if (triedOffers.contains(md)) {
2315 continue;
2316 }
2317
2318 const QStringList mimetypes = md.mimeTypes();
2319 for (const QString &supported : mimetypes) {
2320 QMimeType mimeType = mimeDatabase.mimeTypeForName(supported);
2321 if (mimeType == type && !exactMatches.contains(md)) {
2322 exactMatches << md;
2323 }
2324
2325 if (type.inherits(supported) && !offers.contains(md)) {
2326 offers << md;
2327 }
2328 }
2329 }
2330
2331 if (!exactMatches.isEmpty()) {
2332 offers = exactMatches;
2333 }
2334
2335 if (offers.isEmpty()) {
2336 return KPluginMetaData();
2337 }
2338 int hRank = 0;
2339 // best ranked offer search
2340 int offercount = offers.size();
2341 if (offercount > 1) {
2342 // sort the offers: the offers with an higher priority come before
2343 auto cmp = [](const KPluginMetaData &s1, const KPluginMetaData &s2) {
2344 const QString property = QStringLiteral("X-KDE-Priority");
2345 return s1.rawData().value(property).toInt() > s2.rawData().value(property).toInt();
2346 };
2347 std::stable_sort(offers.begin(), offers.end(), cmp);
2348
2349 if (SettingsCore::chooseGenerators()) {
2351 for (int i = 0; i < offercount; ++i) {
2352 list << offers.at(i).pluginId();
2353 }
2354 ChooseEngineDialog choose(list, type, widget);
2355
2356 if (choose.exec() == QDialog::Rejected) {
2357 return KPluginMetaData();
2358 }
2359
2360 hRank = choose.selectedGenerator();
2361 }
2362 }
2363 Q_ASSERT(hRank < offers.size());
2364 return offers.at(hRank);
2365}
2366
2367Document::OpenResult Document::openDocument(const QString &docFile, const QUrl &url, const QMimeType &_mime, const QString &password)
2368{
2369 QMimeDatabase db;
2370 QMimeType mime = _mime;
2371 QByteArray filedata;
2372 int fd = -1;
2373 if (url.scheme() == QLatin1String("fd")) {
2374 bool ok;
2375 fd = QStringView {url.path()}.mid(1).toInt(&ok);
2376 if (!ok) {
2377 return OpenError;
2378 }
2379 } else if (url.fileName() == QLatin1String("-")) {
2380 fd = 0;
2381 }
2382 bool triedMimeFromFileContent = false;
2383 if (fd < 0) {
2384 if (!mime.isValid()) {
2385 return OpenError;
2386 }
2387
2388 d->m_url = url;
2389 d->m_docFileName = docFile;
2390
2391 if (!d->updateMetadataXmlNameAndDocSize()) {
2392 return OpenError;
2393 }
2394 } else {
2395 QFile qstdin;
2396 const bool ret = qstdin.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle);
2397 if (!ret) {
2398 qWarning() << "failed to read" << url << filedata;
2399 return OpenError;
2400 }
2401
2402 filedata = qstdin.readAll();
2403 mime = db.mimeTypeForData(filedata);
2404 if (!mime.isValid() || mime.isDefault()) {
2405 return OpenError;
2406 }
2407 d->m_docSize = filedata.size();
2408 triedMimeFromFileContent = true;
2409 }
2410
2411 const bool fromFileDescriptor = fd >= 0;
2412
2413 // 0. load Generator
2414 // request only valid non-disabled plugins suitable for the mimetype
2415 KPluginMetaData offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2416 if (!offer.isValid() && !triedMimeFromFileContent) {
2418 triedMimeFromFileContent = true;
2419 if (newmime != mime) {
2420 mime = newmime;
2421 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2422 }
2423 if (!offer.isValid()) {
2424 // There's still no offers, do a final mime search based on the filename
2425 // We need this because sometimes (e.g. when downloading from a webserver) the mimetype we
2426 // use is the one fed by the server, that may be wrong
2427 newmime = db.mimeTypeForUrl(url);
2428
2429 if (!newmime.isDefault() && newmime != mime) {
2430 mime = newmime;
2431 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget);
2432 }
2433 }
2434 }
2435 if (!offer.isValid()) {
2436 d->m_openError = i18n("Can not find a plugin which is able to handle the document being passed.");
2437 Q_EMIT error(d->m_openError, -1);
2438 qCWarning(OkularCoreDebug).nospace() << "No plugin for mimetype '" << mime.name() << "'.";
2439 return OpenError;
2440 }
2441
2442 // 1. load Document
2443 OpenResult openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2444 if (openResult == OpenError) {
2445 QVector<KPluginMetaData> triedOffers;
2446 triedOffers << offer;
2447 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2448
2449 while (offer.isValid()) {
2450 openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2451
2452 if (openResult == OpenError) {
2453 triedOffers << offer;
2454 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2455 } else {
2456 break;
2457 }
2458 }
2459
2460 if (openResult == OpenError && !triedMimeFromFileContent) {
2462 triedMimeFromFileContent = true;
2463 if (newmime != mime) {
2464 mime = newmime;
2465 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2466 while (offer.isValid()) {
2467 openResult = d->openDocumentInternal(offer, fromFileDescriptor, docFile, filedata, password);
2468
2469 if (openResult == OpenError) {
2470 triedOffers << offer;
2471 offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers);
2472 } else {
2473 break;
2474 }
2475 }
2476 }
2477 }
2478
2479 if (openResult == OpenSuccess) {
2480 // Clear errors, since we're trying various generators, maybe one of them errored out
2481 // but we finally succeeded
2482 // TODO one can still see the error message animating out but since this is a very rare
2483 // condition we can leave this for future work
2484 Q_EMIT error(QString(), -1);
2485 }
2486 }
2487 if (openResult != OpenSuccess) {
2488 return openResult;
2489 }
2490
2491 // no need to check for the existence of a synctex file, no parser will be
2492 // created if none exists
2493 d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(docFile).constData(), nullptr, 1);
2494 if (!d->m_synctex_scanner && QFile::exists(docFile + QLatin1String("sync"))) {
2495 d->loadSyncFile(docFile);
2496 }
2497
2498 d->m_generatorName = offer.pluginId();
2499 d->m_pageController = new PageController();
2500 connect(d->m_pageController, &PageController::rotationFinished, this, [this](int p, Okular::Page *op) { d->rotationFinished(p, op); });
2501
2502 for (Page *p : std::as_const(d->m_pagesVector)) {
2503 p->d->m_doc = d;
2504 }
2505
2506 d->m_docdataMigrationNeeded = false;
2507
2508 // 2. load Additional Data (bookmarks, local annotations and metadata) about the document
2509 if (d->m_archiveData) {
2510 // QTemporaryFile is weird and will return false in exists if fileName wasn't called before
2511 d->m_archiveData->metadataFile.fileName();
2512 d->loadDocumentInfo(d->m_archiveData->metadataFile, LoadPageInfo);
2513 d->loadDocumentInfo(LoadGeneralInfo);
2514 } else {
2515 if (d->loadDocumentInfo(LoadPageInfo)) {
2516 d->m_docdataMigrationNeeded = true;
2517 }
2518 d->loadDocumentInfo(LoadGeneralInfo);
2519 }
2520
2521 d->m_bookmarkManager->setUrl(d->m_url);
2522
2523 // 3. setup observers internal lists and data
2524 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged));
2525
2526 // 4. set initial page (restoring the page saved in xml if loaded)
2527 DocumentViewport loadedViewport = (*d->m_viewportIterator);
2528 if (loadedViewport.isValid()) {
2529 (*d->m_viewportIterator) = DocumentViewport();
2530 if (loadedViewport.pageNumber >= (int)d->m_pagesVector.size()) {
2531 loadedViewport.pageNumber = d->m_pagesVector.size() - 1;
2532 }
2533 } else {
2534 loadedViewport.pageNumber = 0;
2535 }
2536 setViewport(loadedViewport);
2537
2538 // start bookmark saver timer
2539 if (!d->m_saveBookmarksTimer) {
2540 d->m_saveBookmarksTimer = new QTimer(this);
2541 connect(d->m_saveBookmarksTimer, &QTimer::timeout, this, [this] { d->saveDocumentInfo(); });
2542 }
2543 d->m_saveBookmarksTimer->start(5 * 60 * 1000);
2544
2545 // start memory check timer
2546 if (!d->m_memCheckTimer) {
2547 d->m_memCheckTimer = new QTimer(this);
2548 connect(d->m_memCheckTimer, &QTimer::timeout, this, [this] { d->slotTimedMemoryCheck(); });
2549 }
2550 d->m_memCheckTimer->start(kMemCheckTime);
2551
2552 const DocumentViewport nextViewport = d->nextDocumentViewport();
2553 if (nextViewport.isValid()) {
2554 setViewport(nextViewport);
2555 d->m_nextDocumentViewport = DocumentViewport();
2556 d->m_nextDocumentDestination = QString();
2557 }
2558
2559 AudioPlayer::instance()->setDocument(fromFileDescriptor ? QUrl() : d->m_url, this);
2560
2561 const QStringList docScripts = d->m_generator->metaData(QStringLiteral("DocumentScripts"), QStringLiteral("JavaScript")).toStringList();
2562 if (!docScripts.isEmpty()) {
2563 d->m_scripter = new Scripter(d);
2564 for (const QString &docscript : docScripts) {
2565 d->m_scripter->execute(JavaScript, docscript);
2566 }
2567 }
2568
2569 return OpenSuccess;
2570}
2571
2572bool DocumentPrivate::updateMetadataXmlNameAndDocSize()
2573{
2574 // m_docFileName is always local so we can use QFileInfo on it
2575 QFileInfo fileReadTest(m_docFileName);
2576 if (!fileReadTest.isFile() && !fileReadTest.isReadable()) {
2577 return false;
2578 }
2579
2580 m_docSize = fileReadTest.size();
2581
2582 // determine the related "xml document-info" filename
2583 if (m_url.isLocalFile()) {
2584 const QString filePath = docDataFileName(m_url, m_docSize);
2585 qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath;
2586 m_xmlFileName = filePath;
2587 } else {
2588 qCDebug(OkularCoreDebug) << "Metadata file: disabled";
2589 m_xmlFileName = QString();
2590 }
2591
2592 return true;
2593}
2594
2596{
2597 if (d->m_generator) {
2598 Okular::GuiInterface *iface = qobject_cast<Okular::GuiInterface *>(d->m_generator);
2599 if (iface) {
2600 return iface->guiClient();
2601 }
2602 }
2603 return nullptr;
2604}
2605
2607{
2608 // check if there's anything to close...
2609 if (!d->m_generator) {
2610 return;
2611 }
2612
2614
2615 delete d->m_pageController;
2616 d->m_pageController = nullptr;
2617
2618 delete d->m_scripter;
2619 d->m_scripter = nullptr;
2620
2621 // remove requests left in queue
2622 d->clearAndWaitForRequests();
2623
2624 if (d->m_fontThread) {
2625 disconnect(d->m_fontThread, nullptr, this, nullptr);
2626 d->m_fontThread->stopExtraction();
2627 d->m_fontThread->wait();
2628 d->m_fontThread = nullptr;
2629 }
2630
2631 // stop any audio playback
2633
2634 // close the current document and save document info if a document is still opened
2635 if (d->m_generator && d->m_pagesVector.size() > 0) {
2636 d->saveDocumentInfo();
2637
2638 // free the content of the opaque backend actions (if any)
2639 // this is a bit awkward since backends can store "random stuff" in the
2640 // BackendOpaqueAction nativeId qvariant so we need to tell them to free it
2641 // ideally we would just do that in the BackendOpaqueAction destructor
2642 // but that's too late in the cleanup process, i.e. the generator has already closed its document
2643 // and the document generator is nullptr
2644 for (Page *p : std::as_const(d->m_pagesVector)) {
2645 const QList<ObjectRect *> &oRects = p->objectRects();
2646 for (ObjectRect *oRect : oRects) {
2647 if (oRect->objectType() == ObjectRect::Action) {
2648 const Action *a = static_cast<const Action *>(oRect->object());
2649 const BackendOpaqueAction *backendAction = dynamic_cast<const BackendOpaqueAction *>(a);
2650 if (backendAction) {
2651 d->m_generator->freeOpaqueActionContents(*backendAction);
2652 }
2653 }
2654 }
2655
2656 const QList<FormField *> forms = p->formFields();
2657 for (const FormField *form : forms) {
2658 const QList<Action *> additionalActions = form->additionalActions();
2659 for (const Action *a : additionalActions) {
2660 const BackendOpaqueAction *backendAction = dynamic_cast<const BackendOpaqueAction *>(a);
2661 if (backendAction) {
2662 d->m_generator->freeOpaqueActionContents(*backendAction);
2663 }
2664 }
2665 }
2666 }
2667
2668 d->m_generator->closeDocument();
2669 }
2670
2671 if (d->m_synctex_scanner) {
2672 synctex_scanner_free(d->m_synctex_scanner);
2673 d->m_synctex_scanner = nullptr;
2674 }
2675
2676 // stop timers
2677 if (d->m_memCheckTimer) {
2678 d->m_memCheckTimer->stop();
2679 }
2680 if (d->m_saveBookmarksTimer) {
2681 d->m_saveBookmarksTimer->stop();
2682 }
2683
2684 if (d->m_generator) {
2685 // disconnect the generator from this document ...
2686 d->m_generator->d_func()->m_document = nullptr;
2687 // .. and this document from the generator signals
2688 disconnect(d->m_generator, nullptr, this, nullptr);
2689
2690 QHash<QString, GeneratorInfo>::const_iterator genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
2691 Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
2692 }
2693 d->m_generator = nullptr;
2694 d->m_generatorName = QString();
2695 d->m_url = QUrl();
2696 d->m_walletGenerator = nullptr;
2697 d->m_docFileName = QString();
2698 d->m_xmlFileName = QString();
2699 delete d->m_tempFile;
2700 d->m_tempFile = nullptr;
2701 delete d->m_archiveData;
2702 d->m_archiveData = nullptr;
2703 d->m_docSize = -1;
2704 d->m_exportCached = false;
2705 d->m_exportFormats.clear();
2706 d->m_exportToText = ExportFormat();
2707 d->m_fontsCached = false;
2708 d->m_fontsCache.clear();
2709 d->m_rotation = Rotation0;
2710
2711 // send an empty list to observers (to free their data)
2713
2714 // delete pages and clear 'd->m_pagesVector' container
2715 QVector<Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
2716 QVector<Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
2717 for (; pIt != pEnd; ++pIt) {
2718 delete *pIt;
2719 }
2720 d->m_pagesVector.clear();
2721
2722 // clear 'memory allocation' descriptors
2723 qDeleteAll(d->m_allocatedPixmaps);
2724 d->m_allocatedPixmaps.clear();
2725
2726 // clear 'running searches' descriptors
2727 QMap<int, RunningSearch *>::const_iterator rIt = d->m_searches.constBegin();
2728 QMap<int, RunningSearch *>::const_iterator rEnd = d->m_searches.constEnd();
2729 for (; rIt != rEnd; ++rIt) {
2730 delete *rIt;
2731 }
2732 d->m_searches.clear();
2733
2734 // clear the visible areas and notify the observers
2735 QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
2736 QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
2737 for (; vIt != vEnd; ++vIt) {
2738 delete *vIt;
2739 }
2740 d->m_pageRects.clear();
2741 foreachObserver(notifyVisibleRectsChanged());
2742
2743 // reset internal variables
2744
2745 d->m_viewportHistory.clear();
2746 d->m_viewportHistory.emplace_back();
2747 d->m_viewportIterator = d->m_viewportHistory.begin();
2748 d->m_allocatedPixmapsTotalMemory = 0;
2749 d->m_allocatedTextPagesFifo.clear();
2750 d->m_pageSize = PageSize();
2751 d->m_pageSizes.clear();
2752
2753 d->m_documentInfo = DocumentInfo();
2754 d->m_documentInfoAskedKeys.clear();
2755
2756 AudioPlayer::instance()->resetDocument();
2757
2758 d->m_undoStack->clear();
2759 d->m_docdataMigrationNeeded = false;
2760
2761#if HAVE_MALLOC_TRIM
2762 // trim unused memory, glibc should do this but it seems it does not
2763 // this can greatly decrease the [perceived] memory consumption of okular
2764 // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827
2765 malloc_trim(0);
2766#endif
2767}
2768
2770{
2771 Q_ASSERT(!d->m_observers.contains(pObserver));
2772 d->m_observers << pObserver;
2773
2774 // if the observer is added while a document is already opened, tell it
2775 if (!d->m_pagesVector.isEmpty()) {
2777 pObserver->notifyViewportChanged(false /*disables smoothMove*/);
2778 }
2779}
2780
2782{
2783 // remove observer from the set. it won't receive notifications anymore
2784 if (d->m_observers.contains(pObserver)) {
2785 // free observer's pixmap data
2786 QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2787 for (; it != end; ++it) {
2788 (*it)->deletePixmap(pObserver);
2789 }
2790
2791 // [MEM] free observer's allocation descriptors
2792 std::list<AllocatedPixmap *>::iterator aIt = d->m_allocatedPixmaps.begin();
2793 std::list<AllocatedPixmap *>::iterator aEnd = d->m_allocatedPixmaps.end();
2794 while (aIt != aEnd) {
2795 AllocatedPixmap *p = *aIt;
2796 if (p->observer == pObserver) {
2797 aIt = d->m_allocatedPixmaps.erase(aIt);
2798 delete p;
2799 } else {
2800 ++aIt;
2801 }
2802 }
2803
2804 for (PixmapRequest *executingRequest : std::as_const(d->m_executingPixmapRequests)) {
2805 if (executingRequest->observer() == pObserver) {
2806 d->cancelRenderingBecauseOf(executingRequest, nullptr);
2807 }
2808 }
2809
2810 // remove observer entry from the set
2811 d->m_observers.remove(pObserver);
2812 }
2813}
2814
2816{
2817 // reparse generator config and if something changed clear Pages
2818 bool configchanged = false;
2819 if (d->m_generator) {
2820 Okular::ConfigInterface *iface = qobject_cast<Okular::ConfigInterface *>(d->m_generator);
2821 if (iface) {
2822 configchanged = iface->reparseConfig();
2823 }
2824 }
2825 if (configchanged) {
2826 // invalidate pixmaps
2827 QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2828 for (; it != end; ++it) {
2829 (*it)->deletePixmaps();
2830 }
2831
2832 // [MEM] remove allocation descriptors
2833 qDeleteAll(d->m_allocatedPixmaps);
2834 d->m_allocatedPixmaps.clear();
2835 d->m_allocatedPixmapsTotalMemory = 0;
2836
2837 // send reload signals to observers
2838 foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap));
2839 }
2840
2841 // free memory if in 'low' profile
2842 if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !d->m_allocatedPixmaps.empty() && !d->m_pagesVector.isEmpty()) {
2843 d->cleanupPixmapMemory();
2844 }
2845}
2846
2848{
2849 return d->m_generator;
2850}
2851
2853{
2854 if (d->m_generator) {
2855 Okular::PrintInterface *iface = qobject_cast<Okular::PrintInterface *>(d->m_generator);
2856 return iface ? true : false;
2857 } else {
2858 return false;
2859 }
2860}
2861
2862bool Document::sign(const NewSignatureData &data, const QString &newPath)
2863{
2864 if (d->m_generator->canSign()) {
2865 return d->m_generator->sign(data, newPath);
2866 } else {
2867 return false;
2868 }
2869}
2870
2872{
2873 return d->m_generator ? d->m_generator->certificateStore() : nullptr;
2874}
2875
2877{
2878 d->editorCommandOverride = editCmd;
2879}
2880
2882{
2883 return d->editorCommandOverride;
2884}
2885
2887{
2890 keys << ks;
2891 }
2892
2893 return documentInfo(keys);
2894}
2895
2897{
2898 DocumentInfo result = d->m_documentInfo;
2899 const QSet<DocumentInfo::Key> missingKeys = keys - d->m_documentInfoAskedKeys;
2900
2901 if (d->m_generator && !missingKeys.isEmpty()) {
2902 DocumentInfo info = d->m_generator->generateDocumentInfo(missingKeys);
2903
2904 if (missingKeys.contains(DocumentInfo::FilePath)) {
2905 info.set(DocumentInfo::FilePath, currentDocument().toDisplayString());
2906 }
2907
2908 if (d->m_docSize != -1 && missingKeys.contains(DocumentInfo::DocumentSize)) {
2909 const QString sizeString = KFormat().formatByteSize(d->m_docSize);
2910 info.set(DocumentInfo::DocumentSize, sizeString);
2911 }
2912 if (missingKeys.contains(DocumentInfo::PagesSize)) {
2913 const QString pagesSize = d->pagesSizeString();
2914 if (!pagesSize.isEmpty()) {
2915 info.set(DocumentInfo::PagesSize, pagesSize);
2916 }
2917 }
2918
2919 if (missingKeys.contains(DocumentInfo::Pages) && info.get(DocumentInfo::Pages).isEmpty()) {
2921 }
2922
2923 d->m_documentInfo.d->values.insert(info.d->values);
2924 d->m_documentInfo.d->titles.insert(info.d->titles);
2925 result.d->values.insert(info.d->values);
2926 result.d->titles.insert(info.d->titles);
2927 }
2928 d->m_documentInfoAskedKeys += keys;
2929
2930 return result;
2931}
2932
2934{
2935 return d->m_generator ? d->m_generator->generateDocumentSynopsis() : nullptr;
2936}
2937
2939{
2940 if (!d->m_generator || !d->m_generator->hasFeature(Generator::FontInfo) || d->m_fontThread) {
2941 return;
2942 }
2943
2944 if (d->m_fontsCached) {
2945 // in case we have cached fonts, simulate a reading
2946 // this way the API is the same, and users no need to care about the
2947 // internal caching
2948 for (int i = 0; i < d->m_fontsCache.count(); ++i) {
2949 Q_EMIT gotFont(d->m_fontsCache.at(i));
2951 }
2953 return;
2954 }
2955
2956 d->m_fontThread = new FontExtractionThread(d->m_generator, pages());
2957 connect(d->m_fontThread, &FontExtractionThread::gotFont, this, [this](const Okular::FontInfo &f) { d->fontReadingGotFont(f); });
2958 connect(d->m_fontThread.data(), &FontExtractionThread::progress, this, [this](int p) { d->slotFontReadingProgress(p); });
2959
2960 d->m_fontThread->startExtraction(/*d->m_generator->hasFeature( Generator::Threaded )*/ true);
2961}
2962
2964{
2965 if (!d->m_fontThread) {
2966 return;
2967 }
2968
2969 disconnect(d->m_fontThread, nullptr, this, nullptr);
2970 d->m_fontThread->stopExtraction();
2971 d->m_fontThread = nullptr;
2972 d->m_fontsCache.clear();
2973}
2974
2976{
2977 return d->m_generator ? d->m_generator->hasFeature(Generator::FontInfo) : false;
2978}
2979
2981{
2982 return d->m_generator ? d->m_generator->canSign() : false;
2983}
2984
2986{
2987 return d->m_generator ? d->m_generator->embeddedFiles() : nullptr;
2988}
2989
2990const Page *Document::page(int n) const
2991{
2992 return (n >= 0 && n < d->m_pagesVector.count()) ? d->m_pagesVector.at(n) : nullptr;
2993}
2994
2996{
2997 return (*d->m_viewportIterator);
2998}
2999
3001{
3002 return d->m_pageRects;
3003}
3004
3006{
3007 QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
3008 QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
3009 for (; vIt != vEnd; ++vIt) {
3010 delete *vIt;
3011 }
3012 d->m_pageRects = visiblePageRects;
3013 // notify change to all other (different from id) observers
3014 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3015 if (o != excludeObserver) {
3016 o->notifyVisibleRectsChanged();
3017 }
3018 }
3019}
3020
3022{
3023 return (*d->m_viewportIterator).pageNumber;
3024}
3025
3027{
3028 return d->m_pagesVector.size();
3029}
3030
3032{
3033 return d->m_url;
3034}
3035
3037{
3038 if (action == Okular::AllowNotes && (d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled)) {
3039 return false;
3040 }
3041 if (action == Okular::AllowFillForms && d->m_docdataMigrationNeeded) {
3042 return false;
3043 }
3044
3045#if !OKULAR_FORCE_DRM
3046 if (KAuthorized::authorize(QStringLiteral("skip_drm")) && !SettingsCore::obeyDRM()) {
3047 return true;
3048 }
3049#endif
3050
3051 return d->m_generator ? d->m_generator->isAllowed(action) : false;
3052}
3053
3055{
3056 return d->m_generator ? d->m_generator->hasFeature(Generator::TextExtraction) : false;
3057}
3058
3060{
3061 return d->m_generator ? d->m_generator->hasFeature(Generator::PageSizes) : false;
3062}
3063
3065{
3066 return d->m_generator ? d->m_generator->hasFeature(Generator::TiledRendering) : false;
3067}
3068
3070{
3071 if (d->m_generator) {
3072 if (d->m_pageSizes.isEmpty()) {
3073 d->m_pageSizes = d->m_generator->pageSizes();
3074 }
3075 return d->m_pageSizes;
3076 }
3077 return PageSize::List();
3078}
3079
3081{
3082 if (!d->m_generator) {
3083 return false;
3084 }
3085
3086 d->cacheExportFormats();
3087 return !d->m_exportToText.isNull();
3088}
3089
3090bool Document::exportToText(const QString &fileName) const
3091{
3092 if (!d->m_generator) {
3093 return false;
3094 }
3095
3096 d->cacheExportFormats();
3097 if (d->m_exportToText.isNull()) {
3098 return false;
3099 }
3100
3101 return d->m_generator->exportTo(fileName, d->m_exportToText);
3102}
3103
3105{
3106 if (!d->m_generator) {
3107 return ExportFormat::List();
3108 }
3109
3110 d->cacheExportFormats();
3111 return d->m_exportFormats;
3112}
3113
3114bool Document::exportTo(const QString &fileName, const ExportFormat &format) const
3115{
3116 return d->m_generator ? d->m_generator->exportTo(fileName, format) : false;
3117}
3118
3120{
3121 return d->m_viewportIterator == d->m_viewportHistory.begin();
3122}
3123
3125{
3126 return d->m_viewportIterator == --(d->m_viewportHistory.end());
3127}
3128
3129QVariant Document::metaData(const QString &key, const QVariant &option) const
3130{
3131 // if option starts with "src:" assume that we are handling a
3132 // source reference
3133 if (key == QLatin1String("NamedViewport") && option.toString().startsWith(QLatin1String("src:"), Qt::CaseInsensitive) && d->m_synctex_scanner) {
3134 const QString reference = option.toString();
3135
3136 // The reference is of form "src:1111Filename", where "1111"
3137 // points to line number 1111 in the file "Filename".
3138 // Extract the file name and the numeral part from the reference string.
3139 // This will fail if Filename starts with a digit.
3140 QString name, lineString;
3141 // Remove "src:". Presence of substring has been checked before this
3142 // function is called.
3143 name = reference.mid(4);
3144 // split
3145 int nameLength = name.length();
3146 int i = 0;
3147 for (i = 0; i < nameLength; ++i) {
3148 if (!name[i].isDigit()) {
3149 break;
3150 }
3151 }
3152 lineString = name.left(i);
3153 name = name.mid(i);
3154 // Remove spaces.
3155 name = name.trimmed();
3156 lineString = lineString.trimmed();
3157 // Convert line to integer.
3158 bool ok;
3159 int line = lineString.toInt(&ok);
3160 if (!ok) {
3161 line = -1;
3162 }
3163
3164 // Use column == -1 for now.
3165 if (synctex_display_query(d->m_synctex_scanner, QFile::encodeName(name).constData(), line, -1, 0) > 0) {
3166 synctex_node_p node;
3167 // For now use the first hit. Could possibly be made smarter
3168 // in case there are multiple hits.
3169 while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
3171
3172 // TeX pages start at 1.
3173 viewport.pageNumber = synctex_node_page(node) - 1;
3174
3175 if (viewport.pageNumber >= 0) {
3176 const QSizeF dpi = d->m_generator->dpi();
3177
3178 // TeX small points ...
3179 double px = (synctex_node_visible_h(node) * dpi.width()) / 72.27;
3180 double py = (synctex_node_visible_v(node) * dpi.height()) / 72.27;
3181 viewport.rePos.normalizedX = px / page(viewport.pageNumber)->width();
3182 viewport.rePos.normalizedY = (py + 0.5) / page(viewport.pageNumber)->height();
3183 viewport.rePos.enabled = true;
3185
3186 return viewport.toString();
3187 }
3188 }
3189 }
3190 }
3191 return d->m_generator ? d->m_generator->metaData(key, option) : QVariant();
3192}
3193
3195{
3196 return d->m_rotation;
3197}
3198
3200{
3201 bool allPagesSameSize = true;
3202 QSizeF size;
3203 for (int i = 0; allPagesSameSize && i < d->m_pagesVector.count(); ++i) {
3204 const Page *p = d->m_pagesVector.at(i);
3205 if (i == 0) {
3206 size = QSizeF(p->width(), p->height());
3207 } else {
3208 allPagesSameSize = (size == QSizeF(p->width(), p->height()));
3209 }
3210 }
3211 if (allPagesSameSize) {
3212 return size;
3213 } else {
3214 return QSizeF();
3215 }
3216}
3217
3219{
3220 if (d->m_generator) {
3221 if (d->m_generator->pagesSizeMetric() != Generator::None) {
3222 const Page *p = d->m_pagesVector.at(page);
3223 return d->localizedSize(QSizeF(p->width(), p->height()));
3224 }
3225 }
3226 return QString();
3227}
3228
3229static bool shouldCancelRenderingBecauseOf(const PixmapRequest &executingRequest, const PixmapRequest &otherRequest)
3230{
3231 // New request has higher priority -> cancel
3232 if (executingRequest.priority() > otherRequest.priority()) {
3233 return true;
3234 }
3235
3236 // New request has lower priority -> don't cancel
3237 if (executingRequest.priority() < otherRequest.priority()) {
3238 return false;
3239 }
3240
3241 // New request has same priority and is from a different observer -> don't cancel
3242 // AFAIK this never happens since all observers have different priorities
3243 if (executingRequest.observer() != otherRequest.observer()) {
3244 return false;
3245 }
3246
3247 // Same priority and observer, different page number -> don't cancel
3248 // may still end up cancelled later in the parent caller if none of the requests
3249 // is of the executingRequest page and RemoveAllPrevious is specified
3250 if (executingRequest.pageNumber() != otherRequest.pageNumber()) {
3251 return false;
3252 }
3253
3254 // Same priority, observer, page, different size -> cancel
3255 if (executingRequest.width() != otherRequest.width()) {
3256 return true;
3257 }
3258
3259 // Same priority, observer, page, different size -> cancel
3260 if (executingRequest.height() != otherRequest.height()) {
3261 return true;
3262 }
3263
3264 // Same priority, observer, page, different tiling -> cancel
3265 if (executingRequest.isTile() != otherRequest.isTile()) {
3266 return true;
3267 }
3268
3269 // Same priority, observer, page, different tiling -> cancel
3270 if (executingRequest.isTile()) {
3271 const NormalizedRect bothRequestsRect = executingRequest.normalizedRect() | otherRequest.normalizedRect();
3272 if (!(bothRequestsRect == executingRequest.normalizedRect())) {
3273 return true;
3274 }
3275 }
3276
3277 return false;
3278}
3279
3280bool DocumentPrivate::cancelRenderingBecauseOf(PixmapRequest *executingRequest, PixmapRequest *newRequest)
3281{
3282 // No point in aborting the rendering already finished, let it go through
3283 if (!executingRequest->d->mResultImage.isNull()) {
3284 return false;
3285 }
3286
3287 if (newRequest && newRequest->asynchronous() && executingRequest->partialUpdatesWanted()) {
3288 newRequest->setPartialUpdatesWanted(true);
3289 }
3290
3291 TilesManager *tm = executingRequest->d->tilesManager();
3292 if (tm) {
3293 tm->setPixmap(nullptr, executingRequest->normalizedRect(), true /*isPartialPixmap*/);
3294 tm->setRequest(NormalizedRect(), 0, 0);
3295 }
3296 PagePrivate::PixmapObject object = executingRequest->page()->d->m_pixmaps.take(executingRequest->observer());
3297 delete object.m_pixmap;
3298
3299 if (executingRequest->d->mShouldAbortRender != 0) {
3300 return false;
3301 }
3302
3303 executingRequest->d->mShouldAbortRender = 1;
3304
3305 if (m_generator->d_ptr->mTextPageGenerationThread && m_generator->d_ptr->mTextPageGenerationThread->page() == executingRequest->page()) {
3306 m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
3307 }
3308
3309 return true;
3310}
3311
3313{
3315}
3316
3318{
3319 if (requests.isEmpty()) {
3320 return;
3321 }
3322
3323 if (!d->m_pageController) {
3324 // delete requests..
3325 qDeleteAll(requests);
3326 // ..and return
3327 return;
3328 }
3329
3330 QSet<DocumentObserver *> observersPixmapCleared;
3331
3332 // 1. [CLEAN STACK] remove previous requests of requesterID
3333 DocumentObserver *requesterObserver = requests.first()->observer();
3334 QSet<int> requestedPages;
3335 {
3336 for (PixmapRequest *request : requests) {
3337 Q_ASSERT(request->observer() == requesterObserver);
3338 requestedPages.insert(request->pageNumber());
3339 }
3340 }
3341 const bool removeAllPrevious = reqOptions & RemoveAllPrevious;
3342 d->m_pixmapRequestsMutex.lock();
3343 std::list<PixmapRequest *>::iterator sIt = d->m_pixmapRequestsStack.begin(), sEnd = d->m_pixmapRequestsStack.end();
3344 while (sIt != sEnd) {
3345 if ((*sIt)->observer() == requesterObserver && (removeAllPrevious || requestedPages.contains((*sIt)->pageNumber()))) {
3346 // delete request and remove it from stack
3347 delete *sIt;
3348 sIt = d->m_pixmapRequestsStack.erase(sIt);
3349 } else {
3350 ++sIt;
3351 }
3352 }
3353
3354 // 1.B [PREPROCESS REQUESTS] tweak some values of the requests
3355 for (PixmapRequest *request : requests) {
3356 // set the 'page field' (see PixmapRequest) and check if it is valid
3357 qCDebug(OkularCoreDebug).nospace() << "request observer=" << request->observer() << " " << request->width() << "x" << request->height() << "@" << request->pageNumber();
3358 if (d->m_pagesVector.value(request->pageNumber()) == nullptr) {
3359 // skip requests referencing an invalid page (must not happen)
3360 delete request;
3361 continue;
3362 }
3363
3364 request->d->mPage = d->m_pagesVector.value(request->pageNumber());
3365
3366 if (request->isTile()) {
3367 // Change the current request rect so that only invalid tiles are
3368 // requested. Also make sure the rect is tile-aligned.
3369 NormalizedRect tilesRect;
3370 const QList<Tile> tiles = request->d->tilesManager()->tilesAt(request->normalizedRect(), TilesManager::TerminalTile);
3371 QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
3372 while (tIt != tEnd) {
3373 const Tile &tile = *tIt;
3374 if (!tile.isValid()) {
3375 if (tilesRect.isNull()) {
3376 tilesRect = tile.rect();
3377 } else {
3378 tilesRect |= tile.rect();
3379 }
3380 }
3381
3382 tIt++;
3383 }
3384
3385 request->setNormalizedRect(tilesRect);
3386 }
3387
3388 if (!request->asynchronous()) {
3389 request->d->mPriority = 0;
3390 }
3391 }
3392
3393 // 1.C [CANCEL REQUESTS] cancel those requests that are running and should be cancelled because of the new requests coming in
3394 if (d->m_generator->hasFeature(Generator::SupportsCancelling)) {
3395 for (PixmapRequest *executingRequest : std::as_const(d->m_executingPixmapRequests)) {
3396 bool newRequestsContainExecutingRequestPage = false;
3397 bool requestCancelled = false;
3398 for (PixmapRequest *newRequest : requests) {
3399 if (newRequest->pageNumber() == executingRequest->pageNumber() && requesterObserver == executingRequest->observer()) {
3400 newRequestsContainExecutingRequestPage = true;
3401 }
3402
3403 if (shouldCancelRenderingBecauseOf(*executingRequest, *newRequest)) {
3404 requestCancelled = d->cancelRenderingBecauseOf(executingRequest, newRequest);
3405 }
3406 }
3407
3408 // If we were told to remove all the previous requests and the executing request page is not part of the new requests, cancel it
3409 if (!requestCancelled && removeAllPrevious && requesterObserver == executingRequest->observer() && !newRequestsContainExecutingRequestPage) {
3410 requestCancelled = d->cancelRenderingBecauseOf(executingRequest, nullptr);
3411 }
3412
3413 if (requestCancelled) {
3414 observersPixmapCleared << executingRequest->observer();
3415 }
3416 }
3417 }
3418
3419 // 2. [ADD TO STACK] add requests to stack
3420 for (PixmapRequest *request : requests) {
3421 // add request to the 'stack' at the right place
3422 if (!request->priority()) {
3423 // add priority zero requests to the top of the stack
3424 d->m_pixmapRequestsStack.push_back(request);
3425 } else {
3426 // insert in stack sorted by priority
3427 sIt = d->m_pixmapRequestsStack.begin();
3428 sEnd = d->m_pixmapRequestsStack.end();
3429 while (sIt != sEnd && (*sIt)->priority() > request->priority()) {
3430 ++sIt;
3431 }
3432 d->m_pixmapRequestsStack.insert(sIt, request);
3433 }
3434 }
3435 d->m_pixmapRequestsMutex.unlock();
3436
3437 // 3. [START FIRST GENERATION] if <NO>generator is ready, start a new generation,
3438 // or else (if gen is running) it will be started when the new contents will
3439 // come from generator (in requestDone())</NO>
3440 // all handling of requests put into sendGeneratorPixmapRequest
3441 // if ( generator->canRequestPixmap() )
3442 d->sendGeneratorPixmapRequest();
3443
3444 for (DocumentObserver *o : std::as_const(observersPixmapCleared)) {
3445 o->notifyContentsCleared(Okular::DocumentObserver::Pixmap);
3446 }
3447}
3448
3449void Document::requestTextPage(uint pageNumber)
3450{
3451 Page *kp = d->m_pagesVector[pageNumber];
3452 if (!d->m_generator || !kp) {
3453 return;
3454 }
3455
3456 // Memory management for TextPages
3457
3458 d->m_generator->generateTextPage(kp);
3459}
3460
3461void DocumentPrivate::notifyAnnotationChanges(int page)
3462{
3463 foreachObserverD(notifyPageChanged(page, DocumentObserver::Annotations));
3464}
3465
3466void DocumentPrivate::notifyFormChanges(int /*page*/)
3467{
3468 recalculateForms();
3469}
3470
3471void Document::addPageAnnotation(int page, Annotation *annotation)
3472{
3473 // Transform annotation's base boundary rectangle into unrotated coordinates
3474 Page *p = d->m_pagesVector[page];
3475 QTransform t = p->d->rotationMatrix();
3476 annotation->d_ptr->baseTransform(t.inverted());
3477 QUndoCommand *uc = new AddAnnotationCommand(this->d, annotation, page);
3478 d->m_undoStack->push(uc);
3479}
3480
3482{
3483 if (!annotation || (annotation->flags() & Annotation::DenyWrite)) {
3484 return false;
3485 }
3486
3488 return false;
3489 }
3490
3491 if ((annotation->flags() & Annotation::External) && !d->canModifyExternalAnnotations()) {
3492 return false;
3493 }
3494
3495 switch (annotation->subType()) {
3496 case Annotation::AText:
3497 case Annotation::ALine:
3498 case Annotation::AGeom:
3500 case Annotation::AStamp:
3501 case Annotation::AInk:
3502 return true;
3503 default:
3504 return false;
3505 }
3506}
3507
3509{
3510 Q_ASSERT(d->m_prevPropsOfAnnotBeingModified.isNull());
3511 if (!d->m_prevPropsOfAnnotBeingModified.isNull()) {
3512 qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties has already been called since last call to Document::modifyPageAnnotationProperties";
3513 return;
3514 }
3515 d->m_prevPropsOfAnnotBeingModified = annotation->getAnnotationPropertiesDomNode();
3516}
3517
3519{
3520 Q_ASSERT(!d->m_prevPropsOfAnnotBeingModified.isNull());
3521 if (d->m_prevPropsOfAnnotBeingModified.isNull()) {
3522 qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties must be called before Annotation is modified";
3523 return;
3524 }
3525 QDomNode prevProps = d->m_prevPropsOfAnnotBeingModified;
3526 QUndoCommand *uc = new Okular::ModifyAnnotationPropertiesCommand(d, annotation, page, prevProps, annotation->getAnnotationPropertiesDomNode());
3527 d->m_undoStack->push(uc);
3528 d->m_prevPropsOfAnnotBeingModified.clear();
3529}
3530
3531void Document::translatePageAnnotation(int page, Annotation *annotation, const NormalizedPoint &delta)
3532{
3533 int complete = (annotation->flags() & Okular::Annotation::BeingMoved) == 0;
3534 QUndoCommand *uc = new Okular::TranslateAnnotationCommand(d, annotation, page, delta, complete);
3535 d->m_undoStack->push(uc);
3536}
3537
3538void Document::adjustPageAnnotation(int page, Annotation *annotation, const Okular::NormalizedPoint &delta1, const Okular::NormalizedPoint &delta2)
3539{
3540 const bool complete = (annotation->flags() & Okular::Annotation::BeingResized) == 0;
3541 QUndoCommand *uc = new Okular::AdjustAnnotationCommand(d, annotation, page, delta1, delta2, complete);
3542 d->m_undoStack->push(uc);
3543}
3544
3545void Document::editPageAnnotationContents(int page, Annotation *annotation, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3546{
3547 QString prevContents = annotation->contents();
3548 QUndoCommand *uc = new EditAnnotationContentsCommand(d, annotation, page, newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos);
3549 d->m_undoStack->push(uc);
3550}
3551
3553{
3554 if (!annotation || (annotation->flags() & Annotation::DenyDelete)) {
3555 return false;
3556 }
3557
3558 if ((annotation->flags() & Annotation::External) && !d->canRemoveExternalAnnotations()) {
3559 return false;
3560 }
3561
3562 switch (annotation->subType()) {
3563 case Annotation::AText:
3564 case Annotation::ALine:
3565 case Annotation::AGeom:
3567 case Annotation::AStamp:
3568 case Annotation::AInk:
3569 case Annotation::ACaret:
3570 return true;
3571 default:
3572 return false;
3573 }
3574}
3575
3577{
3578 QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3579 d->m_undoStack->push(uc);
3580}
3581
3583{
3584 d->m_undoStack->beginMacro(i18nc("remove a collection of annotations from the page", "remove annotations"));
3585 for (Annotation *annotation : annotations) {
3586 QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3587 d->m_undoStack->push(uc);
3588 }
3589 d->m_undoStack->endMacro();
3590}
3591
3592bool DocumentPrivate::canAddAnnotationsNatively() const
3593{
3594 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3595
3597 return true;
3598 }
3599
3600 return false;
3601}
3602
3603bool DocumentPrivate::canModifyExternalAnnotations() const
3604{
3605 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3606
3608 return true;
3609 }
3610
3611 return false;
3612}
3613
3614bool DocumentPrivate::canRemoveExternalAnnotations() const
3615{
3616 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3617
3619 return true;
3620 }
3621
3622 return false;
3623}
3624
3625void Document::setPageTextSelection(int page, std::unique_ptr<RegularAreaRect> &&rect, const QColor &color)
3626{
3627 Page *kp = d->m_pagesVector[page];
3628 if (!d->m_generator || !kp) {
3629 return;
3630 }
3631
3632 // add or remove the selection basing whether rect is null or not
3633 if (rect) {
3634 kp->d->setTextSelections(*rect, color);
3635 } else {
3636 kp->d->deleteTextSelections();
3637 }
3638
3639 // notify observers about the change
3640 foreachObserver(notifyPageChanged(page, DocumentObserver::TextSelection));
3641}
3642
3644{
3645 return d->m_undoStack->canUndo();
3646}
3647
3649{
3650 return d->m_undoStack->canRedo();
3651}
3652
3653/* REFERENCE IMPLEMENTATION: better calling setViewport from other code
3654void Document::setNextPage()
3655{
3656 // advance page and set viewport on observers
3657 if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 )
3658 setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber + 1 ) );
3659}
3660
3661void Document::setPrevPage()
3662{
3663 // go to previous page and set viewport on observers
3664 if ( (*d->m_viewportIterator).pageNumber > 0 )
3665 setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber - 1 ) );
3666}
3667*/
3668
3669void Document::setViewport(const DocumentViewport &viewport, DocumentObserver *excludeObserver, bool smoothMove, bool updateHistory)
3670{
3671 if (!viewport.isValid()) {
3672 qCDebug(OkularCoreDebug) << "invalid viewport:" << viewport.toString();
3673 return;
3674 }
3675 if (viewport.pageNumber >= int(d->m_pagesVector.count())) {
3676 // qCDebug(OkularCoreDebug) << "viewport out of document:" << viewport.toString();
3677 return;
3678 }
3679
3680 // if already broadcasted, don't redo it
3681 DocumentViewport &oldViewport = *d->m_viewportIterator;
3682 // disabled by enrico on 2005-03-18 (less debug output)
3683 // if ( viewport == oldViewport )
3684 // qCDebug(OkularCoreDebug) << "setViewport with the same viewport.";
3685
3686 const int oldPageNumber = oldViewport.pageNumber;
3687
3688 // set internal viewport taking care of history
3689 if (oldViewport.pageNumber == viewport.pageNumber || !oldViewport.isValid() || !updateHistory) {
3690 // if page is unchanged save the viewport at current position in queue
3691 oldViewport = viewport;
3692 } else {
3693 // remove elements after viewportIterator in queue
3694 d->m_viewportHistory.erase(++d->m_viewportIterator, d->m_viewportHistory.end());
3695
3696 // keep the list to a reasonable size by removing head when needed
3697 if (d->m_viewportHistory.size() >= OKULAR_HISTORY_MAXSTEPS) {
3698 d->m_viewportHistory.pop_front();
3699 }
3700
3701 // add the item at the end of the queue
3702 d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), viewport);
3703 }
3704
3705 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3706
3707 const bool currentPageChanged = (oldPageNumber != currentViewportPage);
3708
3709 // notify change to all other (different from id) observers
3710 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3711 if (o != excludeObserver) {
3712 o->notifyViewportChanged(smoothMove);
3713 }
3714
3715 if (currentPageChanged) {
3716 o->notifyCurrentPageChanged(oldPageNumber, currentViewportPage);
3717 }
3718 }
3719}
3720
3721void Document::setViewportPage(int page, DocumentObserver *excludeObserver, bool smoothMove)
3722{
3723 // clamp page in range [0 ... numPages-1]
3724 if (page < 0) {
3725 page = 0;
3726 } else if (page > (int)d->m_pagesVector.count()) {
3727 page = d->m_pagesVector.count() - 1;
3728 }
3729
3730 // make a viewport from the page and broadcast it
3731 setViewport(DocumentViewport(page), excludeObserver, smoothMove);
3732}
3733
3734void Document::setZoom(int factor, DocumentObserver *excludeObserver)
3735{
3736 // notify change to all other (different from id) observers
3737 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3738 if (o != excludeObserver) {
3739 o->notifyZoom(factor);
3740 }
3741 }
3742}
3743
3745// restore viewport from the history
3746{
3747 if (d->m_viewportIterator != d->m_viewportHistory.begin()) {
3748 const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3749
3750 // restore previous viewport and notify it to observers
3751 --d->m_viewportIterator;
3752 foreachObserver(notifyViewportChanged(true));
3753
3754 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3755 if (oldViewportPage != currentViewportPage)
3756 foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3757 }
3758}
3759
3761// restore next viewport from the history
3762{
3763 auto nextIterator = std::list<DocumentViewport>::const_iterator(d->m_viewportIterator);
3764 ++nextIterator;
3765 if (nextIterator != d->m_viewportHistory.end()) {
3766 const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3767
3768 // restore next viewport and notify it to observers
3769 ++d->m_viewportIterator;
3770 foreachObserver(notifyViewportChanged(true));
3771
3772 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3773 if (oldViewportPage != currentViewportPage)
3774 foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3775 }
3776}
3777
3779{
3780 d->m_nextDocumentViewport = viewport;
3781}
3782
3784{
3785 d->m_nextDocumentDestination = namedDestination;
3786}
3787
3788void Document::searchText(int searchID, const QString &text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor &color)
3789{
3790 d->m_searchCancelled = false;
3791
3792 // safety checks: don't perform searches on empty or unsearchable docs
3793 if (!d->m_generator || !d->m_generator->hasFeature(Generator::TextExtraction) || d->m_pagesVector.isEmpty()) {
3795 return;
3796 }
3797
3798 // if searchID search not recorded, create new descriptor and init params
3799 QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3800 if (searchIt == d->m_searches.end()) {
3801 RunningSearch *search = new RunningSearch();
3802 search->continueOnPage = -1;
3803 searchIt = d->m_searches.insert(searchID, search);
3804 }
3805 RunningSearch *s = *searchIt;
3806
3807 // update search structure
3808 bool newText = text != s->cachedString;
3809 s->cachedString = text;
3810 s->cachedType = type;
3811 s->cachedCaseSensitivity = caseSensitivity;
3812 s->cachedViewportMove = moveViewport;
3813 s->cachedColor = color;
3814 s->isCurrentlySearching = true;
3815
3816 // global data for search
3817 QSet<int> *pagesToNotify = new QSet<int>;
3818
3819 // remove highlights from pages and queue them for notifying changes
3820 *pagesToNotify += s->highlightedPages;
3821 for (const int pageNumber : std::as_const(s->highlightedPages)) {
3822 d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3823 }
3824 s->highlightedPages.clear();
3825
3826 // set hourglass cursor
3828
3829 // 1. ALLDOC - process all document marking pages
3830 if (type == AllDocument) {
3832
3833 // search and highlight 'text' (as a solid phrase) on all pages
3834 QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID] { d->doContinueAllDocumentSearch(pagesToNotify, pageMatches, 0, searchID); });
3835 }
3836 // 2. NEXTMATCH - find next matching item (or start from top)
3837 // 3. PREVMATCH - find previous matching item (or start from bottom)
3838 else if (type == NextMatch || type == PreviousMatch) {
3839 // find out from where to start/resume search from
3840 const bool forward = type == NextMatch;
3841 const int viewportPage = (*d->m_viewportIterator).pageNumber;
3842 const int fromStartSearchPage = forward ? 0 : d->m_pagesVector.count() - 1;
3843 int currentPage = fromStart ? fromStartSearchPage : ((s->continueOnPage != -1) ? s->continueOnPage : viewportPage);
3844 Page *lastPage = fromStart ? nullptr : d->m_pagesVector[currentPage];
3845 int pagesDone = 0;
3846
3847 // continue checking last TextPage first (if it is the current page)
3848 RegularAreaRect *match = nullptr;
3849 if (lastPage && lastPage->number() == s->continueOnPage) {
3850 if (newText) {
3851 match = lastPage->findText(searchID, text, forward ? FromTop : FromBottom, caseSensitivity);
3852 } else {
3853 match = lastPage->findText(searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch);
3854 }
3855 if (!match) {
3856 if (forward) {
3857 currentPage++;
3858 } else {
3859 currentPage--;
3860 }
3861 pagesDone++;
3862 }
3863 }
3864
3865 s->pagesDone = pagesDone;
3866
3867 DoContinueDirectionMatchSearchStruct *searchStruct = new DoContinueDirectionMatchSearchStruct();
3868 searchStruct->pagesToNotify = pagesToNotify;
3869 searchStruct->match = match;
3870 searchStruct->currentPage = currentPage;
3871 searchStruct->searchID = searchID;
3872
3873 QTimer::singleShot(0, this, [this, searchStruct] { d->doContinueDirectionMatchSearch(searchStruct); });
3874 }
3875 // 4. GOOGLE* - process all document marking pages
3876 else if (type == GoogleAll || type == GoogleAny) {
3878 const QStringList words = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
3879
3880 // search and highlight every word in 'text' on all pages
3881 QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID, words] { d->doContinueGooglesDocumentSearch(pagesToNotify, pageMatches, 0, searchID, words); });
3882 }
3883}
3884
3886{
3887 // check if searchID is present in runningSearches
3888 QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3889 if (it == d->m_searches.constEnd()) {
3891 return;
3892 }
3893
3894 // start search with cached parameters from last search by searchID
3895 RunningSearch *p = *it;
3896 if (!p->isCurrentlySearching) {
3897 searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, p->cachedType, p->cachedViewportMove, p->cachedColor);
3898 }
3899}
3900
3901void Document::continueSearch(int searchID, SearchType type)
3902{
3903 // check if searchID is present in runningSearches
3904 QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3905 if (it == d->m_searches.constEnd()) {
3907 return;
3908 }
3909
3910 // start search with cached parameters from last search by searchID
3911 RunningSearch *p = *it;
3912 if (!p->isCurrentlySearching) {
3913 searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, type, p->cachedViewportMove, p->cachedColor);
3914 }
3915}
3916
3917void Document::resetSearch(int searchID)
3918{
3919 // if we are closing down, don't bother doing anything
3920 if (!d->m_generator) {
3921 return;
3922 }
3923
3924 // check if searchID is present in runningSearches
3925 QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3926 if (searchIt == d->m_searches.end()) {
3927 return;
3928 }
3929
3930 // get previous parameters for search
3931 RunningSearch *s = *searchIt;
3932
3933 // unhighlight pages and inform observers about that
3934 for (const int pageNumber : std::as_const(s->highlightedPages)) {
3935 d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3936 foreachObserver(notifyPageChanged(pageNumber, DocumentObserver::Highlights));
3937 }
3938
3939 // send the setup signal too (to update views that filter on matches)
3940 foreachObserver(notifySetup(d->m_pagesVector, 0));
3941
3942 // remove search from the runningSearches list and delete it
3943 d->m_searches.erase(searchIt);
3944 delete s;
3945}
3946
3948{
3949 d->m_searchCancelled = true;
3950}
3951
3953{
3954 d->m_undoStack->undo();
3955}
3956
3958{
3959 d->m_undoStack->redo();
3960}
3961
3962void Document::editFormText(int pageNumber, Okular::FormFieldText *form, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3963{
3964 QUndoCommand *uc = new EditFormTextCommand(this->d, form, pageNumber, newContents, newCursorPos, form->text(), prevCursorPos, prevAnchorPos);
3965 d->m_undoStack->push(uc);
3966}
3967
3968void Document::editFormList(int pageNumber, FormFieldChoice *form, const QList<int> &newChoices)
3969{
3970 const QList<int> prevChoices = form->currentChoices();
3971 QUndoCommand *uc = new EditFormListCommand(this->d, form, pageNumber, newChoices, prevChoices);
3972 d->m_undoStack->push(uc);
3973}
3974
3975void Document::editFormCombo(int pageNumber, FormFieldChoice *form, const QString &newText, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3976{
3977 QString prevText;
3978 if (form->currentChoices().isEmpty()) {
3979 prevText = form->editChoice();
3980 } else {
3981 prevText = form->choices().at(form->currentChoices().constFirst());
3982 }
3983
3984 QUndoCommand *uc = new EditFormComboCommand(this->d, form, pageNumber, newText, newCursorPos, prevText, prevCursorPos, prevAnchorPos);
3985 d->m_undoStack->push(uc);
3986}
3987
3988void Document::editFormButtons(int pageNumber, const QList<FormFieldButton *> &formButtons, const QList<bool> &newButtonStates)
3989{
3990 QUndoCommand *uc = new EditFormButtonsCommand(this->d, pageNumber, formButtons, newButtonStates);
3991 d->m_undoStack->push(uc);
3992}
3993
3995{
3996 const int numOfPages = pages();
3997 for (int i = currentPage(); i >= 0; i--) {
3998 d->refreshPixmaps(i);
3999 }
4000 for (int i = currentPage() + 1; i < numOfPages; i++) {
4001 d->refreshPixmaps(i);
4002 }
4003}
4004
4006{
4007 return d->m_bookmarkManager;
4008}
4009
4011{
4012 QList<int> list;
4013 uint docPages = pages();
4014
4015 // pages are 0-indexed internally, but 1-indexed externally
4016 for (uint i = 0; i < docPages; i++) {
4017 if (bookmarkManager()->isBookmarked(i)) {
4018 list << i + 1;
4019 }
4020 }
4021 return list;
4022}
4023
4025{
4026 // Code formerly in Part::slotPrint()
4027 // range detecting
4028 QString range;
4029 uint docPages = pages();
4030 int startId = -1;
4031 int endId = -1;
4032
4033 for (uint i = 0; i < docPages; ++i) {
4034 if (bookmarkManager()->isBookmarked(i)) {
4035 if (startId < 0) {
4036 startId = i;
4037 }
4038 if (endId < 0) {
4039 endId = startId;
4040 } else {
4041 ++endId;
4042 }
4043 } else if (startId >= 0 && endId >= 0) {
4044 if (!range.isEmpty()) {
4045 range += QLatin1Char(',');
4046 }
4047
4048 if (endId - startId > 0) {
4049 range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
4050 } else {
4051 range += QString::number(startId + 1);
4052 }
4053 startId = -1;
4054 endId = -1;
4055 }
4056 }
4057 if (startId >= 0 && endId >= 0) {
4058 if (!range.isEmpty()) {
4059 range += QLatin1Char(',');
4060 }
4061
4062 if (endId - startId > 0) {
4063 range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
4064 } else {
4065 range += QString::number(startId + 1);
4066 }
4067 }
4068 return range;
4069}
4070
4071struct ExecuteNextActionsHelper : public QObject, private DocumentObserver {
4072 Q_OBJECT
4073public:
4074 explicit ExecuteNextActionsHelper(Document *doc)
4075 : m_doc(doc)
4076 {
4077 doc->addObserver(this);
4078 connect(doc, &Document::aboutToClose, this, [this] { b = false; });
4079 }
4080
4081 ~ExecuteNextActionsHelper() override
4082 {
4083 m_doc->removeObserver(this);
4084 }
4085
4086 void notifySetup(const QVector<Okular::Page *> & /*pages*/, int setupFlags) override
4087 {
4088 if (setupFlags == DocumentChanged || setupFlags == UrlChanged) {
4089 b = false;
4090 }
4091 }
4092
4093 bool shouldExecuteNextAction() const
4094 {
4095 return b;
4096 }
4097
4098private:
4099 Document *const m_doc;
4100 bool b = true;
4101};
4102
4104{
4105 if (!action) {
4106 return;
4107 }
4108
4109 // Don't execute next actions if the action itself caused the closing of the document
4110 const ExecuteNextActionsHelper executeNextActionsHelper(this);
4111
4112 switch (action->actionType()) {
4113 case Action::Goto: {
4114 const GotoAction *go = static_cast<const GotoAction *>(action);
4115 d->m_nextDocumentViewport = go->destViewport();
4116 d->m_nextDocumentDestination = go->destinationName();
4117
4118 // Explanation of why d->m_nextDocumentViewport is needed:
4119 // all openRelativeFile does is launch a signal telling we
4120 // want to open another URL, the problem is that when the file is
4121 // non local, the loading is done asynchronously so you can't
4122 // do a setViewport after the if as it was because you are doing the setViewport
4123 // on the old file and when the new arrives there is no setViewport for it and
4124 // it does not show anything
4125
4126 // first open filename if link is pointing outside this document
4127 const QString filename = go->fileName();
4128 if (go->isExternal() && !d->openRelativeFile(filename)) {
4129 qCWarning(OkularCoreDebug).nospace() << "Action: Error opening '" << filename << "'.";
4130 break;
4131 } else {
4132 const DocumentViewport nextViewport = d->nextDocumentViewport();
4133 // skip local links that point to nowhere (broken ones)
4134 if (!nextViewport.isValid()) {
4135 break;
4136 }
4137
4138 setViewport(nextViewport, nullptr, true);
4139 d->m_nextDocumentViewport = DocumentViewport();
4140 d->m_nextDocumentDestination = QString();
4141 }
4142
4143 } break;
4144
4145 case Action::Execute: {
4146 const ExecuteAction *exe = static_cast<const ExecuteAction *>(action);
4147 const QString fileName = exe->fileName();
4148 if (fileName.endsWith(QLatin1String(".pdf"), Qt::CaseInsensitive)) {
4149 d->openRelativeFile(fileName);
4150 break;
4151 }
4152
4153 // Albert: the only pdf i have that has that kind of link don't define
4154 // an application and use the fileName as the file to open
4155 QUrl url = d->giveAbsoluteUrl(fileName);
4156 QMimeDatabase db;
4157 QMimeType mime = db.mimeTypeForUrl(url);
4158 // Check executables
4159 if (KIO::OpenUrlJob::isExecutableFile(url, mime.name())) {
4160 // Don't have any pdf that uses this code path, just a guess on how it should work
4161 if (!exe->parameters().isEmpty()) {
4162 url = d->giveAbsoluteUrl(exe->parameters());
4163 mime = db.mimeTypeForUrl(url);
4164
4165 if (KIO::OpenUrlJob::isExecutableFile(url, mime.name())) {
4166 // this case is a link pointing to an executable with a parameter
4167 // that also is an executable, possibly a hand-crafted pdf
4168 Q_EMIT error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
4169 break;
4170 }
4171 } else {
4172 // this case is a link pointing to an executable with no parameters
4173 // core developers find unacceptable executing it even after asking the user
4174 Q_EMIT error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
4175 break;
4176 }
4177 }
4178
4179 KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mime.name());
4181 job->start();
4182 connect(job, &KIO::OpenUrlJob::result, this, [this, mime](KJob *job) {
4183 if (job->error()) {
4184 Q_EMIT error(i18n("No application found for opening file of mimetype %1.", mime.name()), -1);
4185 }
4186 });
4187 } break;
4188
4189 case Action::DocAction: {
4190 const DocumentAction *docaction = static_cast<const DocumentAction *>(action);
4191 switch (docaction->documentActionType()) {
4193 setViewportPage(0);
4194 break;
4196 if ((*d->m_viewportIterator).pageNumber > 0) {
4197 setViewportPage((*d->m_viewportIterator).pageNumber - 1);
4198 }
4199 break;
4201 if ((*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1) {
4202 setViewportPage((*d->m_viewportIterator).pageNumber + 1);
4203 }
4204 break;
4206 setViewportPage(d->m_pagesVector.count() - 1);
4207 break;
4210 break;
4213 break;
4215 Q_EMIT quit();
4216 break;
4219 break;
4222 break;
4224 Q_EMIT linkFind();
4225 break;
4228 break;
4230 Q_EMIT close();
4231 break;
4234 break;
4237 break;
4238 }
4239 } break;
4240
4241 case Action::Browse: {
4242 const BrowseAction *browse = static_cast<const BrowseAction *>(action);
4243 QString lilySource;
4244 int lilyRow = 0, lilyCol = 0;
4245 // if the url is a mailto one, invoke mailer
4246 if (browse->url().scheme() == QLatin1String("mailto")) {
4247 QDesktopServices::openUrl(browse->url());
4248 } else if (extractLilyPondSourceReference(browse->url(), &lilySource, &lilyRow, &lilyCol)) {
4249 const SourceReference ref(lilySource, lilyRow, lilyCol);
4251 } else {
4252 const QUrl url = browse->url();
4253
4254 // fix for #100366, documents with relative links that are the form of http:foo.pdf
4255 if ((url.scheme() == QLatin1String("http")) && url.host().isEmpty() && url.fileName().endsWith(QLatin1String("pdf"))) {
4256 d->openRelativeFile(url.fileName());
4257 break;
4258 }
4259
4260 // handle documents with relative path
4261 QUrl realUrl;
4262 if (d->m_url.isValid()) {
4263 realUrl = KIO::upUrl(d->m_url).resolved(url);
4264 } else if (!url.isRelative()) {
4265 realUrl = url;
4266 }
4267 if (realUrl.isValid()) {
4268 auto *job = new KIO::OpenUrlJob(realUrl);
4269 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->m_widget.data()));
4270 job->start();
4271 }
4272 }
4273 } break;
4274
4275 case Action::Sound: {
4276 const SoundAction *linksound = static_cast<const SoundAction *>(action);
4277 AudioPlayer::instance()->playSound(linksound->sound(), linksound);
4278 } break;
4279
4280 case Action::Script: {
4281 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4282 if (!d->m_scripter) {
4283 d->m_scripter = new Scripter(d);
4284 }
4285 d->m_scripter->execute(linkscript->scriptType(), linkscript->script());
4286 } break;
4287
4288 case Action::Movie:
4289 Q_EMIT processMovieAction(static_cast<const MovieAction *>(action));
4290 break;
4291 case Action::Rendition: {
4292 const RenditionAction *linkrendition = static_cast<const RenditionAction *>(action);
4293 if (!linkrendition->script().isEmpty()) {
4294 if (!d->m_scripter) {
4295 d->m_scripter = new Scripter(d);
4296 }
4297 d->m_scripter->execute(linkrendition->scriptType(), linkrendition->script());
4298 }
4299
4300 Q_EMIT processRenditionAction(static_cast<const RenditionAction *>(action));
4301 } break;
4302 case Action::BackendOpaque: {
4303 d->m_generator->opaqueAction(static_cast<const BackendOpaqueAction *>(action));
4304 } break;
4305 }
4306
4307 if (executeNextActionsHelper.shouldExecuteNextAction()) {
4308 const QVector<Action *> nextActions = action->nextActions();
4309 for (const Action *a : nextActions) {
4310 processAction(a);
4311 }
4312 }
4313}
4314
4316{
4317 if (action->actionType() != Action::Script) {
4318 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for formatting.";
4319 return;
4320 }
4321
4322 // Lookup the page of the FormFieldText
4323 int foundPage = d->findFieldPageNumber(fft);
4324
4325 if (foundPage == -1) {
4326 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4327 return;
4328 }
4329
4330 const QString unformattedText = fft->text();
4331
4332 std::shared_ptr<Event> event = Event::createFormatEvent(fft, d->m_pagesVector[foundPage]);
4333
4334 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4335
4336 d->executeScriptEvent(event, linkscript);
4337
4338 const QString formattedText = event->value().toString();
4339 if (formattedText != unformattedText) {
4340 // We set the formattedText, because when we call refreshFormWidget
4341 // It will set the QLineEdit to this formattedText
4342 fft->setText(formattedText);
4343 fft->setAppearanceText(formattedText);
4345 d->refreshPixmaps(foundPage);
4346 // Then we make the form have the unformatted text, to use
4347 // in calculations and other things.
4348 fft->setText(unformattedText);
4350 // When the field was calculated we need to refresh even
4351 // if the format script changed nothing. e.g. on error.
4352 // This is because the recalculateForms function delegated
4353 // the responsiblity for the refresh to us.
4355 d->refreshPixmaps(foundPage);
4356 }
4357}
4358
4359QString DocumentPrivate::diff(const QString &oldVal, const QString &newVal)
4360{
4361 // We need to consider unicode surrogate pairs and others so working
4362 // with QString directly, even with the private QStringIterator is
4363 // not that simple to get right
4364 // so let's just convert to ucs4
4365 // also, given that toUcs4 is either a QList or a QVector depending on
4366 // qt version, let's try keep it very auto-typed to ease Qt6 porting
4367
4368 auto oldUcs4 = oldVal.toStdU32String();
4369 auto newUcs4 = newVal.toStdU32String();
4370
4371 for (size_t i = 0; i < std::min(oldUcs4.size(), newUcs4.size()); i++) {
4372 if (oldUcs4.at(i) != newUcs4.at(i)) {
4373 return QString::fromUcs4(std::u32string_view {newUcs4}.substr(i).data(), newUcs4.size() - i);
4374 }
4375 }
4376 if (oldUcs4.size() < newUcs4.size()) {
4377 return QString::fromUcs4(std::u32string_view {newUcs4}.substr(oldUcs4.size()).data(), newUcs4.size() - oldUcs4.size());
4378 }
4379 return {};
4380}
4381
4383{
4384 if (action->actionType() != Action::Script) {
4385 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4386 return;
4387 }
4388 // Lookup the page of the FormFieldText
4389 int foundPage = d->findFieldPageNumber(fft);
4390
4391 if (foundPage == -1) {
4392 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4393 return;
4394 }
4395
4396 std::shared_ptr<Event> event = Event::createKeystrokeEvent(fft, d->m_pagesVector[foundPage]);
4397 event->setChange(DocumentPrivate::diff(fft->text(), newValue.toString()));
4398
4399 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4400
4401 d->executeScriptEvent(event, linkscript);
4402
4403 if (event->returnCode()) {
4404 fft->setText(newValue.toString());
4405 } else {
4407 }
4408}
4409
4411{
4412 if (action->actionType() != Action::Script) {
4413 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4414 return;
4415 }
4416 // Lookup the page of the FormFieldText
4417 int foundPage = d->findFieldPageNumber(fft);
4418
4419 if (foundPage == -1) {
4420 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4421 return;
4422 }
4423
4424 std::shared_ptr<Event> event = Event::createKeystrokeEvent(fft, d->m_pagesVector[foundPage]);
4425 event->setWillCommit(true);
4426
4427 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4428
4429 d->executeScriptEvent(event, linkscript);
4430
4431 if (event->returnCode()) {
4432 fft->setText(event->value().toString());
4433 // TODO commit value
4434 } else {
4435 // TODO reset to committed value
4436 }
4437}
4438
4440{
4441 if (!action || action->actionType() != Action::Script) {
4442 return;
4443 }
4444
4445 // Lookup the page of the FormFieldText
4446 int foundPage = d->findFieldPageNumber(field);
4447
4448 if (foundPage == -1) {
4449 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4450 return;
4451 }
4452
4453 std::shared_ptr<Event> event = Event::createFormFocusEvent(field, d->m_pagesVector[foundPage]);
4454
4455 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4456
4457 d->executeScriptEvent(event, linkscript);
4458}
4459
4460void Document::processValidateAction(const Action *action, Okular::FormFieldText *fft, bool &returnCode)
4461{
4462 if (!action || action->actionType() != Action::Script) {
4463 return;
4464 }
4465
4466 // Lookup the page of the FormFieldText
4467 int foundPage = d->findFieldPageNumber(fft);
4468
4469 if (foundPage == -1) {
4470 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4471 return;
4472 }
4473
4474 std::shared_ptr<Event> event = Event::createFormValidateEvent(fft, d->m_pagesVector[foundPage]);
4475
4476 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4477
4478 d->executeScriptEvent(event, linkscript);
4479 returnCode = event->returnCode();
4480}
4481
4483{
4484 if (!action || action->actionType() != Action::Script) {
4485 return;
4486 }
4487
4488 // Lookup the page of the FormFieldText
4489 int foundPage = d->findFieldPageNumber(ff);
4490
4491 if (foundPage == -1) {
4492 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4493 return;
4494 }
4495
4496 std::shared_ptr<Event> event = Event::createFieldMouseUpEvent(ff, d->m_pagesVector[foundPage]);
4497
4498 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4499
4500 d->executeScriptEvent(event, linkscript);
4501}
4502
4504{
4505 if (!ref) {
4506 return;
4507 }
4508
4509 const QUrl url = d->giveAbsoluteUrl(ref->fileName());
4510 if (!url.isLocalFile()) {
4511 qCDebug(OkularCoreDebug) << url.url() << "is not a local file.";
4512 return;
4513 }
4514
4515 const QString absFileName = url.toLocalFile();
4516 if (!QFile::exists(absFileName)) {
4517 qCDebug(OkularCoreDebug) << "No such file:" << absFileName;
4518 return;
4519 }
4520
4521 bool handled = false;
4522 Q_EMIT sourceReferenceActivated(absFileName, ref->row(), ref->column(), &handled);
4523 if (handled) {
4524 return;
4525 }
4526
4527 static QHash<int, QString> editors;
4528 // init the editors table if empty (on first run, usually)
4529 if (editors.isEmpty()) {
4530 editors = buildEditorsMap();
4531 }
4532
4533 // prefer the editor from the command line
4534 QString p = d->editorCommandOverride;
4535 if (p.isEmpty()) {
4536 QHash<int, QString>::const_iterator it = editors.constFind(SettingsCore::externalEditor());
4537 if (it != editors.constEnd()) {
4538 p = *it;
4539 } else {
4540 p = SettingsCore::externalEditorCommand();
4541 }
4542 }
4543 // custom editor not yet configured
4544 if (p.isEmpty()) {
4545 return;
4546 }
4547
4548 // manually append the %f placeholder if not specified
4549 if (p.indexOf(QLatin1String("%f")) == -1) {
4550 p.append(QLatin1String(" %f"));
4551 }
4552
4553 // replacing the placeholders
4555 map.insert(QLatin1Char('f'), absFileName);
4556 map.insert(QLatin1Char('c'), QString::number(ref->column()));
4557 map.insert(QLatin1Char('l'), QString::number(ref->row()));
4559 if (cmd.isEmpty()) {
4560 return;
4561 }
4562 QStringList args = KShell::splitArgs(cmd);
4563 if (args.isEmpty()) {
4564 return;
4565 }
4566
4567 const QString prog = args.takeFirst();
4568 // Make sure prog is in PATH and not just in the CWD
4569 const QString progFullPath = QStandardPaths::findExecutable(prog);
4570 if (progFullPath.isEmpty()) {
4571 return;
4572 }
4573
4574 KProcess::startDetached(progFullPath, args);
4575}
4576
4577const SourceReference *Document::dynamicSourceReference(int pageNr, double absX, double absY)
4578{
4579 if (!d->m_synctex_scanner) {
4580 return nullptr;
4581 }
4582
4583 const QSizeF dpi = d->m_generator->dpi();
4584
4585 if (synctex_edit_query(d->m_synctex_scanner, pageNr + 1, absX * 72. / dpi.width(), absY * 72. / dpi.height()) > 0) {
4586 synctex_node_p node;
4587 // TODO what should we do if there is really more than one node?
4588 while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
4589 int line = synctex_node_line(node);
4590 int col = synctex_node_column(node);
4591 // column extraction does not seem to be implemented in synctex so far. set the SourceReference default value.
4592 if (col == -1) {
4593 col = 0;
4594 }
4595 const char *name = synctex_scanner_get_name(d->m_synctex_scanner, synctex_node_tag(node));
4596
4597 return new Okular::SourceReference(QFile::decodeName(name), line, col);
4598 }
4599 }
4600 return nullptr;
4601}
4602
4604{
4605 if (d->m_generator) {
4606 if (d->m_generator->hasFeature(Generator::PrintNative)) {
4607 return NativePrinting;
4608 }
4609
4610#ifndef Q_OS_WIN
4611 if (d->m_generator->hasFeature(Generator::PrintPostscript)) {
4612 return PostscriptPrinting;
4613 }
4614#endif
4615 }
4616
4617 return NoPrinting;
4618}
4619
4621{
4622 return d->m_generator ? d->m_generator->hasFeature(Generator::PrintToFile) : false;
4623}
4624
4626{
4627 return d->m_generator ? d->m_generator->print(printer) : Document::UnknownPrintError;
4628}
4629
4631{
4632 switch (error) {
4633 case TemporaryFileOpenPrintError:
4634 return i18n("Could not open a temporary file");
4635 case FileConversionPrintError:
4636 return i18n("Print conversion failed");
4637 case PrintingProcessCrashPrintError:
4638 return i18n("Printing process crashed");
4639 case PrintingProcessStartPrintError:
4640 return i18n("Printing process could not start");
4641 case PrintToFilePrintError:
4642 return i18n("Printing to file failed");
4643 case InvalidPrinterStatePrintError:
4644 return i18n("Printer was in invalid state");
4645 case UnableToFindFilePrintError:
4646 return i18n("Unable to find file to print");
4647 case NoFileToPrintError:
4648 return i18n("There was no file to print");
4649 case NoBinaryToPrintError:
4650 return i18n("Could not find a suitable binary for printing. Make sure CUPS lpr binary is available");
4651 case InvalidPageSizePrintError:
4652 return i18n("The page print size is invalid");
4653 case NoPrintError:
4654 return QString();
4655 case UnknownPrintError:
4656 return QString();
4657 }
4658
4659 return QString();
4660}
4661
4663{
4664 if (d->m_generator) {
4665 PrintInterface *iface = qobject_cast<Okular::PrintInterface *>(d->m_generator);
4666 return iface ? iface->printConfigurationWidget() : nullptr;
4667 } else {
4668 return nullptr;
4669 }
4670}
4671
4673{
4674 if (!dialog) {
4675 return;
4676 }
4677
4678 // We know it's a BackendConfigDialog, but check anyway
4679 BackendConfigDialog *bcd = dynamic_cast<BackendConfigDialog *>(dialog);
4680 if (!bcd) {
4681 return;
4682 }
4683
4684 // ensure that we have all the generators with settings loaded
4685 QVector<KPluginMetaData> offers = DocumentPrivate::configurableGenerators();
4686 d->loadServiceList(offers);
4687
4688 // We want the generators to be sorted by name so let's fill in a QMap
4689 // this sorts by internal id which is not awesome, but at least the sorting
4690 // is stable between runs that before it wasn't
4691 QMap<QString, GeneratorInfo> sortedGenerators;
4692 QHash<QString, GeneratorInfo>::iterator it = d->m_loadedGenerators.begin();
4693 QHash<QString, GeneratorInfo>::iterator itEnd = d->m_loadedGenerators.end();
4694 for (; it != itEnd; ++it) {
4695 sortedGenerators.insert(it.key(), it.value());
4696 }
4697
4698 bool pagesAdded = false;
4699 QMap<QString, GeneratorInfo>::iterator sit = sortedGenerators.begin();
4700 QMap<QString, GeneratorInfo>::iterator sitEnd = sortedGenerators.end();
4701 for (; sit != sitEnd; ++sit) {
4702 Okular::ConfigInterface *iface = d->generatorConfig(sit.value());
4703 if (iface) {
4704 iface->addPages(dialog);
4705 pagesAdded = true;
4706
4707 if (sit.value().generator == d->m_generator) {
4708 const int rowCount = bcd->thePageWidget()->model()->rowCount();
4709 KPageView *view = bcd->thePageWidget();
4710 view->setCurrentPage(view->model()->index(rowCount - 1, 0));
4711 }
4712 }
4713 }
4714 if (pagesAdded) {
4715 connect(dialog, &KConfigDialog::settingsChanged, this, [this] { d->slotGeneratorConfigChanged(); });
4716 }
4717}
4718
4719QVector<KPluginMetaData> DocumentPrivate::configurableGenerators()
4720{
4721 const QVector<KPluginMetaData> available = availableGenerators();
4723 for (const KPluginMetaData &md : available) {
4724 if (md.rawData().value(QStringLiteral("X-KDE-okularHasInternalSettings")).toBool()) {
4725 result << md;
4726 }
4727 }
4728 return result;
4729}
4730
4732{
4733 if (!d->m_generator) {
4734 return KPluginMetaData();
4735 }
4736
4737 auto genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
4738 Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
4739 return genIt.value().metadata;
4740}
4741
4743{
4744 return DocumentPrivate::configurableGenerators().size();
4745}
4746
4748{
4749 // TODO: make it a static member of DocumentPrivate?
4750 QStringList result = d->m_supportedMimeTypes;
4751 if (result.isEmpty()) {
4752 const QVector<KPluginMetaData> available = DocumentPrivate::availableGenerators();
4753 for (const KPluginMetaData &md : available) {
4754 result << md.mimeTypes();
4755 }
4756
4757 // Remove duplicate mimetypes represented by different names
4758 QMimeDatabase mimeDatabase;
4759 QSet<QMimeType> uniqueMimetypes;
4760 for (const QString &mimeName : std::as_const(result)) {
4761 uniqueMimetypes.insert(mimeDatabase.mimeTypeForName(mimeName));
4762 }
4763 result.clear();
4764 for (const QMimeType &mimeType : uniqueMimetypes) {
4765 result.append(mimeType.name());
4766 }
4767
4768 // Add the Okular archive mimetype
4769 result << QStringLiteral("application/vnd.kde.okular-archive");
4770
4771 // Sorting by mimetype name doesn't make a ton of sense,
4772 // but ensures that the list is ordered the same way every time
4773 std::sort(result.begin(), result.end());
4774
4775 d->m_supportedMimeTypes = result;
4776 }
4777 return result;
4778}
4779
4781{
4782 if (!d->m_generator) {
4783 return false;
4784 }
4785
4786 return d->m_generator->hasFeature(Generator::SwapBackingFile);
4787}
4788
4789bool Document::swapBackingFile(const QString &newFileName, const QUrl &url)
4790{
4791 if (!d->m_generator) {
4792 return false;
4793 }
4794
4795 if (!d->m_generator->hasFeature(Generator::SwapBackingFile)) {
4796 return false;
4797 }
4798
4799 // Save metadata about the file we're about to close
4800 d->saveDocumentInfo();
4801
4802 d->clearAndWaitForRequests();
4803
4804 qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName;
4805 QVector<Page *> newPagesVector;
4806 Generator::SwapBackingFileResult result = d->m_generator->swapBackingFile(newFileName, newPagesVector);
4807 if (result != Generator::SwapBackingFileError) {
4808 QList<ObjectRect *> rectsToDelete;
4809 QList<Annotation *> annotationsToDelete;
4810 QSet<PagePrivate *> pagePrivatesToDelete;
4811
4812 if (result == Generator::SwapBackingFileReloadInternalData) {
4813 // Here we need to replace everything that the old generator
4814 // had created with what the new one has without making it look like
4815 // we have actually closed and opened the file again
4816
4817 // Simple sanity check
4818 if (newPagesVector.count() != d->m_pagesVector.count()) {
4819 return false;
4820 }
4821
4822 // Update the undo stack contents
4823 for (int i = 0; i < d->m_undoStack->count(); ++i) {
4824 // Trust me on the const_cast ^_^
4825 QUndoCommand *uc = const_cast<QUndoCommand *>(d->m_undoStack->command(i));
4826 if (OkularUndoCommand *ouc = dynamic_cast<OkularUndoCommand *>(uc)) {
4827 const bool success = ouc->refreshInternalPageReferences(newPagesVector);
4828 if (!success) {
4829 qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc;
4830 return false;
4831 }
4832 } else {
4833 qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc;
4834 return false;
4835 }
4836 }
4837
4838 for (int i = 0; i < d->m_pagesVector.count(); ++i) {
4839 // switch the PagePrivate* from newPage to oldPage
4840 // this way everyone still holding Page* doesn't get
4841 // disturbed by it
4842 Page *oldPage = d->m_pagesVector[i];
4843 Page *newPage = newPagesVector[i];
4844 newPage->d->adoptGeneratedContents(oldPage->d);
4845
4846 pagePrivatesToDelete << oldPage->d;
4847 oldPage->d = newPage->d;
4848 oldPage->d->m_page = oldPage;
4849 oldPage->d->m_doc = d;
4850 newPage->d = nullptr;
4851
4852 annotationsToDelete << oldPage->m_annotations;
4853 rectsToDelete << oldPage->m_rects;
4854 oldPage->m_annotations = newPage->m_annotations;
4855 oldPage->m_rects = newPage->m_rects;
4856 }
4857 qDeleteAll(newPagesVector);
4858 }
4859
4860 d->m_url = url;
4861 d->m_docFileName = newFileName;
4862 d->updateMetadataXmlNameAndDocSize();
4863 d->m_bookmarkManager->setUrl(d->m_url);
4864 d->m_documentInfo = DocumentInfo();
4865 d->m_documentInfoAskedKeys.clear();
4866
4867 if (d->m_synctex_scanner) {
4868 synctex_scanner_free(d->m_synctex_scanner);
4869 d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(newFileName).constData(), nullptr, 1);
4870 if (!d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String("sync"))) {
4871 d->loadSyncFile(newFileName);
4872 }
4873 }
4874
4875 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::UrlChanged));
4876
4877 qDeleteAll(annotationsToDelete);
4878 qDeleteAll(rectsToDelete);
4879 qDeleteAll(pagePrivatesToDelete);
4880
4881 return true;
4882 } else {
4883 return false;
4884 }
4885}
4886
4887bool Document::swapBackingFileArchive(const QString &newFileName, const QUrl &url)
4888{
4889 qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName;
4890
4891 ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive(newFileName);
4892 if (!newArchive) {
4893 return false;
4894 }
4895
4896 const QString tempFileName = newArchive->document.fileName();
4897
4898 const bool success = swapBackingFile(tempFileName, url);
4899
4900 if (success) {
4901 delete d->m_archiveData;
4902 d->m_archiveData = newArchive;
4903 }
4904
4905 return success;
4906}
4907
4909{
4910 if (clean) {
4911 d->m_undoStack->setClean();
4912 } else {
4913 d->m_undoStack->resetClean();
4914 }
4915}
4916
4917bool Document::isHistoryClean() const
4918{
4919 return d->m_undoStack->isClean();
4920}
4921
4923{
4924 if (!d->m_generator) {
4925 return false;
4926 }
4927 Q_ASSERT(!d->m_generatorName.isEmpty());
4928
4929 QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
4930 Q_ASSERT(genIt != d->m_loadedGenerators.end());
4931 SaveInterface *saveIface = d->generatorSave(genIt.value());
4932 if (!saveIface) {
4933 return false;
4934 }
4935
4936 return saveIface->supportsOption(SaveInterface::SaveChanges);
4937}
4938
4940{
4941 switch (cap) {
4943 /* Assume that if the generator supports saving, forms can be saved.
4944 * We have no means to actually query the generator at the moment
4945 * TODO: Add some method to query the generator in SaveInterface */
4946 return canSaveChanges();
4947
4949 return d->canAddAnnotationsNatively();
4950 }
4951
4952 return false;
4953}
4954
4955bool Document::saveChanges(const QString &fileName)
4956{
4957 QString errorText;
4958 return saveChanges(fileName, &errorText);
4959}
4960
4961bool Document::saveChanges(const QString &fileName, QString *errorText)
4962{
4963 if (!d->m_generator || fileName.isEmpty()) {
4964 return false;
4965 }
4966 Q_ASSERT(!d->m_generatorName.isEmpty());
4967
4968 QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
4969 Q_ASSERT(genIt != d->m_loadedGenerators.end());
4970 SaveInterface *saveIface = d->generatorSave(genIt.value());
4971 if (!saveIface || !saveIface->supportsOption(SaveInterface::SaveChanges)) {
4972 return false;
4973 }
4974
4975 return saveIface->save(fileName, SaveInterface::SaveChanges, errorText);
4976}
4977
4979{
4980 if (!view) {
4981 return;
4982 }
4983
4984 Document *viewDoc = view->viewDocument();
4985 if (viewDoc) {
4986 // check if already registered for this document
4987 if (viewDoc == this) {
4988 return;
4989 }
4990
4991 viewDoc->unregisterView(view);
4992 }
4993
4994 d->m_views.insert(view);
4995 view->d_func()->document = d;
4996}
4997
4999{
5000 if (!view) {
5001 return;
5002 }
5003
5004 Document *viewDoc = view->viewDocument();
5005 if (!viewDoc || viewDoc != this) {
5006 return;
5007 }
5008
5009 view->d_func()->document = nullptr;
5010 d->m_views.remove(view);
5011}
5012
5014{
5015 if (d->m_generator) {
5016 return d->m_generator->requestFontData(font);
5017 }
5018
5019 return {};
5020}
5021
5022ArchiveData *DocumentPrivate::unpackDocumentArchive(const QString &archivePath)
5023{
5024 QMimeDatabase db;
5025 const QMimeType mime = db.mimeTypeForFile(archivePath, QMimeDatabase::MatchExtension);
5026 if (!mime.inherits(QStringLiteral("application/vnd.kde.okular-archive"))) {
5027 return nullptr;
5028 }
5029
5030 KZip okularArchive(archivePath);
5031 if (!okularArchive.open(QIODevice::ReadOnly)) {
5032 return nullptr;
5033 }
5034
5035 const KArchiveDirectory *mainDir = okularArchive.directory();
5036
5037 // Check the archive doesn't have folders, we don't create them when saving the archive
5038 // and folders mean paths and paths mean path traversal issues
5039 const QStringList mainDirEntries = mainDir->entries();
5040 for (const QString &entry : mainDirEntries) {
5041 if (mainDir->entry(entry)->isDirectory()) {
5042 qWarning() << "Warning: Found a directory inside" << archivePath << " - Okular does not create files like that so it is most probably forged.";
5043 return nullptr;
5044 }
5045 }
5046
5047 const KArchiveEntry *mainEntry = mainDir->entry(QStringLiteral("content.xml"));
5048 if (!mainEntry || !mainEntry->isFile()) {
5049 return nullptr;
5050 }
5051
5052 std::unique_ptr<QIODevice> mainEntryDevice(static_cast<const KZipFileEntry *>(mainEntry)->createDevice());
5053 QDomDocument doc;
5054 if (!doc.setContent(mainEntryDevice.get())) {
5055 return nullptr;
5056 }
5057 mainEntryDevice.reset();
5058
5059 QDomElement root = doc.documentElement();
5060 if (root.tagName() != QLatin1String("OkularArchive")) {
5061 return nullptr;
5062 }
5063
5064 QString documentFileName;
5065 QString metadataFileName;
5066 QDomElement el = root.firstChild().toElement();
5067 for (; !el.isNull(); el = el.nextSibling().toElement()) {
5068 if (el.tagName() == QLatin1String("Files")) {
5069 QDomElement fileEl = el.firstChild().toElement();
5070 for (; !fileEl.isNull(); fileEl = fileEl.nextSibling().toElement()) {
5071 if (fileEl.tagName() == QLatin1String("DocumentFileName")) {
5072 documentFileName = fileEl.text();
5073 } else if (fileEl.tagName() == QLatin1String("MetadataFileName")) {
5074 metadataFileName = fileEl.text();
5075 }
5076 }
5077 }
5078 }
5079 if (documentFileName.isEmpty()) {
5080 return nullptr;
5081 }
5082
5083 const KArchiveEntry *docEntry = mainDir->entry(documentFileName);
5084 if (!docEntry || !docEntry->isFile()) {
5085 return nullptr;
5086 }
5087
5088 std::unique_ptr<ArchiveData> archiveData(new ArchiveData());
5089 const int dotPos = documentFileName.indexOf(QLatin1Char('.'));
5090 if (dotPos != -1) {
5091 archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos));
5092 }
5093 if (!archiveData->document.open()) {
5094 return nullptr;
5095 }
5096
5097 archiveData->originalFileName = documentFileName;
5098
5099 {
5100 std::unique_ptr<QIODevice> docEntryDevice(static_cast<const KZipFileEntry *>(docEntry)->createDevice());
5101 copyQIODevice(docEntryDevice.get(), &archiveData->document);
5102 archiveData->document.close();
5103 }
5104
5105 const KArchiveEntry *metadataEntry = mainDir->entry(metadataFileName);
5106 if (metadataEntry && metadataEntry->isFile()) {
5107 std::unique_ptr<QIODevice> metadataEntryDevice(static_cast<const KZipFileEntry *>(metadataEntry)->createDevice());
5108 archiveData->metadataFile.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.xml"));
5109 if (archiveData->metadataFile.open()) {
5110 copyQIODevice(metadataEntryDevice.get(), &archiveData->metadataFile);
5111 archiveData->metadataFile.close();
5112 }
5113 }
5114
5115 return archiveData.release();
5116}
5117
5118Document::OpenResult Document::openDocumentArchive(const QString &docFile, const QUrl &url, const QString &password)
5119{
5120 d->m_archiveData = DocumentPrivate::unpackDocumentArchive(docFile);
5121 if (!d->m_archiveData) {
5122 return OpenError;
5123 }
5124
5125 const QString tempFileName = d->m_archiveData->document.fileName();
5126 QMimeDatabase db;
5127 const QMimeType docMime = db.mimeTypeForFile(tempFileName, QMimeDatabase::MatchExtension);
5128 const OpenResult ret = openDocument(tempFileName, url, docMime, password);
5129
5130 if (ret != OpenSuccess) {
5131 delete d->m_archiveData;
5132 d->m_archiveData = nullptr;
5133 }
5134
5135 return ret;
5136}
5137
5139{
5140 if (!d->m_generator) {
5141 return false;
5142 }
5143
5144 /* If we opened an archive, use the name of original file (eg foo.pdf)
5145 * instead of the archive's one (eg foo.okular) */
5146 QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName();
5147 if (docFileName == QLatin1String("-")) {
5148 return false;
5149 }
5150
5151 QString docPath = d->m_docFileName;
5152 const QFileInfo fi(docPath);
5153 if (fi.isSymLink()) {
5154 docPath = fi.symLinkTarget();
5155 }
5156
5157 KZip okularArchive(fileName);
5158 if (!okularArchive.open(QIODevice::WriteOnly)) {
5159 return false;
5160 }
5161
5162 const KUser user;
5163#ifndef Q_OS_WIN
5164 const KUserGroup userGroup(user.groupId());
5165#else
5166 const KUserGroup userGroup(QStringLiteral(""));
5167#endif
5168
5169 QDomDocument contentDoc(QStringLiteral("OkularArchive"));
5170 QDomProcessingInstruction xmlPi = contentDoc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
5171 contentDoc.appendChild(xmlPi);
5172 QDomElement root = contentDoc.createElement(QStringLiteral("OkularArchive"));
5173 contentDoc.appendChild(root);
5174
5175 QDomElement filesNode = contentDoc.createElement(QStringLiteral("Files"));
5176 root.appendChild(filesNode);
5177
5178 QDomElement fileNameNode = contentDoc.createElement(QStringLiteral("DocumentFileName"));
5179 filesNode.appendChild(fileNameNode);
5180 fileNameNode.appendChild(contentDoc.createTextNode(docFileName));
5181
5182 QDomElement metadataFileNameNode = contentDoc.createElement(QStringLiteral("MetadataFileName"));
5183 filesNode.appendChild(metadataFileNameNode);
5184 metadataFileNameNode.appendChild(contentDoc.createTextNode(QStringLiteral("metadata.xml")));
5185
5186 // If the generator can save annotations natively, do it
5187 QTemporaryFile modifiedFile;
5188 bool annotationsSavedNatively = false;
5189 bool formsSavedNatively = false;
5190 if (d->canAddAnnotationsNatively() || canSaveChanges(SaveFormsCapability)) {
5191 if (!modifiedFile.open()) {
5192 return false;
5193 }
5194
5195 const QString modifiedFileName = modifiedFile.fileName();
5196
5197 modifiedFile.close(); // We're only interested in the file name
5198
5199 QString errorText;
5200 if (saveChanges(modifiedFileName, &errorText)) {
5201 docPath = modifiedFileName; // Save this instead of the original file
5202 annotationsSavedNatively = d->canAddAnnotationsNatively();
5203 formsSavedNatively = canSaveChanges(SaveFormsCapability);
5204 } else {
5205 qCWarning(OkularCoreDebug) << "saveChanges failed: " << errorText;
5206 qCDebug(OkularCoreDebug) << "Falling back to saving a copy of the original file";
5207 }
5208 }
5209
5210 PageItems saveWhat = None;
5211 if (!annotationsSavedNatively) {
5212 saveWhat |= AnnotationPageItems;
5213 }
5214 if (!formsSavedNatively) {
5215 saveWhat |= FormFieldPageItems;
5216 }
5217
5218 QTemporaryFile metadataFile;
5219 if (!d->savePageDocumentInfo(&metadataFile, saveWhat)) {
5220 return false;
5221 }
5222
5223 const QByteArray contentDocXml = contentDoc.toByteArray();
5224 const mode_t perm = 0100644;
5225 okularArchive.writeFile(QStringLiteral("content.xml"), contentDocXml, perm, user.loginName(), userGroup.name());
5226
5227 okularArchive.addLocalFile(docPath, docFileName);
5228 okularArchive.addLocalFile(metadataFile.fileName(), QStringLiteral("metadata.xml"));
5229
5230 if (!okularArchive.close()) {
5231 return false;
5232 }
5233
5234 return true;
5235}
5236
5238{
5239 if (!d->m_archiveData) {
5240 return false;
5241 }
5242
5243 // Remove existing file, if present (QFile::copy doesn't overwrite by itself)
5244 QFile::remove(destFileName);
5245
5246 return d->m_archiveData->document.copy(destFileName);
5247}
5248
5250{
5251 double width, height;
5252 int landscape, portrait;
5254
5255 // if some pages are landscape and others are not, the most common wins, as
5256 // QPrinter does not accept a per-page setting
5257 landscape = 0;
5258 portrait = 0;
5259 for (uint i = 0; i < pages(); i++) {
5260 currentPage = page(i);
5261 width = currentPage->width();
5262 height = currentPage->height();
5263 if (currentPage->orientation() == Okular::Rotation90 || currentPage->orientation() == Okular::Rotation270) {
5264 std::swap(width, height);
5265 }
5266 if (width > height) {
5267 landscape++;
5268 } else {
5269 portrait++;
5270 }
5271 }
5272 return (landscape > portrait) ? QPageLayout::Landscape : QPageLayout::Portrait;
5273}
5274
5276{
5277 d->m_annotationEditingEnabled = enable;
5278 foreachObserver(notifySetup(d->m_pagesVector, 0));
5279}
5280
5281void Document::walletDataForFile(const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey) const
5282{
5283 if (d->m_generator) {
5284 d->m_generator->walletDataForFile(fileName, walletName, walletFolder, walletKey);
5285 } else if (d->m_walletGenerator) {
5286 d->m_walletGenerator->walletDataForFile(fileName, walletName, walletFolder, walletKey);
5287 }
5288}
5289
5291{
5292 return d->m_docdataMigrationNeeded;
5293}
5294
5296{
5297 if (d->m_docdataMigrationNeeded) {
5298 d->m_docdataMigrationNeeded = false;
5299 foreachObserver(notifySetup(d->m_pagesVector, 0));
5300 }
5301}
5302
5304{
5305 return d->m_generator ? d->m_generator->layersModel() : nullptr;
5306}
5307
5309{
5310 return d->m_openError;
5311}
5312
5313QByteArray Document::requestSignedRevisionData(const Okular::SignatureInfo &info)
5314{
5315 QFile f(d->m_docFileName);
5316 if (!f.open(QIODevice::ReadOnly)) {
5317 Q_EMIT error(i18n("Could not open '%1'. File does not exist", d->m_docFileName), -1);
5318 return {};
5319 }
5320
5321 const QList<qint64> byteRange = info.signedRangeBounds();
5322 f.seek(byteRange.first());
5323 QByteArray data = f.read(byteRange.last() - byteRange.first());
5324 f.close();
5325
5326 return data;
5327}
5328
5329void Document::refreshPixmaps(int pageNumber)
5330{
5331 d->refreshPixmaps(pageNumber);
5332}
5333
5334void DocumentPrivate::executeScript(const QString &function)
5335{
5336 if (!m_scripter) {
5337 m_scripter = new Scripter(this);
5338 }
5339 m_scripter->execute(JavaScript, function);
5340}
5341
5342void DocumentPrivate::requestDone(PixmapRequest *req)
5343{
5344 if (!req) {
5345 return;
5346 }
5347
5348 if (!m_generator || m_closingLoop) {
5349 m_pixmapRequestsMutex.lock();
5350 m_executingPixmapRequests.remove(req);
5351 m_pixmapRequestsMutex.unlock();
5352 delete req;
5353 if (m_closingLoop) {
5354 m_closingLoop->exit();
5355 }
5356 return;
5357 }
5358
5359#ifndef NDEBUG
5360 if (!m_generator->canGeneratePixmap()) {
5361 qCDebug(OkularCoreDebug) << "requestDone with generator not in READY state.";
5362 }
5363#endif
5364
5365 if (!req->shouldAbortRender()) {
5366 // [MEM] 1.1 find and remove a previous entry for the same page and id
5367 std::list<AllocatedPixmap *>::iterator aIt = m_allocatedPixmaps.begin();
5368 std::list<AllocatedPixmap *>::iterator aEnd = m_allocatedPixmaps.end();
5369 for (; aIt != aEnd; ++aIt) {
5370 if ((*aIt)->page == req->pageNumber() && (*aIt)->observer == req->observer()) {
5371 AllocatedPixmap *p = *aIt;
5372 m_allocatedPixmaps.erase(aIt);
5373 m_allocatedPixmapsTotalMemory -= p->memory;
5374 delete p;
5375 break;
5376 }
5377 }
5378
5379 DocumentObserver *observer = req->observer();
5380 if (m_observers.contains(observer)) {
5381 // [MEM] 1.2 append memory allocation descriptor to the FIFO
5382 qulonglong memoryBytes = 0;
5383 const TilesManager *tm = req->d->tilesManager();
5384 if (tm) {
5385 memoryBytes = tm->totalMemory();
5386 } else {
5387 memoryBytes = 4 * req->width() * req->height();
5388 }
5389
5390 AllocatedPixmap *memoryPage = new AllocatedPixmap(req->observer(), req->pageNumber(), memoryBytes);
5391 m_allocatedPixmaps.push_back(memoryPage);
5392 m_allocatedPixmapsTotalMemory += memoryBytes;
5393
5394 // 2. notify an observer that its pixmap changed
5396 }
5397#ifndef NDEBUG
5398 else {
5399 qCWarning(OkularCoreDebug) << "Receiving a done request for the defunct observer" << observer;
5400 }
5401#endif
5402 }
5403
5404 // 3. delete request
5405 m_pixmapRequestsMutex.lock();
5406 m_executingPixmapRequests.remove(req);
5407 m_pixmapRequestsMutex.unlock();
5408 delete req;
5409
5410 // 4. start a new generation if some is pending
5411 m_pixmapRequestsMutex.lock();
5412 bool hasPixmaps = !m_pixmapRequestsStack.empty();
5413 m_pixmapRequestsMutex.unlock();
5414 if (hasPixmaps) {
5415 sendGeneratorPixmapRequest();
5416 }
5417}
5418
5419void DocumentPrivate::setPageBoundingBox(int page, const NormalizedRect &boundingBox)
5420{
5421 Page *kp = m_pagesVector[page];
5422 if (!m_generator || !kp) {
5423 return;
5424 }
5425
5426 if (kp->boundingBox() == boundingBox) {
5427 return;
5428 }
5429 kp->setBoundingBox(boundingBox);
5430
5431 // notify observers about the change
5432 foreachObserverD(notifyPageChanged(page, DocumentObserver::BoundingBox));
5433
5434 // TODO: For generators that generate the bbox by pixmap scanning, if the first generated pixmap is very small, the bounding box will forever be inaccurate.
5435 // TODO: Crop computation should also consider annotations, actions, etc. to make sure they're not cropped away.
5436 // TODO: Help compute bounding box for generators that create a QPixmap without a QImage, like text and plucker.
5437 // TODO: Don't compute the bounding box if no one needs it (e.g., Trim Borders is off).
5438}
5439
5440void DocumentPrivate::calculateMaxTextPages()
5441{
5442 int multipliers = qMax(1, qRound(getTotalMemory() / 536870912.0)); // 512 MB
5443 switch (SettingsCore::memoryLevel()) {
5444 case SettingsCore::EnumMemoryLevel::Low:
5445 m_maxAllocatedTextPages = multipliers * 2;
5446 break;
5447
5448 case SettingsCore::EnumMemoryLevel::Normal:
5449 m_maxAllocatedTextPages = multipliers * 50;
5450 break;
5451
5452 case SettingsCore::EnumMemoryLevel::Aggressive:
5453 m_maxAllocatedTextPages = multipliers * 250;
5454 break;
5455
5456 case SettingsCore::EnumMemoryLevel::Greedy:
5457 m_maxAllocatedTextPages = multipliers * 1250;
5458 break;
5459 }
5460}
5461
5462void DocumentPrivate::textGenerationDone(Page *page)
5463{
5464 if (!m_pageController) {
5465 return;
5466 }
5467
5468 // 1. If we reached the cache limit, delete the first text page from the fifo
5469 if (m_allocatedTextPagesFifo.size() == m_maxAllocatedTextPages) {
5470 int pageToKick = m_allocatedTextPagesFifo.takeFirst();
5471 if (pageToKick != page->number()) // this should never happen but better be safe than sorry
5472 {
5473 m_pagesVector.at(pageToKick)->setTextPage(nullptr); // deletes the textpage
5474 }
5475 }
5476
5477 // 2. Add the page to the fifo of generated text pages
5478 m_allocatedTextPagesFifo.append(page->number());
5479}
5480
5482{
5483 d->setRotationInternal(r, true);
5484}
5485
5486void DocumentPrivate::setRotationInternal(int r, bool notify)
5487{
5488 Rotation rotation = (Rotation)r;
5489 if (!m_generator || (m_rotation == rotation)) {
5490 return;
5491 }
5492
5493 // tell the pages to rotate
5494 QVector<Okular::Page *>::const_iterator pIt = m_pagesVector.constBegin();
5495 QVector<Okular::Page *>::const_iterator pEnd = m_pagesVector.constEnd();
5496 for (; pIt != pEnd; ++pIt) {
5497 (*pIt)->d->rotateAt(rotation);
5498 }
5499 if (notify) {
5500 // notify the generator that the current rotation has changed
5501 m_generator->rotationChanged(rotation, m_rotation);
5502 }
5503 // set the new rotation
5504 m_rotation = rotation;
5505
5506 if (notify) {
5507 foreachObserverD(notifySetup(m_pagesVector, DocumentObserver::NewLayoutForPages));
5508 foreachObserverD(notifyContentsCleared(DocumentObserver::Pixmap | DocumentObserver::Highlights | DocumentObserver::Annotations));
5509 }
5510 qCDebug(OkularCoreDebug) << "Rotated:" << r;
5511}
5512
5514{
5515 if (!d->m_generator || !d->m_generator->hasFeature(Generator::PageSizes)) {
5516 return;
5517 }
5518
5519 if (d->m_pageSizes.isEmpty()) {
5520 d->m_pageSizes = d->m_generator->pageSizes();
5521 }
5522 int sizeid = d->m_pageSizes.indexOf(size);
5523 if (sizeid == -1) {
5524 return;
5525 }
5526
5527 // tell the pages to change size
5528 QVector<Okular::Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
5529 QVector<Okular::Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
5530 for (; pIt != pEnd; ++pIt) {
5531 (*pIt)->d->changeSize(size);
5532 }
5533 // clear 'memory allocation' descriptors
5534 qDeleteAll(d->m_allocatedPixmaps);
5535 d->m_allocatedPixmaps.clear();
5536 d->m_allocatedPixmapsTotalMemory = 0;
5537 // notify the generator that the current page size has changed
5538 d->m_generator->pageSizeChanged(size, d->m_pageSize);
5539 // set the new page size
5540 d->m_pageSize = size;
5541
5542 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::NewLayoutForPages));
5543 foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap | DocumentObserver::Highlights));
5544 qCDebug(OkularCoreDebug) << "New PageSize id:" << sizeid;
5545}
5546
5547/** DocumentViewport **/
5548
5550 : pageNumber(n)
5551{
5552 // default settings
5553 rePos.enabled = false;
5554 rePos.normalizedX = 0.5;
5555 rePos.normalizedY = 0.0;
5556 rePos.pos = Center;
5557 autoFit.enabled = false;
5558 autoFit.width = false;
5559 autoFit.height = false;
5560}
5561
5563 : pageNumber(-1)
5564{
5565 // default settings (maybe overridden below)
5566 rePos.enabled = false;
5567 rePos.normalizedX = 0.5;
5568 rePos.normalizedY = 0.0;
5569 rePos.pos = Center;
5570 autoFit.enabled = false;
5571 autoFit.width = false;
5572 autoFit.height = false;
5573
5574 // check for string presence
5575 if (xmlDesc.isEmpty()) {
5576 return;
5577 }
5578
5579 // decode the string
5580 bool ok;
5581 int field = 0;
5582 QString token = xmlDesc.section(QLatin1Char(';'), field, field);
5583 while (!token.isEmpty()) {
5584 // decode the current token
5585 if (field == 0) {
5586 pageNumber = token.toInt(&ok);
5587 if (!ok) {
5588 return;
5589 }
5590 } else if (token.startsWith(QLatin1String("C1"))) {
5591 rePos.enabled = true;
5592 rePos.normalizedX = token.section(QLatin1Char(':'), 1, 1).toDouble();
5593 rePos.normalizedY = token.section(QLatin1Char(':'), 2, 2).toDouble();
5594 rePos.pos = Center;
5595 } else if (token.startsWith(QLatin1String("C2"))) {
5596 rePos.enabled = true;
5597 rePos.normalizedX = token.section(QLatin1Char(':'), 1, 1).toDouble();
5598 rePos.normalizedY = token.section(QLatin1Char(':'), 2, 2).toDouble();
5599 if (token.section(QLatin1Char(':'), 3, 3).toInt() == 1) {
5600 rePos.pos = Center;
5601 } else {
5602 rePos.pos = TopLeft;
5603 }
5604 } else if (token.startsWith(QLatin1String("AF1"))) {
5605 autoFit.enabled = true;
5606 autoFit.width = token.section(QLatin1Char(':'), 1, 1) == QLatin1String("T");
5607 autoFit.height = token.section(QLatin1Char(':'), 2, 2) == QLatin1String("T");
5608 }
5609 // proceed tokenizing string
5610 field++;
5611 token = xmlDesc.section(QLatin1Char(';'), field, field);
5612 }
5613}
5614
5616{
5617 // start string with page number
5619 // if has center coordinates, save them on string
5620 if (rePos.enabled) {
5621 s += QStringLiteral(";C2:") + QString::number(rePos.normalizedX) + QLatin1Char(':') + QString::number(rePos.normalizedY) + QLatin1Char(':') + QString::number(rePos.pos);
5622 }
5623 // if has autofit enabled, save its state on string
5624 if (autoFit.enabled) {
5625 s += QStringLiteral(";AF1:") + (autoFit.width ? QLatin1Char('T') : QLatin1Char('F')) + QLatin1Char(':') + (autoFit.height ? QLatin1Char('T') : QLatin1Char('F'));
5626 }
5627 return s;
5628}
5629
5631{
5632 return pageNumber >= 0;
5633}
5634
5636{
5637 bool equal = (pageNumber == other.pageNumber) && (rePos.enabled == other.rePos.enabled) && (autoFit.enabled == other.autoFit.enabled);
5638 if (!equal) {
5639 return false;
5640 }
5641 if (rePos.enabled && ((rePos.normalizedX != other.rePos.normalizedX) || (rePos.normalizedY != other.rePos.normalizedY) || rePos.pos != other.rePos.pos)) {
5642 return false;
5643 }
5644 if (autoFit.enabled && ((autoFit.width != other.autoFit.width) || (autoFit.height != other.autoFit.height))) {
5645 return false;
5646 }
5647 return true;
5648}
5649
5650bool DocumentViewport::operator<(const DocumentViewport &other) const
5651{
5652 // TODO: Check autoFit and Position
5653
5654 if (pageNumber != other.pageNumber) {
5655 return pageNumber < other.pageNumber;
5656 }
5657
5658 if (!rePos.enabled && other.rePos.enabled) {
5659 return true;
5660 }
5661
5662 if (!other.rePos.enabled) {
5663 return false;
5664 }
5665
5666 if (rePos.normalizedY != other.rePos.normalizedY) {
5667 return rePos.normalizedY < other.rePos.normalizedY;
5668 }
5669
5670 return rePos.normalizedX < other.rePos.normalizedX;
5671}
5672
5673/** DocumentInfo **/
5674
5676 : d(new DocumentInfoPrivate())
5677{
5678}
5679
5681 : d(new DocumentInfoPrivate())
5682{
5683 *this = info;
5684}
5685
5686DocumentInfo &DocumentInfo::operator=(const DocumentInfo &info)
5687{
5688 if (this != &info) {
5689 d->values = info.d->values;
5690 d->titles = info.d->titles;
5691 }
5692 return *this;
5693}
5694
5695DocumentInfo::~DocumentInfo()
5696{
5697 delete d;
5698}
5699
5700void DocumentInfo::set(const QString &key, const QString &value, const QString &title)
5701{
5702 d->values[key] = value;
5703 d->titles[key] = title;
5704}
5705
5706void DocumentInfo::set(Key key, const QString &value)
5707{
5708 d->values[getKeyString(key)] = value;
5709}
5710
5712{
5713 return d->values.keys();
5714}
5715
5717{
5718 return get(getKeyString(key));
5719}
5720
5722{
5723 return d->values[key];
5724}
5725
5727{
5728 switch (key) {
5729 case Title:
5730 return QStringLiteral("title");
5731 break;
5732 case Subject:
5733 return QStringLiteral("subject");
5734 break;
5735 case Description:
5736 return QStringLiteral("description");
5737 break;
5738 case Author:
5739 return QStringLiteral("author");
5740 break;
5741 case Creator:
5742 return QStringLiteral("creator");
5743 break;
5744 case Producer:
5745 return QStringLiteral("producer");
5746 break;
5747 case Copyright:
5748 return QStringLiteral("copyright");
5749 break;
5750 case Pages:
5751 return QStringLiteral("pages");
5752 break;
5753 case CreationDate:
5754 return QStringLiteral("creationDate");
5755 break;
5756 case ModificationDate:
5757 return QStringLiteral("modificationDate");
5758 break;
5759 case MimeType:
5760 return QStringLiteral("mimeType");
5761 break;
5762 case Category:
5763 return QStringLiteral("category");
5764 break;
5765 case Keywords:
5766 return QStringLiteral("keywords");
5767 break;
5768 case FilePath:
5769 return QStringLiteral("filePath");
5770 break;
5771 case DocumentSize:
5772 return QStringLiteral("documentSize");
5773 break;
5774 case PagesSize:
5775 return QStringLiteral("pageSize");
5776 break;
5777 default:
5778 qCWarning(OkularCoreDebug) << "Unknown" << key;
5779 return QString();
5780 break;
5781 }
5782}
5783
5785{
5786 if (key == QLatin1String("title")) {
5787 return Title;
5788 } else if (key == QLatin1String("subject")) {
5789 return Subject;
5790 } else if (key == QLatin1String("description")) {
5791 return Description;
5792 } else if (key == QLatin1String("author")) {
5793 return Author;
5794 } else if (key == QLatin1String("creator")) {
5795 return Creator;
5796 } else if (key == QLatin1String("producer")) {
5797 return Producer;
5798 } else if (key == QLatin1String("copyright")) {
5799 return Copyright;
5800 } else if (key == QLatin1String("pages")) {
5801 return Pages;
5802 } else if (key == QLatin1String("creationDate")) {
5803 return CreationDate;
5804 } else if (key == QLatin1String("modificationDate")) {
5805 return ModificationDate;
5806 } else if (key == QLatin1String("mimeType")) {
5807 return MimeType;
5808 } else if (key == QLatin1String("category")) {
5809 return Category;
5810 } else if (key == QLatin1String("keywords")) {
5811 return Keywords;
5812 } else if (key == QLatin1String("filePath")) {
5813 return FilePath;
5814 } else if (key == QLatin1String("documentSize")) {
5815 return DocumentSize;
5816 } else if (key == QLatin1String("pageSize")) {
5817 return PagesSize;
5818 } else {
5819 return Invalid;
5820 }
5821}
5822
5824{
5825 switch (key) {
5826 case Title:
5827 return i18n("Title");
5828 break;
5829 case Subject:
5830 return i18n("Subject");
5831 break;
5832 case Description:
5833 return i18n("Description");
5834 break;
5835 case Author:
5836 return i18n("Author");
5837 break;
5838 case Creator:
5839 return i18n("Creator");
5840 break;
5841 case Producer:
5842 return i18n("Producer");
5843 break;
5844 case Copyright:
5845 return i18n("Copyright");
5846 break;
5847 case Pages:
5848 return i18n("Pages");
5849 break;
5850 case CreationDate:
5851 return i18n("Created");
5852 break;
5853 case ModificationDate:
5854 return i18n("Modified");
5855 break;
5856 case MimeType:
5857 return i18n("MIME Type");
5858 break;
5859 case Category:
5860 return i18n("Category");
5861 break;
5862 case Keywords:
5863 return i18n("Keywords");
5864 break;
5865 case FilePath:
5866 return i18n("File Path");
5867 break;
5868 case DocumentSize:
5869 return i18n("File Size");
5870 break;
5871 case PagesSize:
5872 return i18n("Page Size");
5873 break;
5874 default:
5875 return QString();
5876 break;
5877 }
5878}
5879
5881{
5882 QString title = getKeyTitle(getKeyFromString(key));
5883 if (title.isEmpty()) {
5884 title = d->titles[key];
5885 }
5886 return title;
5887}
5888
5889/** DocumentSynopsis **/
5890
5892 : QDomDocument(QStringLiteral("DocumentSynopsis"))
5893{
5894 // void implementation, only subclassed for naming
5895}
5896
5898 : QDomDocument(document)
5899{
5900}
5901
5902/** EmbeddedFile **/
5903
5907
5911
5913 : pageNumber(page)
5914 , rect(rectangle)
5915{
5916}
5917
5918/** NewSignatureData **/
5919
5920struct Okular::NewSignatureDataPrivate {
5921 NewSignatureDataPrivate() = default;
5922
5923 QString certNickname;
5924 QString certSubjectCommonName;
5925 QString password;
5926 QString documentPassword;
5927 QString location;
5928 QString reason;
5929 QString backgroundImagePath;
5930 int page = -1;
5931 NormalizedRect boundingRectangle;
5932};
5933
5934NewSignatureData::NewSignatureData()
5935 : d(new NewSignatureDataPrivate())
5936{
5937}
5938
5939NewSignatureData::~NewSignatureData()
5940{
5941 delete d;
5942}
5943
5944QString NewSignatureData::certNickname() const
5945{
5946 return d->certNickname;
5947}
5948
5949void NewSignatureData::setCertNickname(const QString &certNickname)
5950{
5951 d->certNickname = certNickname;
5952}
5953
5954QString NewSignatureData::certSubjectCommonName() const
5955{
5956 return d->certSubjectCommonName;
5957}
5958
5959void NewSignatureData::setCertSubjectCommonName(const QString &certSubjectCommonName)
5960{
5961 d->certSubjectCommonName = certSubjectCommonName;
5962}
5963
5964QString NewSignatureData::password() const
5965{
5966 return d->password;
5967}
5968
5969void NewSignatureData::setPassword(const QString &password)
5970{
5971 d->password = password;
5972}
5973
5974int NewSignatureData::page() const
5975{
5976 return d->page;
5977}
5978
5979void NewSignatureData::setPage(int page)
5980{
5981 d->page = page;
5982}
5983
5984NormalizedRect NewSignatureData::boundingRectangle() const
5985{
5986 return d->boundingRectangle;
5987}
5988
5989void NewSignatureData::setBoundingRectangle(const NormalizedRect &rect)
5990{
5991 d->boundingRectangle = rect;
5992}
5993
5995{
5996 return d->documentPassword;
5997}
5998
6000{
6001 d->documentPassword = password;
6002}
6003
6005{
6006 return d->location;
6007}
6008
6010{
6011 d->location = location;
6012}
6013
6015{
6016 return d->reason;
6017}
6018
6020{
6021 d->reason = reason;
6022}
6023
6025{
6026 return d->backgroundImagePath;
6027}
6028
6030{
6031 d->backgroundImagePath = path;
6032}
6033
6034#undef foreachObserver
6035#undef foreachObserverD
6036
6037#include "document.moc"
6038
6039/* kate: replace-tabs on; indent-width 4; */
QStringList entries() const
const KArchiveEntry * entry(const QString &name) const
virtual bool isDirectory() const
virtual bool isFile() const
virtual bool close()
bool addLocalFile(const QString &fileName, const QString &destName)
virtual bool open(QIODevice::OpenMode mode)
bool writeFile(const QString &name, QByteArrayView data, mode_t perm=0100644, const QString &user=QString(), const QString &group=QString(), const QDateTime &atime=QDateTime(), const QDateTime &mtime=QDateTime(), const QDateTime &ctime=QDateTime())
static Q_INVOKABLE bool authorize(const QString &action)
void settingsChanged(const QString &dialogName)
QString formatByteSize(double size, int precision=1, KFormat::BinaryUnitDialect dialect=KFormat::DefaultBinaryDialect, KFormat::BinarySizeUnits units=KFormat::DefaultBinaryUnits) const
void start() override
static bool isExecutableFile(const QUrl &url, const QString &mimetypeName)
int error() const
void result(KJob *job)
void setUiDelegate(KJobUiDelegate *delegate)
void setCurrentPage(const QModelIndex &index)
QAbstractItemModel * model() const
QString pluginId() const
QJsonObject rawData() const
QString fileName() const
static QList< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={}, KPluginMetaDataOptions options={})
bool isValid() const
int startDetached()
QString name() const
KGroupId groupId() const
QString loginName() const
Encapsulates data that describes an action.
Definition action.h:41
QVector< Action * > nextActions() const
Returns the next actions to be executed after.
Definition action.cpp:66
@ Execute
Execute a command or external application.
Definition action.h:48
@ Goto
Goto a given page or external document.
Definition action.h:47
@ DocAction
Start a custom action.
Definition action.h:50
@ BackendOpaque
Calls back to the backend with the action.
Definition action.h:55
@ Movie
Play a movie.
Definition action.h:52
@ Browse
Browse a given website.
Definition action.h:49
@ Script
Executes a Script code.
Definition action.h:53
@ Rendition
Play a movie and/or execute a Script code.
Definition action.h:54
@ Sound
Play a sound.
Definition action.h:51
virtual ActionType actionType() const =0
Returns the type of the action.
Native annotation interface.
virtual bool supports(Capability capability) const =0
Query for the supported capabilities.
virtual void notifyRemoval(Annotation *annotation, int page)=0
Called when an existing annotation at a given page is removed.
virtual void notifyAddition(Annotation *annotation, int page)=0
Called when a new annotation is added to a page.
virtual void notifyModification(const Annotation *annotation, int page, bool appearanceChanged)=0
Called after an existing annotation at a given page is modified.
@ Modification
Generator can edit native annotations.
@ Removal
Generator can remove native annotations.
@ Addition
Generator can create native annotations.
Annotation struct holds properties shared by all annotations.
Definition annotations.h:96
QString contents() const
Returns the contents of the annotation.
int flags() const
Returns the flags of the annotation.
void setContents(const QString &contents)
Sets the contents of the annotation.
@ DenyDelete
Cannot be deleted.
@ ExternallyDrawn
Is drawn externally (by the generator which provided it)
@ BeingMoved
Is being moved (mouse drag and drop). If ExternallyDrawn, the generator must not draw it.
@ External
Is stored external.
@ DenyWrite
Cannot be changed.
@ BeingResized
Is being resized (mouse drag and drop). If ExternallyDrawn, the generator must not draw it.
QDomNode getAnnotationPropertiesDomNode() const
Retrieve the QDomNode representing this annotation's properties.
@ AHighlight
A highlight annotation.
@ AGeom
A geometrical annotation.
@ AText
A textual annotation.
@ AInk
An ink annotation.
@ ALine
A line annotation.
@ ACaret
A caret annotation.
@ AStamp
A stamp annotation.
virtual SubType subType() const =0
Returns the sub type of the annotation.
void playSound(const Sound *sound, const SoundAction *linksound=nullptr)
Enqueue the specified sound for playing, optionally taking more information about the playing from th...
void stopPlaybacks()
Tell the AudioPlayer to stop all the playbacks.
static AudioPlayer * instance()
Gets the instance of the audio player.
Bookmarks manager utility.
The Browse action browses an url by opening a web browser or email client, depending on the url proto...
Definition action.h:239
QUrl url() const
Returns the url to browse.
Definition action.cpp:239
A helper class to store information about x509 certificate.
Abstract interface for configuration control.
virtual bool reparseConfig()=0
This method is called to tell the generator to re-parse its configuration.
virtual void addPages(KConfigDialog *dialog)=0
This method allows the generator to add custom configuration pages to the config dialog of okular.
The DocumentAction action contains an action that is performed on the current document.
Definition action.h:278
DocumentActionType documentActionType() const
Returns the type of action.
Definition action.cpp:268
@ SaveAs
SaveAs the document.
Definition action.h:297
@ Quit
Quit application.
Definition action.h:290
@ Print
Print the document.
Definition action.h:296
@ PageNext
Jump to next page.
Definition action.h:286
@ Find
Open find dialog.
Definition action.h:293
@ Presentation
Start presentation.
Definition action.h:291
@ PageLast
Jump to last page.
Definition action.h:287
@ PageFirst
Jump to first page.
Definition action.h:284
@ GoToPage
Goto page.
Definition action.h:294
@ Close
Close document.
Definition action.h:295
@ HistoryForward
Go forward in page history.
Definition action.h:289
@ EndPresentation
End presentation.
Definition action.h:292
@ PagePrev
Jump to previous page.
Definition action.h:285
@ HistoryBack
Go back in page history.
Definition action.h:288
The DocumentInfo structure can be filled in by generators to display metadata about the currently ope...
Definition document.h:76
QString get(Key key) const
Returns the value for a given key or an null string when the key doesn't exist.
static Key getKeyFromString(const QString &key)
Returns the Key from a string key.
Key
The list of predefined keys.
Definition document.h:83
@ ModificationDate
The date of last modification of the document.
Definition document.h:93
@ Author
The author of the document.
Definition document.h:87
@ Subject
The subject of the document.
Definition document.h:85
@ Category
The category of the document.
Definition document.h:95
@ Producer
The producer of the document (e.g. some software)
Definition document.h:89
@ Copyright
The copyright of the document.
Definition document.h:90
@ CreationDate
The date of creation of the document.
Definition document.h:92
@ MimeType
The mime type of the document.
Definition document.h:94
@ FilePath
The path of the file.
Definition document.h:97
@ PagesSize
The size of the pages (if all pages have the same size)
Definition document.h:99
@ Keywords
The keywords which describe the content of the document.
Definition document.h:96
@ Creator
The creator of the document (this can be different from the author)
Definition document.h:88
@ Pages
The number of pages of the document.
Definition document.h:91
@ DocumentSize
The size of the document.
Definition document.h:98
@ Description
The description of the document.
Definition document.h:86
@ Invalid
An invalid key.
Definition document.h:101
@ Title
The title of the document.
Definition document.h:84
static QString getKeyString(Key key)
Returns the internal string for the given key.
void set(const QString &key, const QString &value, const QString &title=QString())
Sets a value for a custom key.
QString getKeyTitle(const QString &key) const
Returns the user visible string for the given key Takes into account keys added by the set() that tak...
DocumentInfo()
Creates a new document info.
QStringList keys() const
Returns all the keys present in this DocumentInfo.
Base class for objects being notified when something changes.
Definition observer.h:29
virtual void notifySetup(const QVector< Okular::Page * > &pages, int setupFlags)
This method is called whenever the document is initialized or reconstructed.
Definition observer.cpp:21
@ NewLayoutForPages
All the pages have.
Definition observer.h:58
@ DocumentChanged
The document is a new document.
Definition observer.h:57
@ UrlChanged
The URL has changed.
Definition observer.h:59
virtual void notifyPageChanged(int page, int flags)
This method is called whenever the content on page described by the passed flags has been changed.
Definition observer.cpp:29
virtual void notifyViewportChanged(bool smoothMove)
This method is called whenever the viewport has been changed.
Definition observer.cpp:25
@ Annotations
Annotations have been changed.
Definition observer.h:49
@ TextSelection
Text selection has been changed.
Definition observer.h:48
@ Pixmap
Pixmaps has been changed.
Definition observer.h:45
@ BoundingBox
Bounding boxes have been changed.
Definition observer.h:50
@ Highlights
Highlighting information has been changed.
Definition observer.h:47
virtual bool canUnloadPixmap(int page) const
Returns whether the observer agrees that all pixmaps for the given page can be unloaded to improve me...
Definition observer.cpp:45
A DOM tree that describes the Table of Contents.
Definition document.h:1431
DocumentSynopsis()
Creates a new document synopsis object.
A view on the document.
Definition document.h:1350
int pageNumber
The number of the page nearest the center of the viewport.
Definition document.h:1381
struct Okular::DocumentViewport::@0 rePos
If 'rePos.enabled == true' then this structure contains the viewport center or top left depending on ...
bool operator==(const DocumentViewport &other) const
bool isValid() const
Returns whether the viewport is valid.
DocumentViewport(int number=-1)
Creates a new viewport for the given page number.
struct Okular::DocumentViewport::@1 autoFit
If 'autoFit.enabled == true' then the page must be autofit in the viewport.
@ Center
Relative to the center of the page.
Definition document.h:1387
@ TopLeft
Relative to the top left corner of the page.
Definition document.h:1388
QString toString() const
Returns the viewport as xml description.
The Document.
Definition document.h:192
QStringList supportedMimeTypes() const
Returns the list with the supported MIME types.
void setAnnotationEditingEnabled(bool enable)
Control annotation editing (creation, modification and removal), which is enabled by default.
const QVector< VisiblePageRect * > & visiblePageRects() const
Returns the list of visible page rectangles.
void docdataMigrationDone()
Delete annotations and form data from the docdata folder.
void fontReadingEnded()
Reports that the reading of the fonts in the document is finished.
bool isDocdataMigrationNeeded() const
Since version 0.21, okular does not allow editing annotations and form data if they are stored in the...
const Page * page(int number) const
Returns the page object for the given page number or 0 if the number is out of range.
QByteArray requestSignedRevisionData(const Okular::SignatureInfo &info)
Returns the part of document covered by the given signature info.
BookmarkManager * bookmarkManager() const
Returns the bookmark manager of the document.
int configurableGenerators() const
Returns the number of generators that have a configuration widget.
void error(const QString &text, int duration)
This signal is emitted whenever an error occurred.
void unregisterView(View *view)
Unregister the specified view from the current document.
void linkEndPresentation()
This signal is emitted whenever an action requests an end presentation operation.
void cancelSearch()
Cancels the current search.
bool exportToText(const QString &fileName) const
Exports the document as ASCII text and saves it under fileName.
void processSourceReference(const SourceReference *reference)
Processes/Executes the given source reference.
bool canSaveChanges() const
Returns whether the changes to the document (modified annotations, values in form fields,...
PageSize::List pageSizes() const
Returns the list of supported page sizes or an empty list if this feature is not available.
void stopFontReading()
Force the termination of the reading of the information about the fonts in the document,...
void canUndoChanged(bool undoAvailable)
This signal is emitted whenever the availability of the undo function changes.
bool canSign() const
Whether the current document can perform digital signing.
void undo()
Undo last edit command.
Rotation rotation() const
Returns the current rotation of the document.
void removeObserver(DocumentObserver *observer)
Unregisters the given observer for the document.
void editFormText(int pageNumber, Okular::FormFieldText *form, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
Edit the text contents of the specified form on page page to be newContents.
void setRotation(int rotation)
This slot is called whenever the user changes the rotation of the document.
void reloadDocument() const
Reloads the pixmaps for whole document.
PrintingType printingSupport() const
Returns what sort of printing the document supports: Native, Postscript, None.
bool supportsSearching() const
Returns whether the document supports searching.
static QString printErrorString(PrintError error)
QPageLayout::Orientation orientation() const
Returns the orientation of the document (for printing purposes).
@ RemoveAllPrevious
Remove all the previous requests, even for non requested page pixmaps.
Definition document.h:475
@ NoOption
No options.
Definition document.h:474
void requestTextPage(uint pageNumber)
Sends a request for text page generation for the given page pageNumber.
void continueSearch(int searchID)
Continues the search for the given searchID.
void addObserver(DocumentObserver *observer)
Registers a new observer for the document.
void fillConfigDialog(KConfigDialog *dialog)
Fill the KConfigDialog dialog with the setting pages of the generators.
bool swapBackingFile(const QString &newFileName, const QUrl &url)
Reload the document from a new location, without any visible effect to the user.
QList< ExportFormat > exportFormats() const
Returns the list of supported export formats.
bool isAllowed(Permission action) const
Returns whether the given action is allowed in the document.
void processFormMouseUpScripAction(const Action *action, Okular::FormField *ff)
Processes the mouse up action on ff.
void linkGoToPage()
This signal is emitted whenever an action requests a goto operation.
bool historyAtEnd() const
Returns whether the document history is at the end.
KXMLGUIClient * guiClient()
Returns the gui client of the generator, if it provides one.
void searchText(int searchID, const QString &text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor &color)
Searches the given text in the document.
void editFormList(int pageNumber, Okular::FormFieldChoice *form, const QList< int > &newChoices)
Edit the selected list entries in form on page page to be newChoices.
void setNextViewport()
Sets the current document viewport to the previous viewport in the viewport history.
void setPageTextSelection(int page, std::unique_ptr< RegularAreaRect > &&rect, const QColor &color)
Clears the text selection highlights for the given page, creates new ones if rect is not nullptr,...
void canRedoChanged(bool redoAvailable)
This signal is emitted whenever the availability of the redo function changes.
bool canRedo() const
Returns true if there is a redo command available; otherwise returns false.
void linkPresentation()
This signal is emitted whenever an action requests a start presentation operation.
DocumentInfo documentInfo() const
Returns the meta data of the document.
void redo()
Redo last undone edit command.
QString openError() const
Returns the reason why the file opening failed, if any.
void removePageAnnotations(int page, const QList< Annotation * > &annotations)
Removes the given annotations from the given page.
QWidget * printConfigurationWidget() const
Returns a custom printer configuration page or 0 if no custom printer configuration page is available...
void setVisiblePageRects(const QVector< VisiblePageRect * > &visiblePageRects, DocumentObserver *excludeObserver=nullptr)
Sets the list of visible page rectangles.
void setPageSize(const Okular::PageSize &size)
This slot is called whenever the user changes the page size of the document.
void linkFind()
This signal is emitted whenever an action requests a find operation.
bool extractArchivedFile(const QString &destFileName)
Extract the document file from the current archive.
void modifyPageAnnotationProperties(int page, Annotation *annotation)
Modifies the given annotation on the given page.
void processFormatAction(const Action *action, Okular::FormFieldText *fft)
Processes the given format action on fft.
void aboutToClose()
This signal is emitted whenever the document is about to close.
CertificateStore * certificateStore() const
Returns the generator's certificate store (if any)
void gotFont(const Okular::FontInfo &font)
Emitted when a new font is found during the reading of the fonts of the document.
const DocumentViewport & viewport() const
Returns the current viewport of the document.
QList< int > bookmarkedPageList() const
Returns a list of the bookmarked.pages.
void processFocusAction(const Action *action, Okular::FormField *field)
Processes the given focus action on the field.
void requestPrint()
This signal is emitted whenever an action requests a document print operation.
void close()
This signal is emitted whenever an action requests a document close operation.
void walletDataForFile(const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey) const
Returns which wallet data to use to read/write the password for the given fileName.
QUrl currentDocument() const
Returns the url of the currently opened document.
void sourceReferenceActivated(const QString &absFileName, int line, int col, bool *handled)
This signal is emitted whenever a source reference with the given parameters has been activated.
SaveCapability
Saving capabilities.
Definition document.h:859
@ SaveAnnotationsCapability
Can save annotation changes.
Definition document.h:861
@ SaveFormsCapability
Can save form changes.
Definition document.h:860
void setViewportPage(int page, DocumentObserver *excludeObserver=nullptr, bool smoothMove=false)
Sets the current document viewport to the given page.
const QList< EmbeddedFile * > * embeddedFiles() const
Returns the list of embedded files or 0 if no embedded files are available.
void addPageAnnotation(int page, Annotation *annotation)
Adds a new annotation to the given page.
OpenResult openDocumentArchive(const QString &docFile, const QUrl &url, const QString &password=QString())
Opens a document archive.
KPluginMetaData generatorInfo() const
Returns the metadata associated with the generator.
QString bookmarkedPageRange() const
Returns the range of the bookmarked.pages.
void requestPixmaps(const QList< PixmapRequest * > &requests)
Sends requests for pixmap generation.
void registerView(View *view)
Register the specified view for the current document.
bool historyAtBegin() const
Returns whether the document history is at the begin.
QByteArray fontData(const FontInfo &font) const
Gets the font data for the given font.
void setNextDocumentDestination(const QString &namedDestination)
Sets the next namedDestination in the viewport history.
void resetSearch(int searchID)
Resets the search for the given searchID.
bool canSwapBackingFile() const
Returns whether the generator supports hot-swapping the current file with another identical file.
void setZoom(int factor, DocumentObserver *excludeObserver=nullptr)
Sets the zoom for the current document.
void requestSaveAs()
This signal is emitted whenever an action requests a document save as operation.
void quit()
This signal is emitted whenever an action requests an application quit operation.
void fontReadingProgress(int page)
Reports the progress when reading the fonts in the document.
OpenResult openDocument(const QString &docFile, const QUrl &url, const QMimeType &mime, const QString &password=QString())
Opens the document.
void editFormButtons(int pageNumber, const QList< Okular::FormFieldButton * > &formButtons, const QList< bool > &newButtonStates)
Set the states of the group of form buttons formButtons on page page to newButtonStates.
bool saveDocumentArchive(const QString &fileName)
Saves a document archive.
void reparseConfig()
Reparses and applies the configuration.
void processValidateAction(const Action *action, Okular::FormFieldText *fft, bool &returnCode)
Processes the given keystroke action on fft.
bool exportTo(const QString &fileName, const ExportFormat &format) const
Exports the document in the given format and saves it under fileName.
@ NoMatchFound
No match was found.
Definition document.h:625
void setViewport(const DocumentViewport &viewport, DocumentObserver *excludeObserver=nullptr, bool smoothMove=false, bool updateHistory=true)
Sets the current document viewport to the given viewport.
void processAction(const Action *action)
Processes the given action.
~Document() override
Destroys the document.
Document::PrintError print(QPrinter &printer)
Prints the document to the given printer.
void translatePageAnnotation(int page, Annotation *annotation, const Okular::NormalizedPoint &delta)
Translates the position of the given annotation on the given page by a distance delta in normalized c...
bool supportsPageSizes() const
Returns whether the document supports the listing of page sizes.
bool sign(const NewSignatureData &data, const QString &newPath)
Digitally sign document.
QVariant metaData(const QString &key, const QVariant &option=QVariant()) const
Returns the meta data for the given key and option or an empty variant if the key doesn't exists.
void processKeystrokeAction(const Action *action, Okular::FormFieldText *fft, const QVariant &newValue)
Processes the given keystroke action on fft.
const SourceReference * dynamicSourceReference(int pageNr, double absX, double absY)
Asks the generator to dynamically generate a SourceReference for a given page number and absolute X a...
void setEditorCommandOverride(const QString &editCmd)
sets the editor command to the command editCmd, as given at the commandline.
void setHistoryClean(bool clean)
Sets the history to be clean.
void searchFinished(int searchID, Okular::Document::SearchStatus endStatus)
Reports that the current search finished.
@ NoPrintError
Printing succeeded.
Definition document.h:755
const DocumentSynopsis * documentSynopsis() const
Returns the table of content of the document or 0 if no table of content is available.
void refreshFormWidget(Okular::FormField *field)
This signal is emitted whenever a FormField was changed programmatically and the according widget sho...
bool canModifyPageAnnotation(const Annotation *annotation) const
Tests if the annotation can be modified.
QSizeF allPagesSize() const
If all pages have the same size this method returns it, if the page sizes differ an empty size object...
void closeDocument()
Closes the document.
bool canUndo() const
Returns true if there is an undo command available; otherwise returns false.
bool saveChanges(const QString &fileName)
Save the document and the optional changes to it to the specified fileName.
QString pageSizeString(int page) const
Returns the size string for the given page or an empty string if the page is out of range.
bool supportsTiles() const
Returns whether the current document supports tiles.
void processKeystrokeCommitAction(const Action *action, Okular::FormFieldText *fft)
Processes the given keystroke action on fft.
bool isOpened() const
Returns whether the document is currently opened.
void setPrevViewport()
Sets the current document viewport to the next viewport in the viewport history.
bool canConfigurePrinter() const
Returns whether the document can configure the printer itself.
void undoHistoryCleanChanged(bool clean)
This signal is emitted whenever the undo history is clean (i.e.
QString editorCommandOverride() const
returns the overriding editor command.
PrintingType
What type of printing a document supports.
Definition document.h:736
@ PostscriptPrinting
Postscript file printing.
Definition document.h:739
@ NativePrinting
Native Cross-Platform Printing.
Definition document.h:738
@ NoPrinting
Printing Not Supported.
Definition document.h:737
void setNextDocumentViewport(const DocumentViewport &viewport)
Sets the next viewport in the viewport history.
bool canExportToText() const
Returns whether the document supports the export to ASCII text.
void processMovieAction(const Okular::MovieAction *action)
This signal is emitted whenever an movie action is triggered and the UI should process it.
uint currentPage() const
Returns the number of the current page.
void processRenditionAction(const Okular::RenditionAction *action)
This signal is emitted whenever an rendition action is triggered and the UI should process it.
OpenResult
Describes the result of an open document operation.
Definition document.h:210
void editPageAnnotationContents(int page, Annotation *annotation, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
Edits the plain text contents of the given annotation on the given page.
void refreshPixmaps(int pageNumber)
Refresh the pixmaps for the given pageNumber.
bool swapBackingFileArchive(const QString &newFileName, const QUrl &url)
Same as swapBackingFile, but newFileName must be a .okular file.
QAbstractItemModel * layersModel() const
Returns the model for rendering layers (NULL if the document has no layers)
bool canRemovePageAnnotation(const Annotation *annotation) const
Tests if the annotation can be removed.
uint pages() const
Returns the number of pages of the document.
void startFontReading()
Starts the reading of the information about the fonts in the document, if available.
bool canProvideFontInformation() const
Whether the current document can provide information about the fonts used in it.
SearchType
Describes the possible search types.
Definition document.h:612
@ PreviousMatch
Search previous match.
Definition document.h:614
@ GoogleAny
Search complete document (any words in google style)
Definition document.h:617
@ AllDocument
Search complete document.
Definition document.h:615
@ GoogleAll
Search complete document (all words in google style)
Definition document.h:616
@ NextMatch
Search next match.
Definition document.h:613
void editFormCombo(int pageNumber, Okular::FormFieldChoice *form, const QString &newText, int newCursorPos, int prevCursorPos, int prevAnchorPos)
Set the active choice in the combo box form on page page to newText The new cursor position (newCurso...
bool supportsPrintToFile() const
Returns whether the document supports printing to both PDF and PS files.
void adjustPageAnnotation(int page, Annotation *annotation, const Okular::NormalizedPoint &delta1, const Okular::NormalizedPoint &delta2)
Adjusts the position of the top-left and bottom-right corners of given annotation on the given page.
void removePageAnnotation(int page, Annotation *annotation)
Removes the given annotation from the given page.
void prepareToModifyAnnotationProperties(Annotation *annotation)
Prepares to modify the properties of the given annotation.
EmbeddedFile()
Creates a new embedded file.
virtual ~EmbeddedFile()
Destroys the embedded file.
The Execute action executes an external application.
Definition action.h:194
QString fileName() const
Returns the file name of the application to execute.
Definition action.cpp:188
QString parameters() const
Returns the parameters of the application to execute.
Definition action.cpp:194
Defines an entry for the export menu.
Definition generator.h:79
A small class that represents the information of a font.
Definition fontinfo.h:25
Interface of a choice form field.
Definition form.h:348
virtual QString editChoice() const
The text entered into an editable combo box choice field.
Definition form.cpp:298
virtual QList< int > currentChoices() const =0
The currently selected choices.
virtual QStringList choices() const =0
The possible choices of the choice field.
Interface of a text form field.
Definition form.h:261
virtual void setAppearanceText(const QString &text)=0
Set the text which should be rendered by the PDF.
virtual void setText(const QString &text)
Sets the new text in the text field.
Definition form.cpp:204
virtual QString text() const =0
The text of text field.
The base interface of a form field.
Definition form.h:40
Action * additionalAction(AdditionalActionType type) const
Returns the additional action of the given type or nullptr if no action has been defined.
Definition form.cpp:93
@ CalculateField
An action to be performed when the field needs to be recalculated.
Definition form.h:143
@ FormatField
An action to be performed before the field is formatted to display its value.
Definition form.h:141
[Abstract Class] The information generator.
Definition generator.h:188
void error(const QString &message, int duration)
This signal should be emitted whenever an error occurred in the generator.
SwapBackingFileResult
Describes the result of an swap file operation.
Definition generator.h:278
DocumentMetaDataKey
Internal document setting.
Definition generator.h:542
@ TextHintingMetaData
Returns (bool)text hinting from Settings (option is not used)
Definition generator.h:546
@ GraphicsAntialiasMetaData
Returns (bool)graphic antialias from Settings (option is not used)
Definition generator.h:545
@ TextAntialiasMetaData
Returns (bool) text antialias from Settings (option is not used)
Definition generator.h:544
@ PaperColorMetaData
Returns (QColor) the paper color if set in Settings or the default color (white) if option is true (o...
Definition generator.h:543
void notice(const QString &message, int duration)
This signal should be emitted whenever the user should be noticed.
@ Threaded
Whether the Generator supports asynchronous generation of pictures or text pages.
Definition generator.h:202
@ PrintToFile
Whether the Generator supports export to PDF & PS through the Print Dialog.
Definition generator.h:209
@ ReadRawData
Whether the Generator can read a document directly from its raw data.
Definition generator.h:204
@ TextExtraction
Whether the Generator can extract text from the document in the form of TextPage's.
Definition generator.h:203
@ TiledRendering
Whether the Generator can render tiles.
Definition generator.h:210
@ SwapBackingFile
Whether the Generator can hot-swap the file it's reading from.
Definition generator.h:211
@ PrintNative
Whether the Generator supports native cross-platform printing (QPainter-based).
Definition generator.h:207
@ FontInfo
Whether the Generator can provide information about the fonts used in the document.
Definition generator.h:205
@ PageSizes
Whether the Generator can change the size of the document pages.
Definition generator.h:206
@ SupportsCancelling
Whether the Generator can cancel requests.
Definition generator.h:212
@ PrintPostscript
Whether the Generator supports postscript-based file printing.
Definition generator.h:208
@ Points
The page size is given in 1/72 inches.
Definition generator.h:369
@ Pixels
The page size is given in screen pixels.
Definition generator.h:370
@ None
The page size is not defined in a physical metric.
Definition generator.h:368
void warning(const QString &message, int duration)
This signal should be emitted whenever the user should be warned.
The Goto action changes the viewport to another page or loads an external document.
Definition action.h:128
bool isExternal() const
Returns whether the goto action points to an external document.
Definition action.cpp:128
QString destinationName() const
Returns the document named destination the goto action points to.
Definition action.cpp:146
QString fileName() const
Returns the filename of the external document.
Definition action.cpp:134
DocumentViewport destViewport() const
Returns the document viewport the goto action points to.
Definition action.cpp:140
Abstract interface for user interface control.
KXMLGUIClient * guiClient()
This method requests the XML GUI Client provided by the interface.
The Movie action executes an operation on a video on activation.
Definition action.h:446
Data needed to create a new signature.
Definition document.h:1538
QString documentPassword() const
void setReason(const QString &reason)
void setDocumentPassword(const QString &password)
void setLocation(const QString &location)
QString reason() const
QString backgroundImagePath() const
void setBackgroundImagePath(const QString &path)
QString location() const
NormalizedPoint is a helper class which stores the coordinates of a normalized point.
Definition area.h:117
A NormalizedRect is a rectangle which can be defined by two NormalizedPoints.
Definition area.h:189
double bottom
The normalized bottom coordinate.
Definition area.h:435
double right
The normalized right coordinate.
Definition area.h:430
double height() const
Definition area.h:412
double left
The normalized left coordinate.
Definition area.h:420
double width() const
Definition area.h:406
QRect geometry(int xScale, int yScale) const
Returns the rectangle mapped to a reference area of xScale x yScale.
Definition area.cpp:232
double top
The normalized top coordinate.
Definition area.h:425
bool isNull() const
Returns whether this normalized rectangle is a null normalized rect.
Definition area.cpp:155
An area with normalized coordinates that contains a reference to an object.
Definition area.h:458
@ Action
An action.
Definition area.h:464
A small class that represents the size of a page.
Definition pagesize.h:24
Collector for all the data belonging to a page.
Definition page.h:48
void deletePixmap(DocumentObserver *observer)
Deletes the pixmap for the given observer.
Definition page.cpp:753
double height() const
Returns the height of the page.
Definition page.cpp:185
bool hasPixmap(DocumentObserver *observer, int width=-1, int height=-1, const NormalizedRect &rect=NormalizedRect()) const
Returns whether the page of size width x height has a pixmap in the region given by rect for the give...
Definition page.cpp:219
double width() const
Returns the width of the page.
Definition page.cpp:180
void setPageSize(DocumentObserver *observer, int width, int height)
Sets the size of the page (in screen pixels) if there is a TilesManager.
Definition page.cpp:248
Rotation rotation() const
Returns the rotation of the page as defined by the user.
Definition page.cpp:170
Describes a pixmap type request.
Definition generator.h:611
bool preload() const
Returns whether the generation request is for a page that is not important i.e.
void setTile(bool tile)
Sets whether the generator should render only the given normalized rect or the entire page.
bool asynchronous() const
Returns whether the generation should be done synchronous or asynchronous.
bool shouldAbortRender() const
Should the request be aborted if possible?
void setPartialUpdatesWanted(bool partialUpdatesWanted)
Sets whether the request should report back updates if possible.
int width() const
Returns the page width of the requested pixmap.
int height() const
Returns the page height of the requested pixmap.
int priority() const
Returns the priority (less it better, 0 is maximum) of the request.
const NormalizedRect & normalizedRect() const
Returns the normalized region of the page to request.
bool partialUpdatesWanted() const
Should the request report back updates if possible?
int pageNumber() const
Returns the page number of the request.
void setNormalizedRect(const NormalizedRect &rect)
Sets the region of the page to request.
Page * page() const
Returns a pointer to the page where the pixmap shall be generated for.
bool isTile() const
Returns whether the generator should render just the region given by normalizedRect() or the entire p...
DocumentObserver * observer() const
Returns the observer of the request.
Abstract interface for advanced printing control.
virtual QWidget * printConfigurationWidget() const =0
Builds and returns a new printing configuration widget.
This is a list of NormalizedRect, to describe an area consisting of multiple rectangles using normali...
Definition area.h:933
The Rendition action executes an operation on a video or executes some JavaScript code on activation.
Definition action.h:500
ScriptType scriptType() const
Returns the type of script.
Definition action.cpp:566
QString script() const
Returns the script code.
Definition action.cpp:572
Abstract interface for saving.
virtual AnnotationProxy * annotationProxy() const =0
Returns the annotation proxy.
virtual bool save(const QString &fileName, SaveOptions options, QString *errorText)=0
Save to the specified fileName with the specified options.
virtual bool supportsOption(SaveOption option) const =0
Query for the supported saving options.
@ SaveChanges
The possibility to save with the current changes to the document.
The Script action executes a Script code.
Definition action.h:400
ScriptType scriptType() const
Returns the type of action.
Definition action.cpp:429
QString script() const
Returns the code.
Definition action.cpp:435
The Sound action plays a sound on activation.
Definition action.h:336
Okular::Sound * sound() const
Returns the sound object which contains the sound data.
Definition action.cpp:382
This class describes the object rectangle for a source reference.
Definition area.h:599
Defines a source reference.
int column() const
Returns the column of the position in the source file.
int row() const
Returns the row of the position in the source file.
QString fileName() const
Returns the filename of the source.
This class represents a rectangular portion of a page.
Definition tile.h:23
NormalizedRect rect() const
Location of the tile.
bool isValid() const
True if the pixmap is available and updated.
static QSizeF realDpi(const QWindow *windowOnScreen)
Return the real DPI of the display containing given window.
Definition utils.cpp:47
View on the document.
Definition view.h:30
Document * viewDocument() const
Return the document which this view is associated to, or null if it is not associated with any docume...
Definition view.cpp:39
VisiblePageRect(int pageNumber=-1, const NormalizedRect &rectangle=NormalizedRect())
Creates a new visible page rectangle.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
Type type(const QSqlDatabase &db)
char * toString(const EngineQuery &query)
KCALUTILS_EXPORT QString mimeType()
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT StatJob * stat(const QUrl &url, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT QUrl upUrl(const QUrl &url)
KIOCORE_EXPORT KJobUiDelegate * createDefaultJobUiDelegate()
KGUIADDONS_EXPORT QWindow * window(QObject *job)
QString expandMacrosShellQuote(const QString &str, const QHash< QChar, QString > &map, QChar c=QLatin1Char('%'))
KIOCORE_EXPORT QStringList list(const QString &fileClass)
KCOREADDONS_EXPORT QStringList splitArgs(const QString &cmd, Options flags=NoOptions, Errors *err=nullptr)
QAction * forward(const QObject *recvr, const char *slot, QObject *parent)
QString name(StandardAction id)
QAction * zoom(const QObject *recvr, const char *slot, QObject *parent)
const QList< QKeySequence > & end()
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
global.h
Definition action.h:17
@ JavaScript
JavaScript code.
Definition global.h:76
Permission
Describes the DRM capabilities.
Definition global.h:24
@ AllowFillForms
Allows to fill the forms in the document.
Definition global.h:29
@ AllowNotes
Allows to add annotations to the document.
Definition global.h:28
Rotation
A rotation.
Definition global.h:46
@ Rotation270
Rotated 2700 degrees clockwise.
Definition global.h:50
@ Rotation90
Rotated 90 degrees clockwise.
Definition global.h:48
@ Rotation0
Not rotated.
Definition global.h:47
@ FromTop
Searching from top of the page, next result is to be found, there was no earlier search result.
Definition global.h:37
@ FromBottom
Searching from bottom of the page, next result is to be found, there was no earlier search result.
Definition global.h:38
@ PreviousResult
Searching for the previous result on the page, earlier result should be located so we search from the...
Definition global.h:40
@ NextResult
Searching for the next result on the page, earlier result should be located so we search from the las...
Definition global.h:39
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
const char * constData() const const
bool isEmpty() const const
qsizetype size() const const
QChar fromLatin1(char c)
QColor fromHsv(int h, int s, int v, int a)
bool openUrl(const QUrl &url)
bool isRelativePath(const QString &path)
bool mkpath(const QString &dirPath) const const
QString tempPath()
QDomElement createElement(const QString &tagName)
QDomProcessingInstruction createProcessingInstruction(const QString &target, const QString &data)
QDomText createTextNode(const QString &value)
QDomElement documentElement() const const
ParseResult setContent(QAnyStringView text, ParseOptions options)
QByteArray toByteArray(int indent) const const
QString attribute(const QString &name, const QString &defValue) const const
bool hasAttribute(const QString &name) const const
void setAttribute(const QString &name, const QString &value)
QString tagName() const const
QString text() const const
QDomNode appendChild(const QDomNode &newChild)
void clear()
QDomNode firstChild() const const
bool isElement() const const
bool isNull() const const
QDomNode nextSibling() const const
QDomDocument ownerDocument() const const
QDomElement toElement() const const
int exec(ProcessEventsFlags flags)
QString decodeName(const QByteArray &localFileName)
QByteArray encodeName(const QString &fileName)
bool exists(const QString &fileName)
bool exists() const const
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
bool remove()
virtual void close() override
virtual bool seek(qint64 pos) override
bool exists() const const
QString symLinkTarget() const const
void restoreOverrideCursor()
void setOverrideCursor(const QCursor &cursor)
const_iterator constBegin() const const
const_iterator constEnd() const const
const_iterator constFind(const Key &key) const const
iterator end()
iterator find(const Key &key)
bool isEmpty() const const
T value(const Key &key) const const
QByteArray read(qint64 maxSize)
QByteArray readAll()
QJsonValue value(QLatin1StringView key) const const
int toInt(int defaultValue) const const
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
void clear()
const_iterator constBegin() const const
const_iterator constEnd() const const
bool contains(const AT &value) const const
qsizetype count() const const
iterator end()
T & first()
bool isEmpty() const const
T & last()
void push_back(parameter_type value)
qsizetype size() const const
value_type takeFirst()
QLocale system()
iterator begin()
const_iterator constBegin() const const
const_iterator constEnd() const const
bool contains(const Key &key) const const
size_type count() const const
iterator end()
iterator insert(const Key &key, const T &value)
Key key(const T &value, const Key &defaultKey) const const
size_type remove(const Key &key)
QMimeType mimeTypeForData(QIODevice *device) const const
QMimeType mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const const
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
QMimeType mimeTypeForUrl(const QUrl &url) const const
bool inherits(const QString &mimeTypeName) const const
bool isValid() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
virtual bool event(QEvent *e)
PageSizeId id() const const
QString name() const const
int height() const const
int width() const const
int height() const const
int width() const const
void clear()
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
bool isEmpty() const const
T * get() const const
int height() const const
int width() const const
qreal height() const const
bool isValid() const const
qreal width() const const
void push(const T &t)
T & top()
QString findExecutable(const QString &executableName, const QStringList &paths)
QString writableLocation(StandardLocation type)
QString & append(QChar ch)
QString arg(Args &&... args) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromUcs4(const char32_t *unicode, qsizetype size)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
QString & insert(qsizetype position, QChar ch)
bool isEmpty() const const
bool isNull() const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString section(QChar sep, qsizetype start, qsizetype end, SectionFlags flags) const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
double toDouble(bool *ok) const const
int toInt(bool *ok, int base) const const
std::u32string toStdU32String() const const
qulonglong toULongLong(bool *ok, int base) const const
QString trimmed() const const
CaseSensitivity
WaitCursor
SkipEmptyParts
virtual QString fileName() const const override
void timeout()
QTransform inverted(bool *invertible) const const
void canRedoChanged(bool canRedo)
void canUndoChanged(bool canUndo)
void cleanChanged(bool clean)
PreferLocalFile
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
QString host(ComponentFormattingOptions options) const const
bool isEmpty() const const
bool isLocalFile() const const
bool isRelative() const const
bool isValid() const const
QString path(ComponentFormattingOptions options) const const
QUrl resolved(const QUrl &relative) const const
QString scheme() const const
QString toLocalFile() const const
QString url(FormattingOptions options) const const
bool toBool() const const
QString toString() const const
T value() const const
QScreen * screen() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 17 2024 11:54:28 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.