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_metadataLoadingCompleted = false;
2507 d->m_docdataMigrationNeeded = false;
2508
2509 // 2. load Additional Data (bookmarks, local annotations and metadata) about the document
2510 if (d->m_archiveData) {
2511 // QTemporaryFile is weird and will return false in exists if fileName wasn't called before
2512 d->m_archiveData->metadataFile.fileName();
2513 d->loadDocumentInfo(d->m_archiveData->metadataFile, LoadPageInfo);
2514 d->loadDocumentInfo(LoadGeneralInfo);
2515 } else {
2516 if (d->loadDocumentInfo(LoadPageInfo)) {
2517 d->m_docdataMigrationNeeded = true;
2518 }
2519 d->loadDocumentInfo(LoadGeneralInfo);
2520 }
2521
2522 d->m_metadataLoadingCompleted = true;
2523 d->m_bookmarkManager->setUrl(d->m_url);
2524
2525 // 3. setup observers internal lists and data
2526 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged));
2527
2528 // 4. set initial page (restoring the page saved in xml if loaded)
2529 DocumentViewport loadedViewport = (*d->m_viewportIterator);
2530 if (loadedViewport.isValid()) {
2531 (*d->m_viewportIterator) = DocumentViewport();
2532 if (loadedViewport.pageNumber >= (int)d->m_pagesVector.size()) {
2533 loadedViewport.pageNumber = d->m_pagesVector.size() - 1;
2534 }
2535 } else {
2536 loadedViewport.pageNumber = 0;
2537 }
2538 setViewport(loadedViewport);
2539
2540 // start bookmark saver timer
2541 if (!d->m_saveBookmarksTimer) {
2542 d->m_saveBookmarksTimer = new QTimer(this);
2543 connect(d->m_saveBookmarksTimer, &QTimer::timeout, this, [this] { d->saveDocumentInfo(); });
2544 }
2545 d->m_saveBookmarksTimer->start(5 * 60 * 1000);
2546
2547 // start memory check timer
2548 if (!d->m_memCheckTimer) {
2549 d->m_memCheckTimer = new QTimer(this);
2550 connect(d->m_memCheckTimer, &QTimer::timeout, this, [this] { d->slotTimedMemoryCheck(); });
2551 }
2552 d->m_memCheckTimer->start(kMemCheckTime);
2553
2554 const DocumentViewport nextViewport = d->nextDocumentViewport();
2555 if (nextViewport.isValid()) {
2556 setViewport(nextViewport);
2557 d->m_nextDocumentViewport = DocumentViewport();
2558 d->m_nextDocumentDestination = QString();
2559 }
2560
2561 AudioPlayer::instance()->setDocument(fromFileDescriptor ? QUrl() : d->m_url, this);
2562
2563 const QStringList docScripts = d->m_generator->metaData(QStringLiteral("DocumentScripts"), QStringLiteral("JavaScript")).toStringList();
2564 if (!docScripts.isEmpty()) {
2565 d->m_scripter = new Scripter(d);
2566 for (const QString &docscript : docScripts) {
2567 d->m_scripter->execute(JavaScript, docscript);
2568 }
2569 }
2570
2571 return OpenSuccess;
2572}
2573
2574bool DocumentPrivate::updateMetadataXmlNameAndDocSize()
2575{
2576 // m_docFileName is always local so we can use QFileInfo on it
2577 QFileInfo fileReadTest(m_docFileName);
2578 if (!fileReadTest.isFile() && !fileReadTest.isReadable()) {
2579 return false;
2580 }
2581
2582 m_docSize = fileReadTest.size();
2583
2584 // determine the related "xml document-info" filename
2585 if (m_url.isLocalFile()) {
2586 const QString filePath = docDataFileName(m_url, m_docSize);
2587 qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath;
2588 m_xmlFileName = filePath;
2589 } else {
2590 qCDebug(OkularCoreDebug) << "Metadata file: disabled";
2591 m_xmlFileName = QString();
2592 }
2593
2594 return true;
2595}
2596
2598{
2599 if (d->m_generator) {
2600 Okular::GuiInterface *iface = qobject_cast<Okular::GuiInterface *>(d->m_generator);
2601 if (iface) {
2602 return iface->guiClient();
2603 }
2604 }
2605 return nullptr;
2606}
2607
2609{
2610 // check if there's anything to close...
2611 if (!d->m_generator) {
2612 return;
2613 }
2614
2616
2617 delete d->m_pageController;
2618 d->m_pageController = nullptr;
2619
2620 delete d->m_scripter;
2621 d->m_scripter = nullptr;
2622
2623 // remove requests left in queue
2624 d->clearAndWaitForRequests();
2625
2626 if (d->m_fontThread) {
2627 disconnect(d->m_fontThread, nullptr, this, nullptr);
2628 d->m_fontThread->stopExtraction();
2629 d->m_fontThread->wait();
2630 d->m_fontThread = nullptr;
2631 }
2632
2633 // stop any audio playback
2635
2636 // close the current document and save document info if a document is still opened
2637 if (d->m_generator && d->m_pagesVector.size() > 0) {
2638 d->saveDocumentInfo();
2639
2640 // free the content of the opaque backend actions (if any)
2641 // this is a bit awkward since backends can store "random stuff" in the
2642 // BackendOpaqueAction nativeId qvariant so we need to tell them to free it
2643 // ideally we would just do that in the BackendOpaqueAction destructor
2644 // but that's too late in the cleanup process, i.e. the generator has already closed its document
2645 // and the document generator is nullptr
2646 for (Page *p : std::as_const(d->m_pagesVector)) {
2647 const QList<ObjectRect *> &oRects = p->objectRects();
2648 for (ObjectRect *oRect : oRects) {
2649 if (oRect->objectType() == ObjectRect::Action) {
2650 const Action *a = static_cast<const Action *>(oRect->object());
2651 const BackendOpaqueAction *backendAction = dynamic_cast<const BackendOpaqueAction *>(a);
2652 if (backendAction) {
2653 d->m_generator->freeOpaqueActionContents(*backendAction);
2654 }
2655 }
2656 }
2657
2658 const QList<FormField *> forms = p->formFields();
2659 for (const FormField *form : forms) {
2660 const QList<Action *> additionalActions = form->additionalActions();
2661 for (const Action *a : additionalActions) {
2662 const BackendOpaqueAction *backendAction = dynamic_cast<const BackendOpaqueAction *>(a);
2663 if (backendAction) {
2664 d->m_generator->freeOpaqueActionContents(*backendAction);
2665 }
2666 }
2667 }
2668 }
2669
2670 d->m_generator->closeDocument();
2671 }
2672
2673 if (d->m_synctex_scanner) {
2674 synctex_scanner_free(d->m_synctex_scanner);
2675 d->m_synctex_scanner = nullptr;
2676 }
2677
2678 // stop timers
2679 if (d->m_memCheckTimer) {
2680 d->m_memCheckTimer->stop();
2681 }
2682 if (d->m_saveBookmarksTimer) {
2683 d->m_saveBookmarksTimer->stop();
2684 }
2685
2686 if (d->m_generator) {
2687 // disconnect the generator from this document ...
2688 d->m_generator->d_func()->m_document = nullptr;
2689 // .. and this document from the generator signals
2690 disconnect(d->m_generator, nullptr, this, nullptr);
2691
2692 QHash<QString, GeneratorInfo>::const_iterator genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
2693 Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
2694 }
2695 d->m_generator = nullptr;
2696 d->m_generatorName = QString();
2697 d->m_url = QUrl();
2698 d->m_walletGenerator = nullptr;
2699 d->m_docFileName = QString();
2700 d->m_xmlFileName = QString();
2701 delete d->m_tempFile;
2702 d->m_tempFile = nullptr;
2703 delete d->m_archiveData;
2704 d->m_archiveData = nullptr;
2705 d->m_docSize = -1;
2706 d->m_exportCached = false;
2707 d->m_exportFormats.clear();
2708 d->m_exportToText = ExportFormat();
2709 d->m_fontsCached = false;
2710 d->m_fontsCache.clear();
2711 d->m_rotation = Rotation0;
2712
2713 // send an empty list to observers (to free their data)
2715
2716 // delete pages and clear 'd->m_pagesVector' container
2717 QVector<Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
2718 QVector<Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
2719 for (; pIt != pEnd; ++pIt) {
2720 delete *pIt;
2721 }
2722 d->m_pagesVector.clear();
2723
2724 // clear 'memory allocation' descriptors
2725 qDeleteAll(d->m_allocatedPixmaps);
2726 d->m_allocatedPixmaps.clear();
2727
2728 // clear 'running searches' descriptors
2729 QMap<int, RunningSearch *>::const_iterator rIt = d->m_searches.constBegin();
2730 QMap<int, RunningSearch *>::const_iterator rEnd = d->m_searches.constEnd();
2731 for (; rIt != rEnd; ++rIt) {
2732 delete *rIt;
2733 }
2734 d->m_searches.clear();
2735
2736 // clear the visible areas and notify the observers
2737 QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
2738 QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
2739 for (; vIt != vEnd; ++vIt) {
2740 delete *vIt;
2741 }
2742 d->m_pageRects.clear();
2743 foreachObserver(notifyVisibleRectsChanged());
2744
2745 // reset internal variables
2746
2747 d->m_viewportHistory.clear();
2748 d->m_viewportHistory.emplace_back();
2749 d->m_viewportIterator = d->m_viewportHistory.begin();
2750 d->m_allocatedPixmapsTotalMemory = 0;
2751 d->m_allocatedTextPagesFifo.clear();
2752 d->m_pageSize = PageSize();
2753 d->m_pageSizes.clear();
2754
2755 d->m_documentInfo = DocumentInfo();
2756 d->m_documentInfoAskedKeys.clear();
2757
2758 AudioPlayer::instance()->resetDocument();
2759
2760 d->m_undoStack->clear();
2761 d->m_docdataMigrationNeeded = false;
2762
2763#if HAVE_MALLOC_TRIM
2764 // trim unused memory, glibc should do this but it seems it does not
2765 // this can greatly decrease the [perceived] memory consumption of okular
2766 // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827
2767 malloc_trim(0);
2768#endif
2769}
2770
2772{
2773 Q_ASSERT(!d->m_observers.contains(pObserver));
2774 d->m_observers << pObserver;
2775
2776 // if the observer is added while a document is already opened, tell it
2777 if (!d->m_pagesVector.isEmpty()) {
2779 pObserver->notifyViewportChanged(false /*disables smoothMove*/);
2780 }
2781}
2782
2784{
2785 // remove observer from the set. it won't receive notifications anymore
2786 if (d->m_observers.contains(pObserver)) {
2787 // free observer's pixmap data
2788 QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2789 for (; it != end; ++it) {
2790 (*it)->deletePixmap(pObserver);
2791 }
2792
2793 // [MEM] free observer's allocation descriptors
2794 std::list<AllocatedPixmap *>::iterator aIt = d->m_allocatedPixmaps.begin();
2795 std::list<AllocatedPixmap *>::iterator aEnd = d->m_allocatedPixmaps.end();
2796 while (aIt != aEnd) {
2797 AllocatedPixmap *p = *aIt;
2798 if (p->observer == pObserver) {
2799 aIt = d->m_allocatedPixmaps.erase(aIt);
2800 delete p;
2801 } else {
2802 ++aIt;
2803 }
2804 }
2805
2806 for (PixmapRequest *executingRequest : std::as_const(d->m_executingPixmapRequests)) {
2807 if (executingRequest->observer() == pObserver) {
2808 d->cancelRenderingBecauseOf(executingRequest, nullptr);
2809 }
2810 }
2811
2812 // remove observer entry from the set
2813 d->m_observers.remove(pObserver);
2814 }
2815}
2816
2818{
2819 // reparse generator config and if something changed clear Pages
2820 bool configchanged = false;
2821 if (d->m_generator) {
2822 Okular::ConfigInterface *iface = qobject_cast<Okular::ConfigInterface *>(d->m_generator);
2823 if (iface) {
2824 configchanged = iface->reparseConfig();
2825 }
2826 }
2827 if (configchanged) {
2828 // invalidate pixmaps
2829 QVector<Page *>::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd();
2830 for (; it != end; ++it) {
2831 (*it)->deletePixmaps();
2832 }
2833
2834 // [MEM] remove allocation descriptors
2835 qDeleteAll(d->m_allocatedPixmaps);
2836 d->m_allocatedPixmaps.clear();
2837 d->m_allocatedPixmapsTotalMemory = 0;
2838
2839 // send reload signals to observers
2840 foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap));
2841 }
2842
2843 // free memory if in 'low' profile
2844 if (SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !d->m_allocatedPixmaps.empty() && !d->m_pagesVector.isEmpty()) {
2845 d->cleanupPixmapMemory();
2846 }
2847}
2848
2850{
2851 return d->m_generator;
2852}
2853
2855{
2856 if (d->m_generator) {
2857 Okular::PrintInterface *iface = qobject_cast<Okular::PrintInterface *>(d->m_generator);
2858 return iface ? true : false;
2859 } else {
2860 return false;
2861 }
2862}
2863
2864bool Document::sign(const NewSignatureData &data, const QString &newPath)
2865{
2866 if (d->m_generator->canSign()) {
2867 return d->m_generator->sign(data, newPath);
2868 } else {
2869 return false;
2870 }
2871}
2872
2874{
2875 return d->m_generator ? d->m_generator->certificateStore() : nullptr;
2876}
2877
2879{
2880 d->editorCommandOverride = editCmd;
2881}
2882
2884{
2885 return d->editorCommandOverride;
2886}
2887
2889{
2892 keys << ks;
2893 }
2894
2895 return documentInfo(keys);
2896}
2897
2899{
2900 DocumentInfo result = d->m_documentInfo;
2901 const QSet<DocumentInfo::Key> missingKeys = keys - d->m_documentInfoAskedKeys;
2902
2903 if (d->m_generator && !missingKeys.isEmpty()) {
2904 DocumentInfo info = d->m_generator->generateDocumentInfo(missingKeys);
2905
2906 if (missingKeys.contains(DocumentInfo::FilePath)) {
2907 info.set(DocumentInfo::FilePath, currentDocument().toDisplayString());
2908 }
2909
2910 if (d->m_docSize != -1 && missingKeys.contains(DocumentInfo::DocumentSize)) {
2911 const QString sizeString = KFormat().formatByteSize(d->m_docSize);
2912 info.set(DocumentInfo::DocumentSize, sizeString);
2913 }
2914 if (missingKeys.contains(DocumentInfo::PagesSize)) {
2915 const QString pagesSize = d->pagesSizeString();
2916 if (!pagesSize.isEmpty()) {
2917 info.set(DocumentInfo::PagesSize, pagesSize);
2918 }
2919 }
2920
2921 if (missingKeys.contains(DocumentInfo::Pages) && info.get(DocumentInfo::Pages).isEmpty()) {
2923 }
2924
2925 d->m_documentInfo.d->values.insert(info.d->values);
2926 d->m_documentInfo.d->titles.insert(info.d->titles);
2927 result.d->values.insert(info.d->values);
2928 result.d->titles.insert(info.d->titles);
2929 }
2930 d->m_documentInfoAskedKeys += keys;
2931
2932 return result;
2933}
2934
2936{
2937 return d->m_generator ? d->m_generator->generateDocumentSynopsis() : nullptr;
2938}
2939
2941{
2942 if (!d->m_generator || !d->m_generator->hasFeature(Generator::FontInfo) || d->m_fontThread) {
2943 return;
2944 }
2945
2946 if (d->m_fontsCached) {
2947 // in case we have cached fonts, simulate a reading
2948 // this way the API is the same, and users no need to care about the
2949 // internal caching
2950 for (int i = 0; i < d->m_fontsCache.count(); ++i) {
2951 Q_EMIT gotFont(d->m_fontsCache.at(i));
2953 }
2955 return;
2956 }
2957
2958 d->m_fontThread = new FontExtractionThread(d->m_generator, pages());
2959 connect(d->m_fontThread, &FontExtractionThread::gotFont, this, [this](const Okular::FontInfo &f) { d->fontReadingGotFont(f); });
2960 connect(d->m_fontThread.data(), &FontExtractionThread::progress, this, [this](int p) { d->slotFontReadingProgress(p); });
2961
2962 d->m_fontThread->startExtraction(/*d->m_generator->hasFeature( Generator::Threaded )*/ true);
2963}
2964
2966{
2967 if (!d->m_fontThread) {
2968 return;
2969 }
2970
2971 disconnect(d->m_fontThread, nullptr, this, nullptr);
2972 d->m_fontThread->stopExtraction();
2973 d->m_fontThread = nullptr;
2974 d->m_fontsCache.clear();
2975}
2976
2978{
2979 return d->m_generator ? d->m_generator->hasFeature(Generator::FontInfo) : false;
2980}
2981
2983{
2984 return d->m_generator ? d->m_generator->canSign() : false;
2985}
2986
2988{
2989 return d->m_generator ? d->m_generator->embeddedFiles() : nullptr;
2990}
2991
2992const Page *Document::page(int n) const
2993{
2994 return (n >= 0 && n < d->m_pagesVector.count()) ? d->m_pagesVector.at(n) : nullptr;
2995}
2996
2998{
2999 return (*d->m_viewportIterator);
3000}
3001
3003{
3004 return d->m_pageRects;
3005}
3006
3008{
3009 QVector<VisiblePageRect *>::const_iterator vIt = d->m_pageRects.constBegin();
3010 QVector<VisiblePageRect *>::const_iterator vEnd = d->m_pageRects.constEnd();
3011 for (; vIt != vEnd; ++vIt) {
3012 delete *vIt;
3013 }
3014 d->m_pageRects = visiblePageRects;
3015 // notify change to all other (different from id) observers
3016 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3017 if (o != excludeObserver) {
3018 o->notifyVisibleRectsChanged();
3019 }
3020 }
3021}
3022
3024{
3025 return (*d->m_viewportIterator).pageNumber;
3026}
3027
3029{
3030 return d->m_pagesVector.size();
3031}
3032
3034{
3035 return d->m_url;
3036}
3037
3039{
3040 if (action == Okular::AllowNotes && (d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled)) {
3041 return false;
3042 }
3043 if (action == Okular::AllowFillForms && d->m_docdataMigrationNeeded) {
3044 return false;
3045 }
3046
3047#if !OKULAR_FORCE_DRM
3048 if (KAuthorized::authorize(QStringLiteral("skip_drm")) && !SettingsCore::obeyDRM()) {
3049 return true;
3050 }
3051#endif
3052
3053 return d->m_generator ? d->m_generator->isAllowed(action) : false;
3054}
3055
3057{
3058 return d->m_generator ? d->m_generator->hasFeature(Generator::TextExtraction) : false;
3059}
3060
3062{
3063 return d->m_generator ? d->m_generator->hasFeature(Generator::PageSizes) : false;
3064}
3065
3067{
3068 return d->m_generator ? d->m_generator->hasFeature(Generator::TiledRendering) : false;
3069}
3070
3072{
3073 if (d->m_generator) {
3074 if (d->m_pageSizes.isEmpty()) {
3075 d->m_pageSizes = d->m_generator->pageSizes();
3076 }
3077 return d->m_pageSizes;
3078 }
3079 return PageSize::List();
3080}
3081
3083{
3084 if (!d->m_generator) {
3085 return false;
3086 }
3087
3088 d->cacheExportFormats();
3089 return !d->m_exportToText.isNull();
3090}
3091
3092bool Document::exportToText(const QString &fileName) const
3093{
3094 if (!d->m_generator) {
3095 return false;
3096 }
3097
3098 d->cacheExportFormats();
3099 if (d->m_exportToText.isNull()) {
3100 return false;
3101 }
3102
3103 return d->m_generator->exportTo(fileName, d->m_exportToText);
3104}
3105
3107{
3108 if (!d->m_generator) {
3109 return ExportFormat::List();
3110 }
3111
3112 d->cacheExportFormats();
3113 return d->m_exportFormats;
3114}
3115
3116bool Document::exportTo(const QString &fileName, const ExportFormat &format) const
3117{
3118 return d->m_generator ? d->m_generator->exportTo(fileName, format) : false;
3119}
3120
3122{
3123 return d->m_viewportIterator == d->m_viewportHistory.begin();
3124}
3125
3127{
3128 return d->m_viewportIterator == --(d->m_viewportHistory.end());
3129}
3130
3131QVariant Document::metaData(const QString &key, const QVariant &option) const
3132{
3133 // if option starts with "src:" assume that we are handling a
3134 // source reference
3135 if (key == QLatin1String("NamedViewport") && option.toString().startsWith(QLatin1String("src:"), Qt::CaseInsensitive) && d->m_synctex_scanner) {
3136 const QString reference = option.toString();
3137
3138 // The reference is of form "src:1111Filename", where "1111"
3139 // points to line number 1111 in the file "Filename".
3140 // Extract the file name and the numeral part from the reference string.
3141 // This will fail if Filename starts with a digit.
3142 QString name, lineString;
3143 // Remove "src:". Presence of substring has been checked before this
3144 // function is called.
3145 name = reference.mid(4);
3146 // split
3147 int nameLength = name.length();
3148 int i = 0;
3149 for (i = 0; i < nameLength; ++i) {
3150 if (!name[i].isDigit()) {
3151 break;
3152 }
3153 }
3154 lineString = name.left(i);
3155 name = name.mid(i);
3156 // Remove spaces.
3157 name = name.trimmed();
3158 lineString = lineString.trimmed();
3159 // Convert line to integer.
3160 bool ok;
3161 int line = lineString.toInt(&ok);
3162 if (!ok) {
3163 line = -1;
3164 }
3165
3166 // Use column == -1 for now.
3167 if (synctex_display_query(d->m_synctex_scanner, QFile::encodeName(name).constData(), line, -1, 0) > 0) {
3168 synctex_node_p node;
3169 // For now use the first hit. Could possibly be made smarter
3170 // in case there are multiple hits.
3171 while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
3173
3174 // TeX pages start at 1.
3175 viewport.pageNumber = synctex_node_page(node) - 1;
3176
3177 if (viewport.pageNumber >= 0) {
3178 const QSizeF dpi = d->m_generator->dpi();
3179
3180 // TeX small points ...
3181 double px = (synctex_node_visible_h(node) * dpi.width()) / 72.27;
3182 double py = (synctex_node_visible_v(node) * dpi.height()) / 72.27;
3183 viewport.rePos.normalizedX = px / page(viewport.pageNumber)->width();
3184 viewport.rePos.normalizedY = (py + 0.5) / page(viewport.pageNumber)->height();
3185 viewport.rePos.enabled = true;
3187
3188 return viewport.toString();
3189 }
3190 }
3191 }
3192 }
3193 return d->m_generator ? d->m_generator->metaData(key, option) : QVariant();
3194}
3195
3197{
3198 return d->m_rotation;
3199}
3200
3202{
3203 bool allPagesSameSize = true;
3204 QSizeF size;
3205 for (int i = 0; allPagesSameSize && i < d->m_pagesVector.count(); ++i) {
3206 const Page *p = d->m_pagesVector.at(i);
3207 if (i == 0) {
3208 size = QSizeF(p->width(), p->height());
3209 } else {
3210 allPagesSameSize = (size == QSizeF(p->width(), p->height()));
3211 }
3212 }
3213 if (allPagesSameSize) {
3214 return size;
3215 } else {
3216 return QSizeF();
3217 }
3218}
3219
3221{
3222 if (d->m_generator) {
3223 if (d->m_generator->pagesSizeMetric() != Generator::None) {
3224 const Page *p = d->m_pagesVector.at(page);
3225 return d->localizedSize(QSizeF(p->width(), p->height()));
3226 }
3227 }
3228 return QString();
3229}
3230
3231static bool shouldCancelRenderingBecauseOf(const PixmapRequest &executingRequest, const PixmapRequest &otherRequest)
3232{
3233 // New request has higher priority -> cancel
3234 if (executingRequest.priority() > otherRequest.priority()) {
3235 return true;
3236 }
3237
3238 // New request has lower priority -> don't cancel
3239 if (executingRequest.priority() < otherRequest.priority()) {
3240 return false;
3241 }
3242
3243 // New request has same priority and is from a different observer -> don't cancel
3244 // AFAIK this never happens since all observers have different priorities
3245 if (executingRequest.observer() != otherRequest.observer()) {
3246 return false;
3247 }
3248
3249 // Same priority and observer, different page number -> don't cancel
3250 // may still end up cancelled later in the parent caller if none of the requests
3251 // is of the executingRequest page and RemoveAllPrevious is specified
3252 if (executingRequest.pageNumber() != otherRequest.pageNumber()) {
3253 return false;
3254 }
3255
3256 // Same priority, observer, page, different size -> cancel
3257 if (executingRequest.width() != otherRequest.width()) {
3258 return true;
3259 }
3260
3261 // Same priority, observer, page, different size -> cancel
3262 if (executingRequest.height() != otherRequest.height()) {
3263 return true;
3264 }
3265
3266 // Same priority, observer, page, different tiling -> cancel
3267 if (executingRequest.isTile() != otherRequest.isTile()) {
3268 return true;
3269 }
3270
3271 // Same priority, observer, page, different tiling -> cancel
3272 if (executingRequest.isTile()) {
3273 const NormalizedRect bothRequestsRect = executingRequest.normalizedRect() | otherRequest.normalizedRect();
3274 if (!(bothRequestsRect == executingRequest.normalizedRect())) {
3275 return true;
3276 }
3277 }
3278
3279 return false;
3280}
3281
3282bool DocumentPrivate::cancelRenderingBecauseOf(PixmapRequest *executingRequest, PixmapRequest *newRequest)
3283{
3284 // No point in aborting the rendering already finished, let it go through
3285 if (!executingRequest->d->mResultImage.isNull()) {
3286 return false;
3287 }
3288
3289 if (newRequest && newRequest->asynchronous() && executingRequest->partialUpdatesWanted()) {
3290 newRequest->setPartialUpdatesWanted(true);
3291 }
3292
3293 TilesManager *tm = executingRequest->d->tilesManager();
3294 if (tm) {
3295 tm->setPixmap(nullptr, executingRequest->normalizedRect(), true /*isPartialPixmap*/);
3296 tm->setRequest(NormalizedRect(), 0, 0);
3297 }
3298 PagePrivate::PixmapObject object = executingRequest->page()->d->m_pixmaps.take(executingRequest->observer());
3299 delete object.m_pixmap;
3300
3301 if (executingRequest->d->mShouldAbortRender != 0) {
3302 return false;
3303 }
3304
3305 executingRequest->d->mShouldAbortRender = 1;
3306
3307 if (m_generator->d_ptr->mTextPageGenerationThread && m_generator->d_ptr->mTextPageGenerationThread->page() == executingRequest->page()) {
3308 m_generator->d_ptr->mTextPageGenerationThread->abortExtraction();
3309 }
3310
3311 return true;
3312}
3313
3315{
3317}
3318
3320{
3321 if (requests.isEmpty()) {
3322 return;
3323 }
3324
3325 if (!d->m_pageController) {
3326 // delete requests..
3327 qDeleteAll(requests);
3328 // ..and return
3329 return;
3330 }
3331
3332 QSet<DocumentObserver *> observersPixmapCleared;
3333
3334 // 1. [CLEAN STACK] remove previous requests of requesterID
3335 DocumentObserver *requesterObserver = requests.first()->observer();
3336 QSet<int> requestedPages;
3337 {
3338 for (PixmapRequest *request : requests) {
3339 Q_ASSERT(request->observer() == requesterObserver);
3340 requestedPages.insert(request->pageNumber());
3341 }
3342 }
3343 const bool removeAllPrevious = reqOptions & RemoveAllPrevious;
3344 d->m_pixmapRequestsMutex.lock();
3345 std::list<PixmapRequest *>::iterator sIt = d->m_pixmapRequestsStack.begin(), sEnd = d->m_pixmapRequestsStack.end();
3346 while (sIt != sEnd) {
3347 if ((*sIt)->observer() == requesterObserver && (removeAllPrevious || requestedPages.contains((*sIt)->pageNumber()))) {
3348 // delete request and remove it from stack
3349 delete *sIt;
3350 sIt = d->m_pixmapRequestsStack.erase(sIt);
3351 } else {
3352 ++sIt;
3353 }
3354 }
3355
3356 // 1.B [PREPROCESS REQUESTS] tweak some values of the requests
3357 for (PixmapRequest *request : requests) {
3358 // set the 'page field' (see PixmapRequest) and check if it is valid
3359 qCDebug(OkularCoreDebug).nospace() << "request observer=" << request->observer() << " " << request->width() << "x" << request->height() << "@" << request->pageNumber();
3360 if (d->m_pagesVector.value(request->pageNumber()) == nullptr) {
3361 // skip requests referencing an invalid page (must not happen)
3362 delete request;
3363 continue;
3364 }
3365
3366 request->d->mPage = d->m_pagesVector.value(request->pageNumber());
3367
3368 if (request->isTile()) {
3369 // Change the current request rect so that only invalid tiles are
3370 // requested. Also make sure the rect is tile-aligned.
3371 NormalizedRect tilesRect;
3372 const QList<Tile> tiles = request->d->tilesManager()->tilesAt(request->normalizedRect(), TilesManager::TerminalTile);
3373 QList<Tile>::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd();
3374 while (tIt != tEnd) {
3375 const Tile &tile = *tIt;
3376 if (!tile.isValid()) {
3377 if (tilesRect.isNull()) {
3378 tilesRect = tile.rect();
3379 } else {
3380 tilesRect |= tile.rect();
3381 }
3382 }
3383
3384 tIt++;
3385 }
3386
3387 request->setNormalizedRect(tilesRect);
3388 }
3389
3390 if (!request->asynchronous()) {
3391 request->d->mPriority = 0;
3392 }
3393 }
3394
3395 // 1.C [CANCEL REQUESTS] cancel those requests that are running and should be cancelled because of the new requests coming in
3396 if (d->m_generator->hasFeature(Generator::SupportsCancelling)) {
3397 for (PixmapRequest *executingRequest : std::as_const(d->m_executingPixmapRequests)) {
3398 bool newRequestsContainExecutingRequestPage = false;
3399 bool requestCancelled = false;
3400 for (PixmapRequest *newRequest : requests) {
3401 if (newRequest->pageNumber() == executingRequest->pageNumber() && requesterObserver == executingRequest->observer()) {
3402 newRequestsContainExecutingRequestPage = true;
3403 }
3404
3405 if (shouldCancelRenderingBecauseOf(*executingRequest, *newRequest)) {
3406 requestCancelled = d->cancelRenderingBecauseOf(executingRequest, newRequest);
3407 }
3408 }
3409
3410 // If we were told to remove all the previous requests and the executing request page is not part of the new requests, cancel it
3411 if (!requestCancelled && removeAllPrevious && requesterObserver == executingRequest->observer() && !newRequestsContainExecutingRequestPage) {
3412 requestCancelled = d->cancelRenderingBecauseOf(executingRequest, nullptr);
3413 }
3414
3415 if (requestCancelled) {
3416 observersPixmapCleared << executingRequest->observer();
3417 }
3418 }
3419 }
3420
3421 // 2. [ADD TO STACK] add requests to stack
3422 for (PixmapRequest *request : requests) {
3423 // add request to the 'stack' at the right place
3424 if (!request->priority()) {
3425 // add priority zero requests to the top of the stack
3426 d->m_pixmapRequestsStack.push_back(request);
3427 } else {
3428 // insert in stack sorted by priority
3429 sIt = d->m_pixmapRequestsStack.begin();
3430 sEnd = d->m_pixmapRequestsStack.end();
3431 while (sIt != sEnd && (*sIt)->priority() > request->priority()) {
3432 ++sIt;
3433 }
3434 d->m_pixmapRequestsStack.insert(sIt, request);
3435 }
3436 }
3437 d->m_pixmapRequestsMutex.unlock();
3438
3439 // 3. [START FIRST GENERATION] if <NO>generator is ready, start a new generation,
3440 // or else (if gen is running) it will be started when the new contents will
3441 // come from generator (in requestDone())</NO>
3442 // all handling of requests put into sendGeneratorPixmapRequest
3443 // if ( generator->canRequestPixmap() )
3444 d->sendGeneratorPixmapRequest();
3445
3446 for (DocumentObserver *o : std::as_const(observersPixmapCleared)) {
3447 o->notifyContentsCleared(Okular::DocumentObserver::Pixmap);
3448 }
3449}
3450
3451void Document::requestTextPage(uint pageNumber)
3452{
3453 Page *kp = d->m_pagesVector[pageNumber];
3454 if (!d->m_generator || !kp) {
3455 return;
3456 }
3457
3458 // Memory management for TextPages
3459
3460 d->m_generator->generateTextPage(kp);
3461}
3462
3463void DocumentPrivate::notifyAnnotationChanges(int page)
3464{
3465 foreachObserverD(notifyPageChanged(page, DocumentObserver::Annotations));
3466}
3467
3468void DocumentPrivate::notifyFormChanges(int /*page*/)
3469{
3470 recalculateForms();
3471}
3472
3473void Document::addPageAnnotation(int page, Annotation *annotation)
3474{
3475 // Transform annotation's base boundary rectangle into unrotated coordinates
3476 Page *p = d->m_pagesVector[page];
3477 QTransform t = p->d->rotationMatrix();
3478 annotation->d_ptr->baseTransform(t.inverted());
3479 QUndoCommand *uc = new AddAnnotationCommand(this->d, annotation, page);
3480 d->m_undoStack->push(uc);
3481}
3482
3484{
3485 if (!annotation || (annotation->flags() & Annotation::DenyWrite)) {
3486 return false;
3487 }
3488
3490 return false;
3491 }
3492
3493 if ((annotation->flags() & Annotation::External) && !d->canModifyExternalAnnotations()) {
3494 return false;
3495 }
3496
3497 switch (annotation->subType()) {
3498 case Annotation::AText:
3499 case Annotation::ALine:
3500 case Annotation::AGeom:
3502 case Annotation::AStamp:
3503 case Annotation::AInk:
3504 return true;
3505 default:
3506 return false;
3507 }
3508}
3509
3511{
3512 Q_ASSERT(d->m_prevPropsOfAnnotBeingModified.isNull());
3513 if (!d->m_prevPropsOfAnnotBeingModified.isNull()) {
3514 qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties has already been called since last call to Document::modifyPageAnnotationProperties";
3515 return;
3516 }
3517 d->m_prevPropsOfAnnotBeingModified = annotation->getAnnotationPropertiesDomNode();
3518}
3519
3521{
3522 Q_ASSERT(!d->m_prevPropsOfAnnotBeingModified.isNull());
3523 if (d->m_prevPropsOfAnnotBeingModified.isNull()) {
3524 qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties must be called before Annotation is modified";
3525 return;
3526 }
3527 QDomNode prevProps = d->m_prevPropsOfAnnotBeingModified;
3528 QUndoCommand *uc = new Okular::ModifyAnnotationPropertiesCommand(d, annotation, page, prevProps, annotation->getAnnotationPropertiesDomNode());
3529 d->m_undoStack->push(uc);
3530 d->m_prevPropsOfAnnotBeingModified.clear();
3531}
3532
3533void Document::translatePageAnnotation(int page, Annotation *annotation, const NormalizedPoint &delta)
3534{
3535 int complete = (annotation->flags() & Okular::Annotation::BeingMoved) == 0;
3536 QUndoCommand *uc = new Okular::TranslateAnnotationCommand(d, annotation, page, delta, complete);
3537 d->m_undoStack->push(uc);
3538}
3539
3540void Document::adjustPageAnnotation(int page, Annotation *annotation, const Okular::NormalizedPoint &delta1, const Okular::NormalizedPoint &delta2)
3541{
3542 const bool complete = (annotation->flags() & Okular::Annotation::BeingResized) == 0;
3543 QUndoCommand *uc = new Okular::AdjustAnnotationCommand(d, annotation, page, delta1, delta2, complete);
3544 d->m_undoStack->push(uc);
3545}
3546
3547void Document::editPageAnnotationContents(int page, Annotation *annotation, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3548{
3549 QString prevContents = annotation->contents();
3550 QUndoCommand *uc = new EditAnnotationContentsCommand(d, annotation, page, newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos);
3551 d->m_undoStack->push(uc);
3552}
3553
3555{
3556 if (!annotation || (annotation->flags() & Annotation::DenyDelete)) {
3557 return false;
3558 }
3559
3560 if ((annotation->flags() & Annotation::External) && !d->canRemoveExternalAnnotations()) {
3561 return false;
3562 }
3563
3564 switch (annotation->subType()) {
3565 case Annotation::AText:
3566 case Annotation::ALine:
3567 case Annotation::AGeom:
3569 case Annotation::AStamp:
3570 case Annotation::AInk:
3571 case Annotation::ACaret:
3572 return true;
3573 default:
3574 return false;
3575 }
3576}
3577
3579{
3580 QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3581 d->m_undoStack->push(uc);
3582}
3583
3585{
3586 d->m_undoStack->beginMacro(i18nc("remove a collection of annotations from the page", "remove annotations"));
3587 for (Annotation *annotation : annotations) {
3588 QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page);
3589 d->m_undoStack->push(uc);
3590 }
3591 d->m_undoStack->endMacro();
3592}
3593
3594bool DocumentPrivate::canAddAnnotationsNatively() const
3595{
3596 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3597
3599 return true;
3600 }
3601
3602 return false;
3603}
3604
3605bool DocumentPrivate::canModifyExternalAnnotations() const
3606{
3607 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3608
3610 return true;
3611 }
3612
3613 return false;
3614}
3615
3616bool DocumentPrivate::canRemoveExternalAnnotations() const
3617{
3618 Okular::SaveInterface *iface = qobject_cast<Okular::SaveInterface *>(m_generator);
3619
3621 return true;
3622 }
3623
3624 return false;
3625}
3626
3627void Document::setPageTextSelection(int page, std::unique_ptr<RegularAreaRect> &&rect, const QColor &color)
3628{
3629 Page *kp = d->m_pagesVector[page];
3630 if (!d->m_generator || !kp) {
3631 return;
3632 }
3633
3634 // add or remove the selection basing whether rect is null or not
3635 if (rect) {
3636 kp->d->setTextSelections(*rect, color);
3637 } else {
3638 kp->d->deleteTextSelections();
3639 }
3640
3641 // notify observers about the change
3642 foreachObserver(notifyPageChanged(page, DocumentObserver::TextSelection));
3643}
3644
3646{
3647 return d->m_undoStack->canUndo();
3648}
3649
3651{
3652 return d->m_undoStack->canRedo();
3653}
3654
3655/* REFERENCE IMPLEMENTATION: better calling setViewport from other code
3656void Document::setNextPage()
3657{
3658 // advance page and set viewport on observers
3659 if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 )
3660 setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber + 1 ) );
3661}
3662
3663void Document::setPrevPage()
3664{
3665 // go to previous page and set viewport on observers
3666 if ( (*d->m_viewportIterator).pageNumber > 0 )
3667 setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber - 1 ) );
3668}
3669*/
3670
3671void Document::setViewport(const DocumentViewport &viewport, DocumentObserver *excludeObserver, bool smoothMove, bool updateHistory)
3672{
3673 if (!viewport.isValid()) {
3674 qCDebug(OkularCoreDebug) << "invalid viewport:" << viewport.toString();
3675 return;
3676 }
3677 if (viewport.pageNumber >= int(d->m_pagesVector.count())) {
3678 // qCDebug(OkularCoreDebug) << "viewport out of document:" << viewport.toString();
3679 return;
3680 }
3681
3682 // if already broadcasted, don't redo it
3683 DocumentViewport &oldViewport = *d->m_viewportIterator;
3684 // disabled by enrico on 2005-03-18 (less debug output)
3685 // if ( viewport == oldViewport )
3686 // qCDebug(OkularCoreDebug) << "setViewport with the same viewport.";
3687
3688 const int oldPageNumber = oldViewport.pageNumber;
3689
3690 // set internal viewport taking care of history
3691 if (oldViewport.pageNumber == viewport.pageNumber || !oldViewport.isValid() || !updateHistory) {
3692 // if page is unchanged save the viewport at current position in queue
3693 oldViewport = viewport;
3694 } else {
3695 // remove elements after viewportIterator in queue
3696 d->m_viewportHistory.erase(++d->m_viewportIterator, d->m_viewportHistory.end());
3697
3698 // keep the list to a reasonable size by removing head when needed
3699 if (d->m_viewportHistory.size() >= OKULAR_HISTORY_MAXSTEPS) {
3700 d->m_viewportHistory.pop_front();
3701 }
3702
3703 // add the item at the end of the queue
3704 d->m_viewportIterator = d->m_viewportHistory.insert(d->m_viewportHistory.end(), viewport);
3705 }
3706
3707 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3708
3709 const bool currentPageChanged = (oldPageNumber != currentViewportPage);
3710
3711 // notify change to all other (different from id) observers
3712 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3713 if (o != excludeObserver) {
3714 o->notifyViewportChanged(smoothMove);
3715 }
3716
3717 if (currentPageChanged) {
3718 o->notifyCurrentPageChanged(oldPageNumber, currentViewportPage);
3719 }
3720 }
3721}
3722
3723void Document::setViewportPage(int page, DocumentObserver *excludeObserver, bool smoothMove)
3724{
3725 // clamp page in range [0 ... numPages-1]
3726 if (page < 0) {
3727 page = 0;
3728 } else if (page > (int)d->m_pagesVector.count()) {
3729 page = d->m_pagesVector.count() - 1;
3730 }
3731
3732 // make a viewport from the page and broadcast it
3733 setViewport(DocumentViewport(page), excludeObserver, smoothMove);
3734}
3735
3736void Document::setZoom(int factor, DocumentObserver *excludeObserver)
3737{
3738 // notify change to all other (different from id) observers
3739 for (DocumentObserver *o : std::as_const(d->m_observers)) {
3740 if (o != excludeObserver) {
3741 o->notifyZoom(factor);
3742 }
3743 }
3744}
3745
3747// restore viewport from the history
3748{
3749 if (d->m_viewportIterator != d->m_viewportHistory.begin()) {
3750 const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3751
3752 // restore previous viewport and notify it to observers
3753 --d->m_viewportIterator;
3754 foreachObserver(notifyViewportChanged(true));
3755
3756 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3757 if (oldViewportPage != currentViewportPage)
3758 foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3759 }
3760}
3761
3763// restore next viewport from the history
3764{
3765 auto nextIterator = std::list<DocumentViewport>::const_iterator(d->m_viewportIterator);
3766 ++nextIterator;
3767 if (nextIterator != d->m_viewportHistory.end()) {
3768 const int oldViewportPage = (*d->m_viewportIterator).pageNumber;
3769
3770 // restore next viewport and notify it to observers
3771 ++d->m_viewportIterator;
3772 foreachObserver(notifyViewportChanged(true));
3773
3774 const int currentViewportPage = (*d->m_viewportIterator).pageNumber;
3775 if (oldViewportPage != currentViewportPage)
3776 foreachObserver(notifyCurrentPageChanged(oldViewportPage, currentViewportPage));
3777 }
3778}
3779
3781{
3782 d->m_nextDocumentViewport = viewport;
3783}
3784
3786{
3787 d->m_nextDocumentDestination = namedDestination;
3788}
3789
3790void Document::searchText(int searchID, const QString &text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor &color)
3791{
3792 d->m_searchCancelled = false;
3793
3794 // safety checks: don't perform searches on empty or unsearchable docs
3795 if (!d->m_generator || !d->m_generator->hasFeature(Generator::TextExtraction) || d->m_pagesVector.isEmpty()) {
3797 return;
3798 }
3799
3800 // if searchID search not recorded, create new descriptor and init params
3801 QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3802 if (searchIt == d->m_searches.end()) {
3803 RunningSearch *search = new RunningSearch();
3804 search->continueOnPage = -1;
3805 searchIt = d->m_searches.insert(searchID, search);
3806 }
3807 RunningSearch *s = *searchIt;
3808
3809 // update search structure
3810 bool newText = text != s->cachedString;
3811 s->cachedString = text;
3812 s->cachedType = type;
3813 s->cachedCaseSensitivity = caseSensitivity;
3814 s->cachedViewportMove = moveViewport;
3815 s->cachedColor = color;
3816 s->isCurrentlySearching = true;
3817
3818 // global data for search
3819 QSet<int> *pagesToNotify = new QSet<int>;
3820
3821 // remove highlights from pages and queue them for notifying changes
3822 *pagesToNotify += s->highlightedPages;
3823 for (const int pageNumber : std::as_const(s->highlightedPages)) {
3824 d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3825 }
3826 s->highlightedPages.clear();
3827
3828 // set hourglass cursor
3830
3831 // 1. ALLDOC - process all document marking pages
3832 if (type == AllDocument) {
3834
3835 // search and highlight 'text' (as a solid phrase) on all pages
3836 QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID] { d->doContinueAllDocumentSearch(pagesToNotify, pageMatches, 0, searchID); });
3837 }
3838 // 2. NEXTMATCH - find next matching item (or start from top)
3839 // 3. PREVMATCH - find previous matching item (or start from bottom)
3840 else if (type == NextMatch || type == PreviousMatch) {
3841 // find out from where to start/resume search from
3842 const bool forward = type == NextMatch;
3843 const int viewportPage = (*d->m_viewportIterator).pageNumber;
3844 const int fromStartSearchPage = forward ? 0 : d->m_pagesVector.count() - 1;
3845 int currentPage = fromStart ? fromStartSearchPage : ((s->continueOnPage != -1) ? s->continueOnPage : viewportPage);
3846 Page *lastPage = fromStart ? nullptr : d->m_pagesVector[currentPage];
3847 int pagesDone = 0;
3848
3849 // continue checking last TextPage first (if it is the current page)
3850 RegularAreaRect *match = nullptr;
3851 if (lastPage && lastPage->number() == s->continueOnPage) {
3852 if (newText) {
3853 match = lastPage->findText(searchID, text, forward ? FromTop : FromBottom, caseSensitivity);
3854 } else {
3855 match = lastPage->findText(searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch);
3856 }
3857 if (!match) {
3858 if (forward) {
3859 currentPage++;
3860 } else {
3861 currentPage--;
3862 }
3863 pagesDone++;
3864 }
3865 }
3866
3867 s->pagesDone = pagesDone;
3868
3869 DoContinueDirectionMatchSearchStruct *searchStruct = new DoContinueDirectionMatchSearchStruct();
3870 searchStruct->pagesToNotify = pagesToNotify;
3871 searchStruct->match = match;
3872 searchStruct->currentPage = currentPage;
3873 searchStruct->searchID = searchID;
3874
3875 QTimer::singleShot(0, this, [this, searchStruct] { d->doContinueDirectionMatchSearch(searchStruct); });
3876 }
3877 // 4. GOOGLE* - process all document marking pages
3878 else if (type == GoogleAll || type == GoogleAny) {
3880 const QStringList words = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
3881
3882 // search and highlight every word in 'text' on all pages
3883 QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID, words] { d->doContinueGooglesDocumentSearch(pagesToNotify, pageMatches, 0, searchID, words); });
3884 }
3885}
3886
3888{
3889 // check if searchID is present in runningSearches
3890 QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3891 if (it == d->m_searches.constEnd()) {
3893 return;
3894 }
3895
3896 // start search with cached parameters from last search by searchID
3897 RunningSearch *p = *it;
3898 if (!p->isCurrentlySearching) {
3899 searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, p->cachedType, p->cachedViewportMove, p->cachedColor);
3900 }
3901}
3902
3903void Document::continueSearch(int searchID, SearchType type)
3904{
3905 // check if searchID is present in runningSearches
3906 QMap<int, RunningSearch *>::const_iterator it = d->m_searches.constFind(searchID);
3907 if (it == d->m_searches.constEnd()) {
3909 return;
3910 }
3911
3912 // start search with cached parameters from last search by searchID
3913 RunningSearch *p = *it;
3914 if (!p->isCurrentlySearching) {
3915 searchText(searchID, p->cachedString, false, p->cachedCaseSensitivity, type, p->cachedViewportMove, p->cachedColor);
3916 }
3917}
3918
3919void Document::resetSearch(int searchID)
3920{
3921 // if we are closing down, don't bother doing anything
3922 if (!d->m_generator) {
3923 return;
3924 }
3925
3926 // check if searchID is present in runningSearches
3927 QMap<int, RunningSearch *>::iterator searchIt = d->m_searches.find(searchID);
3928 if (searchIt == d->m_searches.end()) {
3929 return;
3930 }
3931
3932 // get previous parameters for search
3933 RunningSearch *s = *searchIt;
3934
3935 // unhighlight pages and inform observers about that
3936 for (const int pageNumber : std::as_const(s->highlightedPages)) {
3937 d->m_pagesVector.at(pageNumber)->d->deleteHighlights(searchID);
3938 foreachObserver(notifyPageChanged(pageNumber, DocumentObserver::Highlights));
3939 }
3940
3941 // send the setup signal too (to update views that filter on matches)
3942 foreachObserver(notifySetup(d->m_pagesVector, 0));
3943
3944 // remove search from the runningSearches list and delete it
3945 d->m_searches.erase(searchIt);
3946 delete s;
3947}
3948
3950{
3951 d->m_searchCancelled = true;
3952}
3953
3955{
3956 d->m_undoStack->undo();
3957}
3958
3960{
3961 d->m_undoStack->redo();
3962}
3963
3964void Document::editFormText(int pageNumber, Okular::FormFieldText *form, const QString &newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3965{
3966 QUndoCommand *uc = new EditFormTextCommand(this->d, form, pageNumber, newContents, newCursorPos, form->text(), prevCursorPos, prevAnchorPos);
3967 d->m_undoStack->push(uc);
3968}
3969
3970void Document::editFormList(int pageNumber, FormFieldChoice *form, const QList<int> &newChoices)
3971{
3972 const QList<int> prevChoices = form->currentChoices();
3973 QUndoCommand *uc = new EditFormListCommand(this->d, form, pageNumber, newChoices, prevChoices);
3974 d->m_undoStack->push(uc);
3975}
3976
3977void Document::editFormCombo(int pageNumber, FormFieldChoice *form, const QString &newText, int newCursorPos, int prevCursorPos, int prevAnchorPos)
3978{
3979 QString prevText;
3980 if (form->currentChoices().isEmpty()) {
3981 prevText = form->editChoice();
3982 } else {
3983 prevText = form->choices().at(form->currentChoices().constFirst());
3984 }
3985
3986 QUndoCommand *uc = new EditFormComboCommand(this->d, form, pageNumber, newText, newCursorPos, prevText, prevCursorPos, prevAnchorPos);
3987 d->m_undoStack->push(uc);
3988}
3989
3990void Document::editFormButtons(int pageNumber, const QList<FormFieldButton *> &formButtons, const QList<bool> &newButtonStates)
3991{
3992 QUndoCommand *uc = new EditFormButtonsCommand(this->d, pageNumber, formButtons, newButtonStates);
3993 d->m_undoStack->push(uc);
3994}
3995
3997{
3998 const int numOfPages = pages();
3999 for (int i = currentPage(); i >= 0; i--) {
4000 d->refreshPixmaps(i);
4001 }
4002 for (int i = currentPage() + 1; i < numOfPages; i++) {
4003 d->refreshPixmaps(i);
4004 }
4005}
4006
4008{
4009 return d->m_bookmarkManager;
4010}
4011
4013{
4014 QList<int> list;
4015 uint docPages = pages();
4016
4017 // pages are 0-indexed internally, but 1-indexed externally
4018 for (uint i = 0; i < docPages; i++) {
4019 if (bookmarkManager()->isBookmarked(i)) {
4020 list << i + 1;
4021 }
4022 }
4023 return list;
4024}
4025
4027{
4028 // Code formerly in Part::slotPrint()
4029 // range detecting
4030 QString range;
4031 uint docPages = pages();
4032 int startId = -1;
4033 int endId = -1;
4034
4035 for (uint i = 0; i < docPages; ++i) {
4036 if (bookmarkManager()->isBookmarked(i)) {
4037 if (startId < 0) {
4038 startId = i;
4039 }
4040 if (endId < 0) {
4041 endId = startId;
4042 } else {
4043 ++endId;
4044 }
4045 } else if (startId >= 0 && endId >= 0) {
4046 if (!range.isEmpty()) {
4047 range += QLatin1Char(',');
4048 }
4049
4050 if (endId - startId > 0) {
4051 range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
4052 } else {
4053 range += QString::number(startId + 1);
4054 }
4055 startId = -1;
4056 endId = -1;
4057 }
4058 }
4059 if (startId >= 0 && endId >= 0) {
4060 if (!range.isEmpty()) {
4061 range += QLatin1Char(',');
4062 }
4063
4064 if (endId - startId > 0) {
4065 range += QStringLiteral("%1-%2").arg(startId + 1).arg(endId + 1);
4066 } else {
4067 range += QString::number(startId + 1);
4068 }
4069 }
4070 return range;
4071}
4072
4073struct ExecuteNextActionsHelper : public QObject, private DocumentObserver {
4074 Q_OBJECT
4075public:
4076 explicit ExecuteNextActionsHelper(Document *doc)
4077 : m_doc(doc)
4078 {
4079 doc->addObserver(this);
4080 connect(doc, &Document::aboutToClose, this, [this] { b = false; });
4081 }
4082
4083 ~ExecuteNextActionsHelper() override
4084 {
4085 m_doc->removeObserver(this);
4086 }
4087
4088 void notifySetup(const QVector<Okular::Page *> & /*pages*/, int setupFlags) override
4089 {
4090 if (setupFlags == DocumentChanged || setupFlags == UrlChanged) {
4091 b = false;
4092 }
4093 }
4094
4095 bool shouldExecuteNextAction() const
4096 {
4097 return b;
4098 }
4099
4100private:
4101 Document *const m_doc;
4102 bool b = true;
4103};
4104
4106{
4107 if (!action) {
4108 return;
4109 }
4110
4111 // Don't execute next actions if the action itself caused the closing of the document
4112 const ExecuteNextActionsHelper executeNextActionsHelper(this);
4113
4114 switch (action->actionType()) {
4115 case Action::Goto: {
4116 const GotoAction *go = static_cast<const GotoAction *>(action);
4117 d->m_nextDocumentViewport = go->destViewport();
4118 d->m_nextDocumentDestination = go->destinationName();
4119
4120 // Explanation of why d->m_nextDocumentViewport is needed:
4121 // all openRelativeFile does is launch a signal telling we
4122 // want to open another URL, the problem is that when the file is
4123 // non local, the loading is done asynchronously so you can't
4124 // do a setViewport after the if as it was because you are doing the setViewport
4125 // on the old file and when the new arrives there is no setViewport for it and
4126 // it does not show anything
4127
4128 // first open filename if link is pointing outside this document
4129 const QString filename = go->fileName();
4130 if (go->isExternal() && !d->openRelativeFile(filename)) {
4131 qCWarning(OkularCoreDebug).nospace() << "Action: Error opening '" << filename << "'.";
4132 break;
4133 } else {
4134 const DocumentViewport nextViewport = d->nextDocumentViewport();
4135 // skip local links that point to nowhere (broken ones)
4136 if (!nextViewport.isValid()) {
4137 break;
4138 }
4139
4140 setViewport(nextViewport, nullptr, true);
4141 d->m_nextDocumentViewport = DocumentViewport();
4142 d->m_nextDocumentDestination = QString();
4143 }
4144
4145 } break;
4146
4147 case Action::Execute: {
4148 const ExecuteAction *exe = static_cast<const ExecuteAction *>(action);
4149 const QString fileName = exe->fileName();
4150 if (fileName.endsWith(QLatin1String(".pdf"), Qt::CaseInsensitive)) {
4151 d->openRelativeFile(fileName);
4152 break;
4153 }
4154
4155 // Albert: the only pdf i have that has that kind of link don't define
4156 // an application and use the fileName as the file to open
4157 QUrl url = d->giveAbsoluteUrl(fileName);
4158 QMimeDatabase db;
4159 QMimeType mime = db.mimeTypeForUrl(url);
4160 // Check executables
4161 if (KIO::OpenUrlJob::isExecutableFile(url, mime.name())) {
4162 // Don't have any pdf that uses this code path, just a guess on how it should work
4163 if (!exe->parameters().isEmpty()) {
4164 url = d->giveAbsoluteUrl(exe->parameters());
4165 mime = db.mimeTypeForUrl(url);
4166
4167 if (KIO::OpenUrlJob::isExecutableFile(url, mime.name())) {
4168 // this case is a link pointing to an executable with a parameter
4169 // that also is an executable, possibly a hand-crafted pdf
4170 Q_EMIT error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
4171 break;
4172 }
4173 } else {
4174 // this case is a link pointing to an executable with no parameters
4175 // core developers find unacceptable executing it even after asking the user
4176 Q_EMIT error(i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that."), -1);
4177 break;
4178 }
4179 }
4180
4181 KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mime.name());
4183 job->start();
4184 connect(job, &KIO::OpenUrlJob::result, this, [this, mime](KJob *job) {
4185 if (job->error()) {
4186 Q_EMIT error(i18n("No application found for opening file of mimetype %1.", mime.name()), -1);
4187 }
4188 });
4189 } break;
4190
4191 case Action::DocAction: {
4192 const DocumentAction *docaction = static_cast<const DocumentAction *>(action);
4193 switch (docaction->documentActionType()) {
4195 setViewportPage(0);
4196 break;
4198 if ((*d->m_viewportIterator).pageNumber > 0) {
4199 setViewportPage((*d->m_viewportIterator).pageNumber - 1);
4200 }
4201 break;
4203 if ((*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1) {
4204 setViewportPage((*d->m_viewportIterator).pageNumber + 1);
4205 }
4206 break;
4208 setViewportPage(d->m_pagesVector.count() - 1);
4209 break;
4212 break;
4215 break;
4217 Q_EMIT quit();
4218 break;
4221 break;
4224 break;
4226 Q_EMIT linkFind();
4227 break;
4230 break;
4232 Q_EMIT close();
4233 break;
4236 break;
4239 break;
4240 }
4241 } break;
4242
4243 case Action::Browse: {
4244 const BrowseAction *browse = static_cast<const BrowseAction *>(action);
4245 QString lilySource;
4246 int lilyRow = 0, lilyCol = 0;
4247 // if the url is a mailto one, invoke mailer
4248 if (browse->url().scheme() == QLatin1String("mailto")) {
4249 QDesktopServices::openUrl(browse->url());
4250 } else if (extractLilyPondSourceReference(browse->url(), &lilySource, &lilyRow, &lilyCol)) {
4251 const SourceReference ref(lilySource, lilyRow, lilyCol);
4253 } else {
4254 const QUrl url = browse->url();
4255
4256 // fix for #100366, documents with relative links that are the form of http:foo.pdf
4257 if ((url.scheme() == QLatin1String("http")) && url.host().isEmpty() && url.fileName().endsWith(QLatin1String("pdf"))) {
4258 d->openRelativeFile(url.fileName());
4259 break;
4260 }
4261
4262 // handle documents with relative path
4263 QUrl realUrl;
4264 if (d->m_url.isValid()) {
4265 realUrl = KIO::upUrl(d->m_url).resolved(url);
4266 } else if (!url.isRelative()) {
4267 realUrl = url;
4268 }
4269 if (realUrl.isValid()) {
4270 auto *job = new KIO::OpenUrlJob(realUrl);
4271 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->m_widget.data()));
4272 job->start();
4273 }
4274 }
4275 } break;
4276
4277 case Action::Sound: {
4278 const SoundAction *linksound = static_cast<const SoundAction *>(action);
4279 AudioPlayer::instance()->playSound(linksound->sound(), linksound);
4280 } break;
4281
4282 case Action::Script: {
4283 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4284 if (!d->m_scripter) {
4285 d->m_scripter = new Scripter(d);
4286 }
4287 d->m_scripter->execute(linkscript->scriptType(), linkscript->script());
4288 } break;
4289
4290 case Action::Movie:
4291 Q_EMIT processMovieAction(static_cast<const MovieAction *>(action));
4292 break;
4293 case Action::Rendition: {
4294 const RenditionAction *linkrendition = static_cast<const RenditionAction *>(action);
4295 if (!linkrendition->script().isEmpty()) {
4296 if (!d->m_scripter) {
4297 d->m_scripter = new Scripter(d);
4298 }
4299 d->m_scripter->execute(linkrendition->scriptType(), linkrendition->script());
4300 }
4301
4302 Q_EMIT processRenditionAction(static_cast<const RenditionAction *>(action));
4303 } break;
4304 case Action::BackendOpaque: {
4305 d->m_generator->opaqueAction(static_cast<const BackendOpaqueAction *>(action));
4306 } break;
4307 }
4308
4309 if (executeNextActionsHelper.shouldExecuteNextAction()) {
4310 const QVector<Action *> nextActions = action->nextActions();
4311 for (const Action *a : nextActions) {
4312 processAction(a);
4313 }
4314 }
4315}
4316
4318{
4319 if (action->actionType() != Action::Script) {
4320 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for formatting.";
4321 return;
4322 }
4323
4324 // Lookup the page of the FormFieldText
4325 int foundPage = d->findFieldPageNumber(fft);
4326
4327 if (foundPage == -1) {
4328 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4329 return;
4330 }
4331
4332 const QString unformattedText = fft->text();
4333
4334 std::shared_ptr<Event> event = Event::createFormatEvent(fft, d->m_pagesVector[foundPage]);
4335
4336 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4337
4338 d->executeScriptEvent(event, linkscript);
4339
4340 const QString formattedText = event->value().toString();
4341 if (formattedText != unformattedText) {
4342 // We set the formattedText, because when we call refreshFormWidget
4343 // It will set the QLineEdit to this formattedText
4344 fft->setText(formattedText);
4345 fft->setAppearanceText(formattedText);
4347 d->refreshPixmaps(foundPage);
4348 // Then we make the form have the unformatted text, to use
4349 // in calculations and other things.
4350 fft->setText(unformattedText);
4352 // When the field was calculated we need to refresh even
4353 // if the format script changed nothing. e.g. on error.
4354 // This is because the recalculateForms function delegated
4355 // the responsiblity for the refresh to us.
4357 d->refreshPixmaps(foundPage);
4358 }
4359}
4360
4361QString DocumentPrivate::diff(const QString &oldVal, const QString &newVal)
4362{
4363 // We need to consider unicode surrogate pairs and others so working
4364 // with QString directly, even with the private QStringIterator is
4365 // not that simple to get right
4366 // so let's just convert to ucs4
4367 // also, given that toUcs4 is either a QList or a QVector depending on
4368 // qt version, let's try keep it very auto-typed to ease Qt6 porting
4369
4370 auto oldUcs4 = oldVal.toStdU32String();
4371 auto newUcs4 = newVal.toStdU32String();
4372
4373 for (size_t i = 0; i < std::min(oldUcs4.size(), newUcs4.size()); i++) {
4374 if (oldUcs4.at(i) != newUcs4.at(i)) {
4375 return QString::fromUcs4(std::u32string_view {newUcs4}.substr(i).data(), newUcs4.size() - i);
4376 }
4377 }
4378 if (oldUcs4.size() < newUcs4.size()) {
4379 return QString::fromUcs4(std::u32string_view {newUcs4}.substr(oldUcs4.size()).data(), newUcs4.size() - oldUcs4.size());
4380 }
4381 return {};
4382}
4383
4385{
4386 if (action->actionType() != Action::Script) {
4387 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4388 return;
4389 }
4390 // Lookup the page of the FormFieldText
4391 int foundPage = d->findFieldPageNumber(fft);
4392
4393 if (foundPage == -1) {
4394 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4395 return;
4396 }
4397
4398 std::shared_ptr<Event> event = Event::createKeystrokeEvent(fft, d->m_pagesVector[foundPage]);
4399 event->setChange(DocumentPrivate::diff(fft->text(), newValue.toString()));
4400
4401 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4402
4403 d->executeScriptEvent(event, linkscript);
4404
4405 if (event->returnCode()) {
4406 fft->setText(newValue.toString());
4407 } else {
4409 }
4410}
4411
4413{
4414 if (action->actionType() != Action::Script) {
4415 qCDebug(OkularCoreDebug) << "Unsupported action type" << action->actionType() << "for keystroke.";
4416 return;
4417 }
4418 // Lookup the page of the FormFieldText
4419 int foundPage = d->findFieldPageNumber(fft);
4420
4421 if (foundPage == -1) {
4422 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4423 return;
4424 }
4425
4426 std::shared_ptr<Event> event = Event::createKeystrokeEvent(fft, d->m_pagesVector[foundPage]);
4427 event->setWillCommit(true);
4428
4429 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4430
4431 d->executeScriptEvent(event, linkscript);
4432
4433 if (event->returnCode()) {
4434 fft->setText(event->value().toString());
4435 // TODO commit value
4436 } else {
4437 // TODO reset to committed value
4438 }
4439}
4440
4442{
4443 if (!action || action->actionType() != Action::Script) {
4444 return;
4445 }
4446
4447 // Lookup the page of the FormFieldText
4448 int foundPage = d->findFieldPageNumber(field);
4449
4450 if (foundPage == -1) {
4451 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4452 return;
4453 }
4454
4455 std::shared_ptr<Event> event = Event::createFormFocusEvent(field, d->m_pagesVector[foundPage]);
4456
4457 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4458
4459 d->executeScriptEvent(event, linkscript);
4460}
4461
4462void Document::processValidateAction(const Action *action, Okular::FormFieldText *fft, bool &returnCode)
4463{
4464 if (!action || action->actionType() != Action::Script) {
4465 return;
4466 }
4467
4468 // Lookup the page of the FormFieldText
4469 int foundPage = d->findFieldPageNumber(fft);
4470
4471 if (foundPage == -1) {
4472 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4473 return;
4474 }
4475
4476 std::shared_ptr<Event> event = Event::createFormValidateEvent(fft, d->m_pagesVector[foundPage]);
4477
4478 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4479
4480 d->executeScriptEvent(event, linkscript);
4481 returnCode = event->returnCode();
4482}
4483
4485{
4486 if (!action || action->actionType() != Action::Script) {
4487 return;
4488 }
4489
4490 // Lookup the page of the FormFieldText
4491 int foundPage = d->findFieldPageNumber(ff);
4492
4493 if (foundPage == -1) {
4494 qCDebug(OkularCoreDebug) << "Could not find page for formfield!";
4495 return;
4496 }
4497
4498 std::shared_ptr<Event> event = Event::createFieldMouseUpEvent(ff, d->m_pagesVector[foundPage]);
4499
4500 const ScriptAction *linkscript = static_cast<const ScriptAction *>(action);
4501
4502 d->executeScriptEvent(event, linkscript);
4503}
4504
4506{
4507 if (!ref) {
4508 return;
4509 }
4510
4511 const QUrl url = d->giveAbsoluteUrl(ref->fileName());
4512 if (!url.isLocalFile()) {
4513 qCDebug(OkularCoreDebug) << url.url() << "is not a local file.";
4514 return;
4515 }
4516
4517 const QString absFileName = url.toLocalFile();
4518 if (!QFile::exists(absFileName)) {
4519 qCDebug(OkularCoreDebug) << "No such file:" << absFileName;
4520 return;
4521 }
4522
4523 bool handled = false;
4524 Q_EMIT sourceReferenceActivated(absFileName, ref->row(), ref->column(), &handled);
4525 if (handled) {
4526 return;
4527 }
4528
4529 static QHash<int, QString> editors;
4530 // init the editors table if empty (on first run, usually)
4531 if (editors.isEmpty()) {
4532 editors = buildEditorsMap();
4533 }
4534
4535 // prefer the editor from the command line
4536 QString p = d->editorCommandOverride;
4537 if (p.isEmpty()) {
4538 QHash<int, QString>::const_iterator it = editors.constFind(SettingsCore::externalEditor());
4539 if (it != editors.constEnd()) {
4540 p = *it;
4541 } else {
4542 p = SettingsCore::externalEditorCommand();
4543 }
4544 }
4545 // custom editor not yet configured
4546 if (p.isEmpty()) {
4547 return;
4548 }
4549
4550 // manually append the %f placeholder if not specified
4551 if (p.indexOf(QLatin1String("%f")) == -1) {
4552 p.append(QLatin1String(" %f"));
4553 }
4554
4555 // replacing the placeholders
4557 map.insert(QLatin1Char('f'), absFileName);
4558 map.insert(QLatin1Char('c'), QString::number(ref->column()));
4559 map.insert(QLatin1Char('l'), QString::number(ref->row()));
4561 if (cmd.isEmpty()) {
4562 return;
4563 }
4564 QStringList args = KShell::splitArgs(cmd);
4565 if (args.isEmpty()) {
4566 return;
4567 }
4568
4569 const QString prog = args.takeFirst();
4570 // Make sure prog is in PATH and not just in the CWD
4571 const QString progFullPath = QStandardPaths::findExecutable(prog);
4572 if (progFullPath.isEmpty()) {
4573 return;
4574 }
4575
4576 KProcess::startDetached(progFullPath, args);
4577}
4578
4579const SourceReference *Document::dynamicSourceReference(int pageNr, double absX, double absY)
4580{
4581 if (!d->m_synctex_scanner) {
4582 return nullptr;
4583 }
4584
4585 const QSizeF dpi = d->m_generator->dpi();
4586
4587 if (synctex_edit_query(d->m_synctex_scanner, pageNr + 1, absX * 72. / dpi.width(), absY * 72. / dpi.height()) > 0) {
4588 synctex_node_p node;
4589 // TODO what should we do if there is really more than one node?
4590 while ((node = synctex_scanner_next_result(d->m_synctex_scanner))) {
4591 int line = synctex_node_line(node);
4592 int col = synctex_node_column(node);
4593 // column extraction does not seem to be implemented in synctex so far. set the SourceReference default value.
4594 if (col == -1) {
4595 col = 0;
4596 }
4597 const char *name = synctex_scanner_get_name(d->m_synctex_scanner, synctex_node_tag(node));
4598
4599 return new Okular::SourceReference(QFile::decodeName(name), line, col);
4600 }
4601 }
4602 return nullptr;
4603}
4604
4606{
4607 if (d->m_generator) {
4608 if (d->m_generator->hasFeature(Generator::PrintNative)) {
4609 return NativePrinting;
4610 }
4611
4612#ifndef Q_OS_WIN
4613 if (d->m_generator->hasFeature(Generator::PrintPostscript)) {
4614 return PostscriptPrinting;
4615 }
4616#endif
4617 }
4618
4619 return NoPrinting;
4620}
4621
4623{
4624 return d->m_generator ? d->m_generator->hasFeature(Generator::PrintToFile) : false;
4625}
4626
4628{
4629 return d->m_generator ? d->m_generator->print(printer) : Document::UnknownPrintError;
4630}
4631
4633{
4634 switch (error) {
4635 case TemporaryFileOpenPrintError:
4636 return i18n("Could not open a temporary file");
4637 case FileConversionPrintError:
4638 return i18n("Print conversion failed");
4639 case PrintingProcessCrashPrintError:
4640 return i18n("Printing process crashed");
4641 case PrintingProcessStartPrintError:
4642 return i18n("Printing process could not start");
4643 case PrintToFilePrintError:
4644 return i18n("Printing to file failed");
4645 case InvalidPrinterStatePrintError:
4646 return i18n("Printer was in invalid state");
4647 case UnableToFindFilePrintError:
4648 return i18n("Unable to find file to print");
4649 case NoFileToPrintError:
4650 return i18n("There was no file to print");
4651 case NoBinaryToPrintError:
4652 return i18n("Could not find a suitable binary for printing. Make sure CUPS lpr binary is available");
4653 case InvalidPageSizePrintError:
4654 return i18n("The page print size is invalid");
4655 case NoPrintError:
4656 return QString();
4657 case UnknownPrintError:
4658 return QString();
4659 }
4660
4661 return QString();
4662}
4663
4665{
4666 if (d->m_generator) {
4667 PrintInterface *iface = qobject_cast<Okular::PrintInterface *>(d->m_generator);
4668 return iface ? iface->printConfigurationWidget() : nullptr;
4669 } else {
4670 return nullptr;
4671 }
4672}
4673
4675{
4676 if (!dialog) {
4677 return;
4678 }
4679
4680 // We know it's a BackendConfigDialog, but check anyway
4681 BackendConfigDialog *bcd = dynamic_cast<BackendConfigDialog *>(dialog);
4682 if (!bcd) {
4683 return;
4684 }
4685
4686 // ensure that we have all the generators with settings loaded
4687 QVector<KPluginMetaData> offers = DocumentPrivate::configurableGenerators();
4688 d->loadServiceList(offers);
4689
4690 // We want the generators to be sorted by name so let's fill in a QMap
4691 // this sorts by internal id which is not awesome, but at least the sorting
4692 // is stable between runs that before it wasn't
4693 QMap<QString, GeneratorInfo> sortedGenerators;
4694 QHash<QString, GeneratorInfo>::iterator it = d->m_loadedGenerators.begin();
4695 QHash<QString, GeneratorInfo>::iterator itEnd = d->m_loadedGenerators.end();
4696 for (; it != itEnd; ++it) {
4697 sortedGenerators.insert(it.key(), it.value());
4698 }
4699
4700 bool pagesAdded = false;
4701 QMap<QString, GeneratorInfo>::iterator sit = sortedGenerators.begin();
4702 QMap<QString, GeneratorInfo>::iterator sitEnd = sortedGenerators.end();
4703 for (; sit != sitEnd; ++sit) {
4704 Okular::ConfigInterface *iface = d->generatorConfig(sit.value());
4705 if (iface) {
4706 iface->addPages(dialog);
4707 pagesAdded = true;
4708
4709 if (sit.value().generator == d->m_generator) {
4710 const int rowCount = bcd->thePageWidget()->model()->rowCount();
4711 KPageView *view = bcd->thePageWidget();
4712 view->setCurrentPage(view->model()->index(rowCount - 1, 0));
4713 }
4714 }
4715 }
4716 if (pagesAdded) {
4717 connect(dialog, &KConfigDialog::settingsChanged, this, [this] { d->slotGeneratorConfigChanged(); });
4718 }
4719}
4720
4721QVector<KPluginMetaData> DocumentPrivate::configurableGenerators()
4722{
4723 const QVector<KPluginMetaData> available = availableGenerators();
4725 for (const KPluginMetaData &md : available) {
4726 if (md.rawData().value(QStringLiteral("X-KDE-okularHasInternalSettings")).toBool()) {
4727 result << md;
4728 }
4729 }
4730 return result;
4731}
4732
4734{
4735 if (!d->m_generator) {
4736 return KPluginMetaData();
4737 }
4738
4739 auto genIt = d->m_loadedGenerators.constFind(d->m_generatorName);
4740 Q_ASSERT(genIt != d->m_loadedGenerators.constEnd());
4741 return genIt.value().metadata;
4742}
4743
4745{
4746 return DocumentPrivate::configurableGenerators().size();
4747}
4748
4750{
4751 // TODO: make it a static member of DocumentPrivate?
4752 QStringList result = d->m_supportedMimeTypes;
4753 if (result.isEmpty()) {
4754 const QVector<KPluginMetaData> available = DocumentPrivate::availableGenerators();
4755 for (const KPluginMetaData &md : available) {
4756 result << md.mimeTypes();
4757 }
4758
4759 // Remove duplicate mimetypes represented by different names
4760 QMimeDatabase mimeDatabase;
4761 QSet<QMimeType> uniqueMimetypes;
4762 for (const QString &mimeName : std::as_const(result)) {
4763 uniqueMimetypes.insert(mimeDatabase.mimeTypeForName(mimeName));
4764 }
4765 result.clear();
4766 for (const QMimeType &mimeType : uniqueMimetypes) {
4767 result.append(mimeType.name());
4768 }
4769
4770 // Add the Okular archive mimetype
4771 result << QStringLiteral("application/vnd.kde.okular-archive");
4772
4773 // Sorting by mimetype name doesn't make a ton of sense,
4774 // but ensures that the list is ordered the same way every time
4775 std::sort(result.begin(), result.end());
4776
4777 d->m_supportedMimeTypes = result;
4778 }
4779 return result;
4780}
4781
4783{
4784 if (!d->m_generator) {
4785 return false;
4786 }
4787
4788 return d->m_generator->hasFeature(Generator::SwapBackingFile);
4789}
4790
4791bool Document::swapBackingFile(const QString &newFileName, const QUrl &url)
4792{
4793 if (!d->m_generator) {
4794 return false;
4795 }
4796
4797 if (!d->m_generator->hasFeature(Generator::SwapBackingFile)) {
4798 return false;
4799 }
4800
4801 // Save metadata about the file we're about to close
4802 d->saveDocumentInfo();
4803
4804 d->clearAndWaitForRequests();
4805
4806 qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName;
4807 QVector<Page *> newPagesVector;
4808 Generator::SwapBackingFileResult result = d->m_generator->swapBackingFile(newFileName, newPagesVector);
4809 if (result != Generator::SwapBackingFileError) {
4810 QList<ObjectRect *> rectsToDelete;
4811 QList<Annotation *> annotationsToDelete;
4812 QSet<PagePrivate *> pagePrivatesToDelete;
4813
4814 if (result == Generator::SwapBackingFileReloadInternalData) {
4815 // Here we need to replace everything that the old generator
4816 // had created with what the new one has without making it look like
4817 // we have actually closed and opened the file again
4818
4819 // Simple sanity check
4820 if (newPagesVector.count() != d->m_pagesVector.count()) {
4821 return false;
4822 }
4823
4824 // Update the undo stack contents
4825 for (int i = 0; i < d->m_undoStack->count(); ++i) {
4826 // Trust me on the const_cast ^_^
4827 QUndoCommand *uc = const_cast<QUndoCommand *>(d->m_undoStack->command(i));
4828 if (OkularUndoCommand *ouc = dynamic_cast<OkularUndoCommand *>(uc)) {
4829 const bool success = ouc->refreshInternalPageReferences(newPagesVector);
4830 if (!success) {
4831 qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc;
4832 return false;
4833 }
4834 } else {
4835 qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc;
4836 return false;
4837 }
4838 }
4839
4840 for (int i = 0; i < d->m_pagesVector.count(); ++i) {
4841 // switch the PagePrivate* from newPage to oldPage
4842 // this way everyone still holding Page* doesn't get
4843 // disturbed by it
4844 Page *oldPage = d->m_pagesVector[i];
4845 Page *newPage = newPagesVector[i];
4846 newPage->d->adoptGeneratedContents(oldPage->d);
4847
4848 pagePrivatesToDelete << oldPage->d;
4849 oldPage->d = newPage->d;
4850 oldPage->d->m_page = oldPage;
4851 oldPage->d->m_doc = d;
4852 newPage->d = nullptr;
4853
4854 annotationsToDelete << oldPage->m_annotations;
4855 rectsToDelete << oldPage->m_rects;
4856 oldPage->m_annotations = newPage->m_annotations;
4857 oldPage->m_rects = newPage->m_rects;
4858 }
4859 qDeleteAll(newPagesVector);
4860 }
4861
4862 d->m_url = url;
4863 d->m_docFileName = newFileName;
4864 d->updateMetadataXmlNameAndDocSize();
4865 d->m_bookmarkManager->setUrl(d->m_url);
4866 d->m_documentInfo = DocumentInfo();
4867 d->m_documentInfoAskedKeys.clear();
4868
4869 if (d->m_synctex_scanner) {
4870 synctex_scanner_free(d->m_synctex_scanner);
4871 d->m_synctex_scanner = synctex_scanner_new_with_output_file(QFile::encodeName(newFileName).constData(), nullptr, 1);
4872 if (!d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String("sync"))) {
4873 d->loadSyncFile(newFileName);
4874 }
4875 }
4876
4877 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::UrlChanged));
4878
4879 qDeleteAll(annotationsToDelete);
4880 qDeleteAll(rectsToDelete);
4881 qDeleteAll(pagePrivatesToDelete);
4882
4883 return true;
4884 } else {
4885 return false;
4886 }
4887}
4888
4889bool Document::swapBackingFileArchive(const QString &newFileName, const QUrl &url)
4890{
4891 qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName;
4892
4893 ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive(newFileName);
4894 if (!newArchive) {
4895 return false;
4896 }
4897
4898 const QString tempFileName = newArchive->document.fileName();
4899
4900 const bool success = swapBackingFile(tempFileName, url);
4901
4902 if (success) {
4903 delete d->m_archiveData;
4904 d->m_archiveData = newArchive;
4905 }
4906
4907 return success;
4908}
4909
4911{
4912 if (clean) {
4913 d->m_undoStack->setClean();
4914 } else {
4915 d->m_undoStack->resetClean();
4916 }
4917}
4918
4919bool Document::isHistoryClean() const
4920{
4921 return d->m_undoStack->isClean();
4922}
4923
4925{
4926 if (!d->m_generator) {
4927 return false;
4928 }
4929 Q_ASSERT(!d->m_generatorName.isEmpty());
4930
4931 QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
4932 Q_ASSERT(genIt != d->m_loadedGenerators.end());
4933 SaveInterface *saveIface = d->generatorSave(genIt.value());
4934 if (!saveIface) {
4935 return false;
4936 }
4937
4938 return saveIface->supportsOption(SaveInterface::SaveChanges);
4939}
4940
4942{
4943 switch (cap) {
4945 /* Assume that if the generator supports saving, forms can be saved.
4946 * We have no means to actually query the generator at the moment
4947 * TODO: Add some method to query the generator in SaveInterface */
4948 return canSaveChanges();
4949
4951 return d->canAddAnnotationsNatively();
4952 }
4953
4954 return false;
4955}
4956
4957bool Document::saveChanges(const QString &fileName)
4958{
4959 QString errorText;
4960 return saveChanges(fileName, &errorText);
4961}
4962
4963bool Document::saveChanges(const QString &fileName, QString *errorText)
4964{
4965 if (!d->m_generator || fileName.isEmpty()) {
4966 return false;
4967 }
4968 Q_ASSERT(!d->m_generatorName.isEmpty());
4969
4970 QHash<QString, GeneratorInfo>::iterator genIt = d->m_loadedGenerators.find(d->m_generatorName);
4971 Q_ASSERT(genIt != d->m_loadedGenerators.end());
4972 SaveInterface *saveIface = d->generatorSave(genIt.value());
4973 if (!saveIface || !saveIface->supportsOption(SaveInterface::SaveChanges)) {
4974 return false;
4975 }
4976
4977 return saveIface->save(fileName, SaveInterface::SaveChanges, errorText);
4978}
4979
4981{
4982 if (!view) {
4983 return;
4984 }
4985
4986 Document *viewDoc = view->viewDocument();
4987 if (viewDoc) {
4988 // check if already registered for this document
4989 if (viewDoc == this) {
4990 return;
4991 }
4992
4993 viewDoc->unregisterView(view);
4994 }
4995
4996 d->m_views.insert(view);
4997 view->d_func()->document = d;
4998}
4999
5001{
5002 if (!view) {
5003 return;
5004 }
5005
5006 Document *viewDoc = view->viewDocument();
5007 if (!viewDoc || viewDoc != this) {
5008 return;
5009 }
5010
5011 view->d_func()->document = nullptr;
5012 d->m_views.remove(view);
5013}
5014
5016{
5017 if (d->m_generator) {
5018 return d->m_generator->requestFontData(font);
5019 }
5020
5021 return {};
5022}
5023
5024ArchiveData *DocumentPrivate::unpackDocumentArchive(const QString &archivePath)
5025{
5026 QMimeDatabase db;
5027 const QMimeType mime = db.mimeTypeForFile(archivePath, QMimeDatabase::MatchExtension);
5028 if (!mime.inherits(QStringLiteral("application/vnd.kde.okular-archive"))) {
5029 return nullptr;
5030 }
5031
5032 KZip okularArchive(archivePath);
5033 if (!okularArchive.open(QIODevice::ReadOnly)) {
5034 return nullptr;
5035 }
5036
5037 const KArchiveDirectory *mainDir = okularArchive.directory();
5038
5039 // Check the archive doesn't have folders, we don't create them when saving the archive
5040 // and folders mean paths and paths mean path traversal issues
5041 const QStringList mainDirEntries = mainDir->entries();
5042 for (const QString &entry : mainDirEntries) {
5043 if (mainDir->entry(entry)->isDirectory()) {
5044 qWarning() << "Warning: Found a directory inside" << archivePath << " - Okular does not create files like that so it is most probably forged.";
5045 return nullptr;
5046 }
5047 }
5048
5049 const KArchiveEntry *mainEntry = mainDir->entry(QStringLiteral("content.xml"));
5050 if (!mainEntry || !mainEntry->isFile()) {
5051 return nullptr;
5052 }
5053
5054 std::unique_ptr<QIODevice> mainEntryDevice(static_cast<const KZipFileEntry *>(mainEntry)->createDevice());
5055 QDomDocument doc;
5056 if (!doc.setContent(mainEntryDevice.get())) {
5057 return nullptr;
5058 }
5059 mainEntryDevice.reset();
5060
5061 QDomElement root = doc.documentElement();
5062 if (root.tagName() != QLatin1String("OkularArchive")) {
5063 return nullptr;
5064 }
5065
5066 QString documentFileName;
5067 QString metadataFileName;
5068 QDomElement el = root.firstChild().toElement();
5069 for (; !el.isNull(); el = el.nextSibling().toElement()) {
5070 if (el.tagName() == QLatin1String("Files")) {
5071 QDomElement fileEl = el.firstChild().toElement();
5072 for (; !fileEl.isNull(); fileEl = fileEl.nextSibling().toElement()) {
5073 if (fileEl.tagName() == QLatin1String("DocumentFileName")) {
5074 documentFileName = fileEl.text();
5075 } else if (fileEl.tagName() == QLatin1String("MetadataFileName")) {
5076 metadataFileName = fileEl.text();
5077 }
5078 }
5079 }
5080 }
5081 if (documentFileName.isEmpty()) {
5082 return nullptr;
5083 }
5084
5085 const KArchiveEntry *docEntry = mainDir->entry(documentFileName);
5086 if (!docEntry || !docEntry->isFile()) {
5087 return nullptr;
5088 }
5089
5090 std::unique_ptr<ArchiveData> archiveData(new ArchiveData());
5091 const int dotPos = documentFileName.indexOf(QLatin1Char('.'));
5092 if (dotPos != -1) {
5093 archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos));
5094 }
5095 if (!archiveData->document.open()) {
5096 return nullptr;
5097 }
5098
5099 archiveData->originalFileName = documentFileName;
5100
5101 {
5102 std::unique_ptr<QIODevice> docEntryDevice(static_cast<const KZipFileEntry *>(docEntry)->createDevice());
5103 copyQIODevice(docEntryDevice.get(), &archiveData->document);
5104 archiveData->document.close();
5105 }
5106
5107 const KArchiveEntry *metadataEntry = mainDir->entry(metadataFileName);
5108 if (metadataEntry && metadataEntry->isFile()) {
5109 std::unique_ptr<QIODevice> metadataEntryDevice(static_cast<const KZipFileEntry *>(metadataEntry)->createDevice());
5110 archiveData->metadataFile.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.xml"));
5111 if (archiveData->metadataFile.open()) {
5112 copyQIODevice(metadataEntryDevice.get(), &archiveData->metadataFile);
5113 archiveData->metadataFile.close();
5114 }
5115 }
5116
5117 return archiveData.release();
5118}
5119
5120Document::OpenResult Document::openDocumentArchive(const QString &docFile, const QUrl &url, const QString &password)
5121{
5122 d->m_archiveData = DocumentPrivate::unpackDocumentArchive(docFile);
5123 if (!d->m_archiveData) {
5124 return OpenError;
5125 }
5126
5127 const QString tempFileName = d->m_archiveData->document.fileName();
5128 QMimeDatabase db;
5129 const QMimeType docMime = db.mimeTypeForFile(tempFileName, QMimeDatabase::MatchExtension);
5130 const OpenResult ret = openDocument(tempFileName, url, docMime, password);
5131
5132 if (ret != OpenSuccess) {
5133 delete d->m_archiveData;
5134 d->m_archiveData = nullptr;
5135 }
5136
5137 return ret;
5138}
5139
5141{
5142 if (!d->m_generator) {
5143 return false;
5144 }
5145
5146 /* If we opened an archive, use the name of original file (eg foo.pdf)
5147 * instead of the archive's one (eg foo.okular) */
5148 QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName();
5149 if (docFileName == QLatin1String("-")) {
5150 return false;
5151 }
5152
5153 QString docPath = d->m_docFileName;
5154 const QFileInfo fi(docPath);
5155 if (fi.isSymLink()) {
5156 docPath = fi.symLinkTarget();
5157 }
5158
5159 KZip okularArchive(fileName);
5160 if (!okularArchive.open(QIODevice::WriteOnly)) {
5161 return false;
5162 }
5163
5164 const KUser user;
5165#ifndef Q_OS_WIN
5166 const KUserGroup userGroup(user.groupId());
5167#else
5168 const KUserGroup userGroup(QStringLiteral(""));
5169#endif
5170
5171 QDomDocument contentDoc(QStringLiteral("OkularArchive"));
5172 QDomProcessingInstruction xmlPi = contentDoc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\""));
5173 contentDoc.appendChild(xmlPi);
5174 QDomElement root = contentDoc.createElement(QStringLiteral("OkularArchive"));
5175 contentDoc.appendChild(root);
5176
5177 QDomElement filesNode = contentDoc.createElement(QStringLiteral("Files"));
5178 root.appendChild(filesNode);
5179
5180 QDomElement fileNameNode = contentDoc.createElement(QStringLiteral("DocumentFileName"));
5181 filesNode.appendChild(fileNameNode);
5182 fileNameNode.appendChild(contentDoc.createTextNode(docFileName));
5183
5184 QDomElement metadataFileNameNode = contentDoc.createElement(QStringLiteral("MetadataFileName"));
5185 filesNode.appendChild(metadataFileNameNode);
5186 metadataFileNameNode.appendChild(contentDoc.createTextNode(QStringLiteral("metadata.xml")));
5187
5188 // If the generator can save annotations natively, do it
5189 QTemporaryFile modifiedFile;
5190 bool annotationsSavedNatively = false;
5191 bool formsSavedNatively = false;
5192 if (d->canAddAnnotationsNatively() || canSaveChanges(SaveFormsCapability)) {
5193 if (!modifiedFile.open()) {
5194 return false;
5195 }
5196
5197 const QString modifiedFileName = modifiedFile.fileName();
5198
5199 modifiedFile.close(); // We're only interested in the file name
5200
5201 QString errorText;
5202 if (saveChanges(modifiedFileName, &errorText)) {
5203 docPath = modifiedFileName; // Save this instead of the original file
5204 annotationsSavedNatively = d->canAddAnnotationsNatively();
5205 formsSavedNatively = canSaveChanges(SaveFormsCapability);
5206 } else {
5207 qCWarning(OkularCoreDebug) << "saveChanges failed: " << errorText;
5208 qCDebug(OkularCoreDebug) << "Falling back to saving a copy of the original file";
5209 }
5210 }
5211
5212 PageItems saveWhat = None;
5213 if (!annotationsSavedNatively) {
5214 saveWhat |= AnnotationPageItems;
5215 }
5216 if (!formsSavedNatively) {
5217 saveWhat |= FormFieldPageItems;
5218 }
5219
5220 QTemporaryFile metadataFile;
5221 if (!d->savePageDocumentInfo(&metadataFile, saveWhat)) {
5222 return false;
5223 }
5224
5225 const QByteArray contentDocXml = contentDoc.toByteArray();
5226 const mode_t perm = 0100644;
5227 okularArchive.writeFile(QStringLiteral("content.xml"), contentDocXml, perm, user.loginName(), userGroup.name());
5228
5229 okularArchive.addLocalFile(docPath, docFileName);
5230 okularArchive.addLocalFile(metadataFile.fileName(), QStringLiteral("metadata.xml"));
5231
5232 if (!okularArchive.close()) {
5233 return false;
5234 }
5235
5236 return true;
5237}
5238
5240{
5241 if (!d->m_archiveData) {
5242 return false;
5243 }
5244
5245 // Remove existing file, if present (QFile::copy doesn't overwrite by itself)
5246 QFile::remove(destFileName);
5247
5248 return d->m_archiveData->document.copy(destFileName);
5249}
5250
5252{
5253 double width, height;
5254 int landscape, portrait;
5256
5257 // if some pages are landscape and others are not, the most common wins, as
5258 // QPrinter does not accept a per-page setting
5259 landscape = 0;
5260 portrait = 0;
5261 for (uint i = 0; i < pages(); i++) {
5262 currentPage = page(i);
5263 width = currentPage->width();
5264 height = currentPage->height();
5265 if (currentPage->orientation() == Okular::Rotation90 || currentPage->orientation() == Okular::Rotation270) {
5266 std::swap(width, height);
5267 }
5268 if (width > height) {
5269 landscape++;
5270 } else {
5271 portrait++;
5272 }
5273 }
5274 return (landscape > portrait) ? QPageLayout::Landscape : QPageLayout::Portrait;
5275}
5276
5278{
5279 d->m_annotationEditingEnabled = enable;
5280 foreachObserver(notifySetup(d->m_pagesVector, 0));
5281}
5282
5283void Document::walletDataForFile(const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey) const
5284{
5285 if (d->m_generator) {
5286 d->m_generator->walletDataForFile(fileName, walletName, walletFolder, walletKey);
5287 } else if (d->m_walletGenerator) {
5288 d->m_walletGenerator->walletDataForFile(fileName, walletName, walletFolder, walletKey);
5289 }
5290}
5291
5293{
5294 return d->m_docdataMigrationNeeded;
5295}
5296
5298{
5299 if (d->m_docdataMigrationNeeded) {
5300 d->m_docdataMigrationNeeded = false;
5301 foreachObserver(notifySetup(d->m_pagesVector, 0));
5302 }
5303}
5304
5306{
5307 return d->m_generator ? d->m_generator->layersModel() : nullptr;
5308}
5309
5311{
5312 return d->m_openError;
5313}
5314
5315QByteArray Document::requestSignedRevisionData(const Okular::SignatureInfo &info)
5316{
5317 QFile f(d->m_docFileName);
5318 if (!f.open(QIODevice::ReadOnly)) {
5319 Q_EMIT error(i18n("Could not open '%1'. File does not exist", d->m_docFileName), -1);
5320 return {};
5321 }
5322
5323 const QList<qint64> byteRange = info.signedRangeBounds();
5324 f.seek(byteRange.first());
5325 QByteArray data = f.read(byteRange.last() - byteRange.first());
5326 f.close();
5327
5328 return data;
5329}
5330
5331void Document::refreshPixmaps(int pageNumber)
5332{
5333 d->refreshPixmaps(pageNumber);
5334}
5335
5336void DocumentPrivate::executeScript(const QString &function)
5337{
5338 if (!m_scripter) {
5339 m_scripter = new Scripter(this);
5340 }
5341 m_scripter->execute(JavaScript, function);
5342}
5343
5344void DocumentPrivate::requestDone(PixmapRequest *req)
5345{
5346 if (!req) {
5347 return;
5348 }
5349
5350 if (!m_generator || m_closingLoop) {
5351 m_pixmapRequestsMutex.lock();
5352 m_executingPixmapRequests.remove(req);
5353 m_pixmapRequestsMutex.unlock();
5354 delete req;
5355 if (m_closingLoop) {
5356 m_closingLoop->exit();
5357 }
5358 return;
5359 }
5360
5361#ifndef NDEBUG
5362 if (!m_generator->canGeneratePixmap()) {
5363 qCDebug(OkularCoreDebug) << "requestDone with generator not in READY state.";
5364 }
5365#endif
5366
5367 if (!req->shouldAbortRender()) {
5368 // [MEM] 1.1 find and remove a previous entry for the same page and id
5369 std::list<AllocatedPixmap *>::iterator aIt = m_allocatedPixmaps.begin();
5370 std::list<AllocatedPixmap *>::iterator aEnd = m_allocatedPixmaps.end();
5371 for (; aIt != aEnd; ++aIt) {
5372 if ((*aIt)->page == req->pageNumber() && (*aIt)->observer == req->observer()) {
5373 AllocatedPixmap *p = *aIt;
5374 m_allocatedPixmaps.erase(aIt);
5375 m_allocatedPixmapsTotalMemory -= p->memory;
5376 delete p;
5377 break;
5378 }
5379 }
5380
5381 DocumentObserver *observer = req->observer();
5382 if (m_observers.contains(observer)) {
5383 // [MEM] 1.2 append memory allocation descriptor to the FIFO
5384 qulonglong memoryBytes = 0;
5385 const TilesManager *tm = req->d->tilesManager();
5386 if (tm) {
5387 memoryBytes = tm->totalMemory();
5388 } else {
5389 memoryBytes = 4 * req->width() * req->height();
5390 }
5391
5392 AllocatedPixmap *memoryPage = new AllocatedPixmap(req->observer(), req->pageNumber(), memoryBytes);
5393 m_allocatedPixmaps.push_back(memoryPage);
5394 m_allocatedPixmapsTotalMemory += memoryBytes;
5395
5396 // 2. notify an observer that its pixmap changed
5398 }
5399#ifndef NDEBUG
5400 else {
5401 qCWarning(OkularCoreDebug) << "Receiving a done request for the defunct observer" << observer;
5402 }
5403#endif
5404 }
5405
5406 // 3. delete request
5407 m_pixmapRequestsMutex.lock();
5408 m_executingPixmapRequests.remove(req);
5409 m_pixmapRequestsMutex.unlock();
5410 delete req;
5411
5412 // 4. start a new generation if some is pending
5413 m_pixmapRequestsMutex.lock();
5414 bool hasPixmaps = !m_pixmapRequestsStack.empty();
5415 m_pixmapRequestsMutex.unlock();
5416 if (hasPixmaps) {
5417 sendGeneratorPixmapRequest();
5418 }
5419}
5420
5421void DocumentPrivate::setPageBoundingBox(int page, const NormalizedRect &boundingBox)
5422{
5423 Page *kp = m_pagesVector[page];
5424 if (!m_generator || !kp) {
5425 return;
5426 }
5427
5428 if (kp->boundingBox() == boundingBox) {
5429 return;
5430 }
5431 kp->setBoundingBox(boundingBox);
5432
5433 // notify observers about the change
5434 foreachObserverD(notifyPageChanged(page, DocumentObserver::BoundingBox));
5435
5436 // 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.
5437 // TODO: Crop computation should also consider annotations, actions, etc. to make sure they're not cropped away.
5438 // TODO: Help compute bounding box for generators that create a QPixmap without a QImage, like text and plucker.
5439 // TODO: Don't compute the bounding box if no one needs it (e.g., Trim Borders is off).
5440}
5441
5442void DocumentPrivate::calculateMaxTextPages()
5443{
5444 int multipliers = qMax(1, qRound(getTotalMemory() / 536870912.0)); // 512 MB
5445 switch (SettingsCore::memoryLevel()) {
5446 case SettingsCore::EnumMemoryLevel::Low:
5447 m_maxAllocatedTextPages = multipliers * 2;
5448 break;
5449
5450 case SettingsCore::EnumMemoryLevel::Normal:
5451 m_maxAllocatedTextPages = multipliers * 50;
5452 break;
5453
5454 case SettingsCore::EnumMemoryLevel::Aggressive:
5455 m_maxAllocatedTextPages = multipliers * 250;
5456 break;
5457
5458 case SettingsCore::EnumMemoryLevel::Greedy:
5459 m_maxAllocatedTextPages = multipliers * 1250;
5460 break;
5461 }
5462}
5463
5464void DocumentPrivate::textGenerationDone(Page *page)
5465{
5466 if (!m_pageController) {
5467 return;
5468 }
5469
5470 // 1. If we reached the cache limit, delete the first text page from the fifo
5471 if (m_allocatedTextPagesFifo.size() == m_maxAllocatedTextPages) {
5472 int pageToKick = m_allocatedTextPagesFifo.takeFirst();
5473 if (pageToKick != page->number()) // this should never happen but better be safe than sorry
5474 {
5475 m_pagesVector.at(pageToKick)->setTextPage(nullptr); // deletes the textpage
5476 }
5477 }
5478
5479 // 2. Add the page to the fifo of generated text pages
5480 m_allocatedTextPagesFifo.append(page->number());
5481}
5482
5484{
5485 d->setRotationInternal(r, true);
5486}
5487
5488void DocumentPrivate::setRotationInternal(int r, bool notify)
5489{
5490 Rotation rotation = (Rotation)r;
5491 if (!m_generator || (m_rotation == rotation)) {
5492 return;
5493 }
5494
5495 // tell the pages to rotate
5496 QVector<Okular::Page *>::const_iterator pIt = m_pagesVector.constBegin();
5497 QVector<Okular::Page *>::const_iterator pEnd = m_pagesVector.constEnd();
5498 for (; pIt != pEnd; ++pIt) {
5499 (*pIt)->d->rotateAt(rotation);
5500 }
5501 if (notify) {
5502 // notify the generator that the current rotation has changed
5503 m_generator->rotationChanged(rotation, m_rotation);
5504 }
5505 // set the new rotation
5506 m_rotation = rotation;
5507
5508 if (notify) {
5509 foreachObserverD(notifySetup(m_pagesVector, DocumentObserver::NewLayoutForPages));
5510 foreachObserverD(notifyContentsCleared(DocumentObserver::Pixmap | DocumentObserver::Highlights | DocumentObserver::Annotations));
5511 }
5512 qCDebug(OkularCoreDebug) << "Rotated:" << r;
5513}
5514
5516{
5517 if (!d->m_generator || !d->m_generator->hasFeature(Generator::PageSizes)) {
5518 return;
5519 }
5520
5521 if (d->m_pageSizes.isEmpty()) {
5522 d->m_pageSizes = d->m_generator->pageSizes();
5523 }
5524 int sizeid = d->m_pageSizes.indexOf(size);
5525 if (sizeid == -1) {
5526 return;
5527 }
5528
5529 // tell the pages to change size
5530 QVector<Okular::Page *>::const_iterator pIt = d->m_pagesVector.constBegin();
5531 QVector<Okular::Page *>::const_iterator pEnd = d->m_pagesVector.constEnd();
5532 for (; pIt != pEnd; ++pIt) {
5533 (*pIt)->d->changeSize(size);
5534 }
5535 // clear 'memory allocation' descriptors
5536 qDeleteAll(d->m_allocatedPixmaps);
5537 d->m_allocatedPixmaps.clear();
5538 d->m_allocatedPixmapsTotalMemory = 0;
5539 // notify the generator that the current page size has changed
5540 d->m_generator->pageSizeChanged(size, d->m_pageSize);
5541 // set the new page size
5542 d->m_pageSize = size;
5543
5544 foreachObserver(notifySetup(d->m_pagesVector, DocumentObserver::NewLayoutForPages));
5545 foreachObserver(notifyContentsCleared(DocumentObserver::Pixmap | DocumentObserver::Highlights));
5546 qCDebug(OkularCoreDebug) << "New PageSize id:" << sizeid;
5547}
5548
5549/** DocumentViewport **/
5550
5552 : pageNumber(n)
5553{
5554 // default settings
5555 rePos.enabled = false;
5556 rePos.normalizedX = 0.5;
5557 rePos.normalizedY = 0.0;
5558 rePos.pos = Center;
5559 autoFit.enabled = false;
5560 autoFit.width = false;
5561 autoFit.height = false;
5562}
5563
5565 : pageNumber(-1)
5566{
5567 // default settings (maybe overridden below)
5568 rePos.enabled = false;
5569 rePos.normalizedX = 0.5;
5570 rePos.normalizedY = 0.0;
5571 rePos.pos = Center;
5572 autoFit.enabled = false;
5573 autoFit.width = false;
5574 autoFit.height = false;
5575
5576 // check for string presence
5577 if (xmlDesc.isEmpty()) {
5578 return;
5579 }
5580
5581 // decode the string
5582 bool ok;
5583 int field = 0;
5584 QString token = xmlDesc.section(QLatin1Char(';'), field, field);
5585 while (!token.isEmpty()) {
5586 // decode the current token
5587 if (field == 0) {
5588 pageNumber = token.toInt(&ok);
5589 if (!ok) {
5590 return;
5591 }
5592 } else if (token.startsWith(QLatin1String("C1"))) {
5593 rePos.enabled = true;
5594 rePos.normalizedX = token.section(QLatin1Char(':'), 1, 1).toDouble();
5595 rePos.normalizedY = token.section(QLatin1Char(':'), 2, 2).toDouble();
5596 rePos.pos = Center;
5597 } else if (token.startsWith(QLatin1String("C2"))) {
5598 rePos.enabled = true;
5599 rePos.normalizedX = token.section(QLatin1Char(':'), 1, 1).toDouble();
5600 rePos.normalizedY = token.section(QLatin1Char(':'), 2, 2).toDouble();
5601 if (token.section(QLatin1Char(':'), 3, 3).toInt() == 1) {
5602 rePos.pos = Center;
5603 } else {
5604 rePos.pos = TopLeft;
5605 }
5606 } else if (token.startsWith(QLatin1String("AF1"))) {
5607 autoFit.enabled = true;
5608 autoFit.width = token.section(QLatin1Char(':'), 1, 1) == QLatin1String("T");
5609 autoFit.height = token.section(QLatin1Char(':'), 2, 2) == QLatin1String("T");
5610 }
5611 // proceed tokenizing string
5612 field++;
5613 token = xmlDesc.section(QLatin1Char(';'), field, field);
5614 }
5615}
5616
5618{
5619 // start string with page number
5621 // if has center coordinates, save them on string
5622 if (rePos.enabled) {
5623 s += QStringLiteral(";C2:") + QString::number(rePos.normalizedX) + QLatin1Char(':') + QString::number(rePos.normalizedY) + QLatin1Char(':') + QString::number(rePos.pos);
5624 }
5625 // if has autofit enabled, save its state on string
5626 if (autoFit.enabled) {
5627 s += QStringLiteral(";AF1:") + (autoFit.width ? QLatin1Char('T') : QLatin1Char('F')) + QLatin1Char(':') + (autoFit.height ? QLatin1Char('T') : QLatin1Char('F'));
5628 }
5629 return s;
5630}
5631
5633{
5634 return pageNumber >= 0;
5635}
5636
5638{
5639 bool equal = (pageNumber == other.pageNumber) && (rePos.enabled == other.rePos.enabled) && (autoFit.enabled == other.autoFit.enabled);
5640 if (!equal) {
5641 return false;
5642 }
5643 if (rePos.enabled && ((rePos.normalizedX != other.rePos.normalizedX) || (rePos.normalizedY != other.rePos.normalizedY) || rePos.pos != other.rePos.pos)) {
5644 return false;
5645 }
5646 if (autoFit.enabled && ((autoFit.width != other.autoFit.width) || (autoFit.height != other.autoFit.height))) {
5647 return false;
5648 }
5649 return true;
5650}
5651
5652bool DocumentViewport::operator<(const DocumentViewport &other) const
5653{
5654 // TODO: Check autoFit and Position
5655
5656 if (pageNumber != other.pageNumber) {
5657 return pageNumber < other.pageNumber;
5658 }
5659
5660 if (!rePos.enabled && other.rePos.enabled) {
5661 return true;
5662 }
5663
5664 if (!other.rePos.enabled) {
5665 return false;
5666 }
5667
5668 if (rePos.normalizedY != other.rePos.normalizedY) {
5669 return rePos.normalizedY < other.rePos.normalizedY;
5670 }
5671
5672 return rePos.normalizedX < other.rePos.normalizedX;
5673}
5674
5675/** DocumentInfo **/
5676
5678 : d(new DocumentInfoPrivate())
5679{
5680}
5681
5683 : d(new DocumentInfoPrivate())
5684{
5685 *this = info;
5686}
5687
5688DocumentInfo &DocumentInfo::operator=(const DocumentInfo &info)
5689{
5690 if (this != &info) {
5691 d->values = info.d->values;
5692 d->titles = info.d->titles;
5693 }
5694 return *this;
5695}
5696
5697DocumentInfo::~DocumentInfo()
5698{
5699 delete d;
5700}
5701
5702void DocumentInfo::set(const QString &key, const QString &value, const QString &title)
5703{
5704 d->values[key] = value;
5705 d->titles[key] = title;
5706}
5707
5708void DocumentInfo::set(Key key, const QString &value)
5709{
5710 d->values[getKeyString(key)] = value;
5711}
5712
5714{
5715 return d->values.keys();
5716}
5717
5719{
5720 return get(getKeyString(key));
5721}
5722
5724{
5725 return d->values[key];
5726}
5727
5729{
5730 switch (key) {
5731 case Title:
5732 return QStringLiteral("title");
5733 break;
5734 case Subject:
5735 return QStringLiteral("subject");
5736 break;
5737 case Description:
5738 return QStringLiteral("description");
5739 break;
5740 case Author:
5741 return QStringLiteral("author");
5742 break;
5743 case Creator:
5744 return QStringLiteral("creator");
5745 break;
5746 case Producer:
5747 return QStringLiteral("producer");
5748 break;
5749 case Copyright:
5750 return QStringLiteral("copyright");
5751 break;
5752 case Pages:
5753 return QStringLiteral("pages");
5754 break;
5755 case CreationDate:
5756 return QStringLiteral("creationDate");
5757 break;
5758 case ModificationDate:
5759 return QStringLiteral("modificationDate");
5760 break;
5761 case MimeType:
5762 return QStringLiteral("mimeType");
5763 break;
5764 case Category:
5765 return QStringLiteral("category");
5766 break;
5767 case Keywords:
5768 return QStringLiteral("keywords");
5769 break;
5770 case FilePath:
5771 return QStringLiteral("filePath");
5772 break;
5773 case DocumentSize:
5774 return QStringLiteral("documentSize");
5775 break;
5776 case PagesSize:
5777 return QStringLiteral("pageSize");
5778 break;
5779 default:
5780 qCWarning(OkularCoreDebug) << "Unknown" << key;
5781 return QString();
5782 break;
5783 }
5784}
5785
5787{
5788 if (key == QLatin1String("title")) {
5789 return Title;
5790 } else if (key == QLatin1String("subject")) {
5791 return Subject;
5792 } else if (key == QLatin1String("description")) {
5793 return Description;
5794 } else if (key == QLatin1String("author")) {
5795 return Author;
5796 } else if (key == QLatin1String("creator")) {
5797 return Creator;
5798 } else if (key == QLatin1String("producer")) {
5799 return Producer;
5800 } else if (key == QLatin1String("copyright")) {
5801 return Copyright;
5802 } else if (key == QLatin1String("pages")) {
5803 return Pages;
5804 } else if (key == QLatin1String("creationDate")) {
5805 return CreationDate;
5806 } else if (key == QLatin1String("modificationDate")) {
5807 return ModificationDate;
5808 } else if (key == QLatin1String("mimeType")) {
5809 return MimeType;
5810 } else if (key == QLatin1String("category")) {
5811 return Category;
5812 } else if (key == QLatin1String("keywords")) {
5813 return Keywords;
5814 } else if (key == QLatin1String("filePath")) {
5815 return FilePath;
5816 } else if (key == QLatin1String("documentSize")) {
5817 return DocumentSize;
5818 } else if (key == QLatin1String("pageSize")) {
5819 return PagesSize;
5820 } else {
5821 return Invalid;
5822 }
5823}
5824
5826{
5827 switch (key) {
5828 case Title:
5829 return i18n("Title");
5830 break;
5831 case Subject:
5832 return i18n("Subject");
5833 break;
5834 case Description:
5835 return i18n("Description");
5836 break;
5837 case Author:
5838 return i18n("Author");
5839 break;
5840 case Creator:
5841 return i18n("Creator");
5842 break;
5843 case Producer:
5844 return i18n("Producer");
5845 break;
5846 case Copyright:
5847 return i18n("Copyright");
5848 break;
5849 case Pages:
5850 return i18n("Pages");
5851 break;
5852 case CreationDate:
5853 return i18n("Created");
5854 break;
5855 case ModificationDate:
5856 return i18n("Modified");
5857 break;
5858 case MimeType:
5859 return i18n("MIME Type");
5860 break;
5861 case Category:
5862 return i18n("Category");
5863 break;
5864 case Keywords:
5865 return i18n("Keywords");
5866 break;
5867 case FilePath:
5868 return i18n("File Path");
5869 break;
5870 case DocumentSize:
5871 return i18n("File Size");
5872 break;
5873 case PagesSize:
5874 return i18n("Page Size");
5875 break;
5876 default:
5877 return QString();
5878 break;
5879 }
5880}
5881
5883{
5884 QString title = getKeyTitle(getKeyFromString(key));
5885 if (title.isEmpty()) {
5886 title = d->titles[key];
5887 }
5888 return title;
5889}
5890
5891/** DocumentSynopsis **/
5892
5894 : QDomDocument(QStringLiteral("DocumentSynopsis"))
5895{
5896 // void implementation, only subclassed for naming
5897}
5898
5900 : QDomDocument(document)
5901{
5902}
5903
5904/** EmbeddedFile **/
5905
5909
5913
5915 : pageNumber(page)
5916 , rect(rectangle)
5917{
5918}
5919
5920/** NewSignatureData **/
5921
5922struct Okular::NewSignatureDataPrivate {
5923 NewSignatureDataPrivate() = default;
5924
5925 QString certNickname;
5926 QString certSubjectCommonName;
5927 QString password;
5928 QString documentPassword;
5929 QString location;
5930 QString reason;
5931 QString backgroundImagePath;
5932 int page = -1;
5933 NormalizedRect boundingRectangle;
5934};
5935
5936NewSignatureData::NewSignatureData()
5937 : d(new NewSignatureDataPrivate())
5938{
5939}
5940
5941NewSignatureData::~NewSignatureData()
5942{
5943 delete d;
5944}
5945
5946QString NewSignatureData::certNickname() const
5947{
5948 return d->certNickname;
5949}
5950
5951void NewSignatureData::setCertNickname(const QString &certNickname)
5952{
5953 d->certNickname = certNickname;
5954}
5955
5956QString NewSignatureData::certSubjectCommonName() const
5957{
5958 return d->certSubjectCommonName;
5959}
5960
5961void NewSignatureData::setCertSubjectCommonName(const QString &certSubjectCommonName)
5962{
5963 d->certSubjectCommonName = certSubjectCommonName;
5964}
5965
5966QString NewSignatureData::password() const
5967{
5968 return d->password;
5969}
5970
5971void NewSignatureData::setPassword(const QString &password)
5972{
5973 d->password = password;
5974}
5975
5976int NewSignatureData::page() const
5977{
5978 return d->page;
5979}
5980
5981void NewSignatureData::setPage(int page)
5982{
5983 d->page = page;
5984}
5985
5986NormalizedRect NewSignatureData::boundingRectangle() const
5987{
5988 return d->boundingRectangle;
5989}
5990
5991void NewSignatureData::setBoundingRectangle(const NormalizedRect &rect)
5992{
5993 d->boundingRectangle = rect;
5994}
5995
5997{
5998 return d->documentPassword;
5999}
6000
6002{
6003 d->documentPassword = password;
6004}
6005
6007{
6008 return d->location;
6009}
6010
6012{
6013 d->location = location;
6014}
6015
6017{
6018 return d->reason;
6019}
6020
6022{
6023 d->reason = reason;
6024}
6025
6027{
6028 return d->backgroundImagePath;
6029}
6030
6032{
6033 d->backgroundImagePath = path;
6034}
6035
6036#undef foreachObserver
6037#undef foreachObserverD
6038
6039#include "document.moc"
6040
6041/* 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()
QWidget * 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 * zoom(const QObject *recvr, const char *slot, QObject *parent)
const QList< QKeySequence > & forward()
const QList< QKeySequence > & end()
QString name(StandardShortcut id)
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
QWidget * window() const const
QWindow * windowHandle() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:17:35 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.