Fix forward navigation control on Linux.
[dorian] / bookview.cpp
1 #include <QDir>
2 #include <QtGui>
3 #include <QWebFrame>
4
5 #if defined(Q_OS_SYMBIAN)
6 #   include "mediakeysobserver.h"
7 #   include "flickcharm.h"
8 #endif
9
10 #include "book.h"
11 #include "bookview.h"
12 #include "library.h"
13 #include "settings.h"
14 #include "trace.h"
15 #include "progress.h"
16 #include "progressdialog.h"
17 #include "platform.h"
18
19 BookView::BookView(QWidget *parent): QWebView(parent), contentIndex(-1),
20     mBook(0), restorePositionAfterLoad(false), positionAfterLoad(0),
21     restoreFragmentAfterLoad(false), loaded(false), grabbingVolumeKeys(false)
22 {
23     TRACE;
24
25     // Create timer for scheduling restores
26     restoreTimer = new QTimer(this);
27
28     // Set up web view defaults
29     settings()->setAttribute(QWebSettings::AutoLoadImages, true);
30     settings()->setAttribute(QWebSettings::JavascriptEnabled, true);
31     settings()->setAttribute(QWebSettings::JavaEnabled, false);
32     settings()->setAttribute(QWebSettings::PluginsEnabled, false);
33     settings()->setAttribute(QWebSettings::PrivateBrowsingEnabled, true);
34     settings()->setAttribute(QWebSettings::JavascriptCanOpenWindows, false);
35     settings()->setAttribute(QWebSettings::JavascriptCanAccessClipboard,
36                              false);
37     settings()->setAttribute(QWebSettings::OfflineStorageDatabaseEnabled,
38                              false);
39     settings()->setAttribute(QWebSettings::OfflineWebApplicationCacheEnabled,
40                              false);
41     settings()->setAttribute(QWebSettings::LocalStorageEnabled, false);
42     settings()->setAttribute(QWebSettings::ZoomTextOnly, true);
43     settings()->setAttribute(QWebSettings::LocalContentCanAccessRemoteUrls,
44                              false);
45     settings()->setDefaultTextEncoding("utf-8");
46     page()->setContentEditable(false);
47     QWebFrame *frame = page()->mainFrame();
48 #if defined(Q_WS_MAEMO_5) || defined(Q_OS_SYMBIAN)
49     frame->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff);
50 #endif
51     frame->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAlwaysOff);
52     connect(this, SIGNAL(loadFinished(bool)),
53             this, SLOT(onLoadFinished(bool)));
54     connect(frame, SIGNAL(javaScriptWindowObjectCleared()),
55             this, SLOT(addJavaScriptObjects()));
56
57     // Suppress unwanted text selections on Maemo and Symbian
58 #if defined(Q_WS_MAEMO_5) || defined(Q_OS_SYMBIAN)
59     installEventFilter(this);
60 #endif
61
62     // Pre-load bookmark icon
63     bookmarkImage = QImage(":/icons/bookmark.png");
64
65     // Handle settings changes, force handling initial settings
66     connect(Settings::instance(), SIGNAL(valueChanged(const QString &)),
67             this, SLOT(onSettingsChanged(const QString &)));
68     setBook(0);
69
70     // Enable kinetic scrolling
71 #if defined(Q_WS_MAEMO_5)
72     scrollerMonitor = 0;
73     scroller = property("kineticScroller").value<QAbstractKineticScroller *>();
74 #elif defined(Q_OS_SYMBIAN)
75     scrollerMonitor = 0;
76     charm = new FlickCharm(this);
77     charm->activateOn(this);
78 #endif
79
80     // Observe media keys on Symbian
81 #ifdef Q_OS_SYMBIAN
82     MediaKeysObserver *observer = MediaKeysObserver::instance();
83     connect(observer, SIGNAL(mediaKeyPressed(MediaKeysObserver::MediaKeys)),
84             this, SLOT(onMediaKeysPressed(MediaKeysObserver::MediaKeys)));
85 #endif
86 }
87
88 void BookView::loadContent(int index)
89 {
90     TRACE;
91
92     if (!mBook) {
93         return;
94     }
95     if ((index < 0) || (index >= mBook->parts.size())) {
96         return;
97     }
98
99     QString contentFile(mBook->content[mBook->parts[index]].href);
100     if (mBook->parts[index] == "error") {
101         setHtml(contentFile);
102     } else {
103         loaded = false;
104         emit partLoadStart(index);
105         QUrl u = QUrl::fromLocalFile(QDir(mBook->rootPath()).
106                                      absoluteFilePath(contentFile));
107         qDebug() << "Loading" << u;
108         load(u);
109     }
110     contentIndex = index;
111 }
112
113 void BookView::setBook(Book *book)
114 {
115     TRACE;
116
117     // Bail out if new book is the same as current book
118     if (book == mBook) {
119         return;
120     }
121
122     // Save reading position of current book
123     setLastBookmark();
124
125     // Set new book as the current book
126     mBook = book;
127
128     // Bail out if new book is null
129     if (!book) {
130         contentIndex = 0;
131         setHtml(tr("No book"));
132         return;
133     }
134
135     // Open new book
136     if (book->open()) {
137         // Restore last reading position - this will force
138         // a reload as well
139         contentIndex = -1;
140         restoreLastBookmark();
141     } else {
142         mBook = 0;
143         contentIndex = 0;
144         setHtml(tr("Failed to open book"));
145     }
146 }
147
148 Book *BookView::book()
149 {
150     return mBook;
151 }
152
153 void BookView::goPrevious()
154 {
155     TRACE;
156     if (mBook && (contentIndex > 0)) {
157         mBook->setLastBookmark(contentIndex - 1, 0);
158         loadContent(contentIndex - 1);
159     }
160 }
161
162 void BookView::goNext()
163 {
164     TRACE;
165     if (mBook && (contentIndex < (mBook->parts.size() - 1))) {
166         mBook->setLastBookmark(contentIndex + 1, 0);
167         loadContent(contentIndex + 1);
168     }
169 }
170
171 void BookView::setLastBookmark(bool fast)
172 {
173     TRACE;
174     if (mBook) {
175         QWebFrame *frame = page()->mainFrame();
176         int height = frame->contentsSize().height();
177         int pos = frame->scrollPosition().y();
178         qDebug() << QString("At %1 (%2%, height %3)").
179                 arg(pos).arg((qreal)pos / (qreal)height * 100).arg(height);
180         mBook->setLastBookmark(contentIndex, (qreal)pos / (qreal)height, fast);
181     } else {
182         qDebug() << "(no book)";
183     }
184 }
185
186 void BookView::restoreLastBookmark()
187 {
188     TRACE;
189     if (mBook) {
190         goToBookmark(mBook->lastBookmark());
191     }
192 }
193
194 void BookView::goToBookmark(const Book::Bookmark &bookmark)
195 {
196     TRACE;
197     if (mBook) {
198         if (bookmark.part != contentIndex) {
199             qDebug () << "Loading new part" << bookmark.part;
200             mBook->setLastBookmark(bookmark.part, bookmark.pos);
201             restorePositionAfterLoad = true;
202             positionAfterLoad = bookmark.pos;
203             loadContent(bookmark.part);
204         } else {
205             emit partLoadEnd(contentIndex);
206             goToPosition(bookmark.pos);
207         }
208     }
209 }
210
211 void BookView::goToPart(int part, const QString &fragment)
212 {
213     TRACE;
214     if (mBook) {
215         if (fragment.isEmpty()) {
216             goToBookmark(Book::Bookmark(part, 0));
217         } else {
218             if (part != contentIndex) {
219                 qDebug() << "Loading new part" << part;
220                 restoreFragmentAfterLoad = true;
221                 fragmentAfterLoad = fragment;
222                 loadContent(part);
223             } else {
224                 goToFragment(fragment);
225                 showProgress();
226             }
227         }
228     }
229 }
230
231 void BookView::goToFragment(const QString &fragment)
232 {
233     TRACE;
234     if (!fragment.isEmpty()) {
235         QVariant ret = page()->mainFrame()->evaluateJavaScript(
236                 QString("window.location='") + fragment + "'");
237         qDebug() << ret;
238     }
239 }
240
241 void BookView::onLoadFinished(bool ok)
242 {
243     TRACE;
244     if (!ok) {
245         qDebug() << "Not OK";
246         return;
247     }
248     loaded = true;
249     onSettingsChanged("scheme");
250     onSettingsChanged("zoom");
251     onSettingsChanged("font");
252     scheduleRestoreAfterLoad();
253 }
254
255 void BookView::scheduleRestoreAfterLoad()
256 {
257     TRACE;
258     if (restoreTimer->isActive()) {
259         // Ignore request if a restore is already in progress
260         return;
261     }
262
263     disconnect(restoreTimer, SIGNAL(timeout()), this, 0);
264     connect(restoreTimer, SIGNAL(timeout()), this, SLOT(restoreAfterLoad()));
265     restoreTimer->setSingleShot(true);
266     restoreTimer->start(210);
267 }
268
269 void BookView::scheduleRestoreLastBookmark()
270 {
271     TRACE;
272     if (restoreTimer->isActive()) {
273         // Ignore request if a restore is already in progress
274         return;
275     }
276
277     disconnect(restoreTimer, SIGNAL(timeout()), this, 0);
278     connect(restoreTimer, SIGNAL(timeout()), this,
279             SLOT(restoreLastBookmark()));
280     restoreTimer->setSingleShot(true);
281     restoreTimer->start(210);
282 }
283
284 void BookView::restoreAfterLoad()
285 {
286     TRACE;
287     if (restoreFragmentAfterLoad) {
288         qDebug() << "Restorint to fragment" << fragmentAfterLoad;
289         goToFragment(fragmentAfterLoad);
290         restoreFragmentAfterLoad = false;
291     } else if (restorePositionAfterLoad) {
292         qDebug() << "Restoring to position" << positionAfterLoad;
293         goToPosition(positionAfterLoad);
294         restorePositionAfterLoad = false;
295     }
296
297     emit partLoadEnd(contentIndex);
298     showProgress();
299 }
300
301 void BookView::onSettingsChanged(const QString &key)
302 {
303     Settings *s = Settings::instance();
304     Platform *p = Platform::instance();
305
306     if (key == "zoom") {
307         int value = s->value(key, p->defaultZoom()).toInt();
308         qDebug() << "BookView::onSettingsChanged: zoom" << value;
309         setZoomFactor(value / 100.);
310     }
311     else if (key == "font") {
312         QString face = s->value(key, p->defaultFont()).toString();
313         qDebug() << "BookView::onSettingsChanged: font" << face;
314         settings()->setFontFamily(QWebSettings::StandardFont, face);
315     }
316     else if (key == "scheme") {
317         QWebFrame *frame = page()->mainFrame();
318         QString scheme = Settings::instance()->value("scheme").toString();
319         if ((scheme != "day") && (scheme != "night") && (scheme != "sand") &&
320             (scheme != "default")) {
321             scheme = "default";
322         }
323         qDebug() << "BookView::onSettingsChanged: scheme" << scheme;
324         QFile script(":/styles/" + scheme + ".js");
325         script.open(QFile::ReadOnly);
326         QString scriptText = script.readAll();
327         script.close();
328         (void)frame->evaluateJavaScript(scriptText);
329     }
330     else if (key == "usevolumekeys") {
331         bool grab = s->value(key, false).toBool();
332         qDebug() << "BookView::onSettingsChanged: usevolumekeys" << grab;
333         grabVolumeKeys(grab);
334     }
335 }
336
337 void BookView::paintEvent(QPaintEvent *e)
338 {
339     QWebView::paintEvent(e);
340     if (!mBook || !loaded) {
341         return;
342     }
343
344     // Paint bookmarks
345     QWebFrame *frame = page()->mainFrame();
346     int contentsHeight = frame->contentsSize().height();
347     QPoint scrollPos = frame->scrollPosition();
348     QPixmap bookmarkPixmap = QPixmap::fromImage(bookmarkImage);
349     QPainter painter(this);
350     foreach (Book::Bookmark b, mBook->bookmarks()) {
351         if (b.part != contentIndex) {
352             continue;
353         }
354         int height = contentsHeight;
355         int bookmarkPos = (int)((qreal)height * (qreal)b.pos);
356         painter.drawPixmap(2, bookmarkPos - scrollPos.y(), bookmarkPixmap);
357     }
358
359     // Paint page separator(s)
360     QPen pen(Qt::gray);
361     pen.setStyle(Qt::DotLine);
362     pen.setWidth(3);
363     painter.setPen(pen);
364     if (contentIndex > 0) {
365         painter.drawLine(0, -scrollPos.y(), width(), -scrollPos.y());
366     }
367     if (contentIndex < (mBook->parts.size() - 1)) {
368         int h = contentsHeight - scrollPos.y() - 1;
369         painter.drawLine(0, h, width(), h);
370     }
371 }
372
373 void BookView::mousePressEvent(QMouseEvent *e)
374 {
375     QWebView::mousePressEvent(e);
376 #if defined(Q_WS_MAEMO_5)
377     // Start monitoring kinetic scroll
378     if (scrollerMonitor) {
379         killTimer(scrollerMonitor);
380         scrollerMonitor = 0;
381     }
382     if (scroller) {
383         scrollerMonitor = startTimer(500);
384     }
385 #elif defined(Q_OS_SYMBIAN)
386     // Do nothing
387 #else
388     // Handle mouse press on the scroll bar
389     QWebFrame *frame = page()->mainFrame();
390     if (frame->scrollBarGeometry(Qt::Vertical).contains(e->pos())) {
391         e->accept();
392         return;
393     }
394 #endif
395     e->ignore();
396 }
397
398 void BookView::wheelEvent(QWheelEvent *e)
399 {
400     QWebView::wheelEvent(e);
401     showProgress();
402 }
403
404 void BookView::addBookmark(const QString &note)
405 {
406     TRACE;
407     if (!mBook) {
408         return;
409     }
410     int y = page()->mainFrame()->scrollPosition().y();
411     int height = page()->mainFrame()->contentsSize().height();
412     qDebug() << ((qreal)y / (qreal)height);
413     mBook->addBookmark(contentIndex, (qreal)y / (qreal)height, note);
414     update();
415 }
416
417 QString BookView::tmpPath()
418 {
419     return QDir::tempPath() + "/dorian";
420 }
421
422 bool BookView::eventFilter(QObject *o, QEvent *e)
423 {
424 #if 0
425     if (e->type() != QEvent::Paint && e->type() != QEvent::MouseMove) {
426         if (e->type() == QEvent::Resize) {
427             qDebug() << "BookView::eventFilter QEvent::Resize to"
428                     << page()->mainFrame()->contentsSize().height();
429         } else if (e->type() == QEvent::Timer) {
430             qDebug() << "BookView::eventFilter" << "QEvent::Timer"
431                     << ((QTimerEvent *)e)->timerId();
432         } else {
433             qDebug() << "BookView::eventFilter" << Trace::event(e->type());
434         }
435     }
436 #endif
437
438     // Work around Qt bug that sometimes selects web view contents during swipe
439     switch (e->type()) {
440     case QEvent::MouseButtonPress:
441         mousePressed = true;
442         break;
443     case QEvent::MouseButtonRelease:
444 #ifndef Q_OS_SYMBIAN // Too heavy on Symbian
445         showProgress();
446 #endif
447         mousePressed = false;
448         break;
449     case QEvent::MouseMove:
450         if (mousePressed) {
451             return true;
452         }
453         break;
454     case QEvent::MouseButtonDblClick:
455         return true;
456     default:
457         break;
458     }
459
460     return QObject::eventFilter(o, e);
461 }
462
463 void BookView::addJavaScriptObjects()
464 {
465     TRACE;
466     page()->mainFrame()->addToJavaScriptWindowObject("bv", this);
467 }
468
469 void BookView::goToPosition(qreal position)
470 {
471     TRACE;
472     int contentsHeight = page()->mainFrame()->contentsSize().height();
473     int scrollPos = (int)((qreal)contentsHeight * position);
474     page()->mainFrame()->setScrollPosition(QPoint(0, scrollPos));
475     // FIXME: update();
476     qDebug() << "BookView::goToPosition: To" << scrollPos << "("
477             << (position * 100) << "%, height" << contentsHeight << ")";
478 }
479
480 void BookView::showProgress()
481 {
482     TRACE;
483     if (mBook) {
484         int contentsHeight = page()->mainFrame()->contentsSize().height();
485         qreal pos = (qreal)(page()->mainFrame()->scrollPosition().y()) /
486                     (qreal)contentsHeight;
487         emit progress(mBook->getProgress(contentIndex, pos));
488     }
489 }
490
491 void BookView::timerEvent(QTimerEvent *e)
492 {
493 #if defined(Q_WS_MAEMO_5)
494     if (e->timerId() == scrollerMonitor) {
495         if (scroller &&
496             ((scroller->state() == QAbstractKineticScroller::AutoScrolling) ||
497              (scroller->state() == QAbstractKineticScroller::Pushing))) {
498             showProgress();
499         } else {
500             killTimer(scrollerMonitor);
501             scrollerMonitor = -1;
502         }
503     }
504 #endif // Q_WS_MAEMO_5
505
506     QWebView::timerEvent(e);
507 }
508
509 void BookView::hideEvent(QHideEvent *e)
510 {
511     Trace t("BookView::hideEvent");
512
513 #if defined(Q_OS_SYMBIAN)
514     setLastBookmark();
515 #endif
516
517     QWebView::hideEvent(e);
518 }
519
520 void BookView::goPreviousPage()
521 {
522     QWebFrame *frame = page()->mainFrame();
523     const int pos = frame->scrollPosition().y();
524     frame->scroll(0, -1);
525     if (pos == frame->scrollPosition().y()) {
526         if (contentIndex > 0) {
527             Book::Bookmark bookmark(contentIndex - 1, 1.0);
528             mBook->setLastBookmark(contentIndex - 1, 1.0);
529             goToBookmark(bookmark);
530         }
531     } else {
532         showProgress();
533         QPropertyAnimation *slide =
534                 new QPropertyAnimation(frame, "scrollPosition");
535         const QPoint *offset = new QPoint(0, height() - 18);
536         slide->setDuration(400);
537         slide->setStartValue(frame->scrollPosition());
538         slide->setEndValue(frame->scrollPosition() - *offset);
539         slide->setEasingCurve(QEasingCurve::OutQuad);
540         slide->start(QAbstractAnimation::DeleteWhenStopped);
541         delete offset;
542     }
543 }
544
545 void BookView::goNextPage()
546 {
547     TRACE;
548     QWebFrame *frame = page()->mainFrame();
549     const int pos = frame->scrollPosition().y();
550     frame->scroll(0, 1);
551     if (pos == frame->scrollPosition().y()) {
552         goNext();
553     } else {
554         showProgress();
555         QPropertyAnimation *slide =
556                 new QPropertyAnimation(frame, "scrollPosition");
557         const QPoint *offset = new QPoint(0, (height() - 18));
558         slide->setDuration(400);
559         slide->setStartValue(frame->scrollPosition());
560         slide->setEndValue(frame->scrollPosition() + *offset);
561         slide->setEasingCurve(QEasingCurve::OutQuad);
562         slide->start(QAbstractAnimation::DeleteWhenStopped);
563         delete offset;
564     }
565 }
566
567 void BookView::grabVolumeKeys(bool grab)
568 {
569     TRACE;
570     grabbingVolumeKeys = grab;
571 }
572
573 #ifdef Q_OS_SYMBIAN
574
575 void BookView::onMediaKeysPressed(MediaKeysObserver::MediaKeys key)
576 {
577     TRACE;
578     qDebug() << "Key" << (int)key;
579     if (grabbingVolumeKeys) {
580         if (key == MediaKeysObserver::EVolIncKey) {
581             qDebug() << "Volume up";
582             goPreviousPage();
583         } else if (key == MediaKeysObserver::EVolDecKey){
584             qDebug() << "Volume down";
585             goNextPage();
586         }
587     }
588 }
589
590 #endif // Q_OS_SYMBIAN