[Okular-devel] Redrawing the screen due to optional content group changes

Jeremy Maitin-Shepard jeremy at jeremyms.com
Thu Jul 16 01:10:03 UTC 2015


I've started to add sufficient Javascript support in order for animate.sty
latex animations to work.  In my opinion this is quite valuable
functionality since I am not aware of any way to make PDF animations that
can be displayed in a free software PDF viewer.

animate.sty supports 3 modes for generating the animation:

1. icon mode: This requires being able to look up an icon (corresponding to
the frames of the animation) by name via getIcon, and to change the icon
associated with a form widget using buttonSetIcon.  I'm not exactly sure
what an icon is, and poppler doesn't seem to support any of these
operations, so this method doesn't seem very promising.

2. widget-based: This requires being able to change the visibility of a
form widget.  Again, poppler doesn't seem to support this, so this method
isn't very promising.

3. optional content group (ocg)-based: This requires being able to set the
visibility of optional content groups.  Poppler does in fact have some (not
well-documented) support for this.

I implemented the necessary Javascript interface for optional content
groups, via the QAbstractItemModel interface provided in
poppler-optcontent.  I also implemented the necessary Javascript timer
functionality.  The result is that animations almost work.  The problem is
that Okular doesn't seem to get notified about the visibility changes, so a
new frame is only shown if the zoom level is changed to force a redraw.

Any advice about how to deal with this redraw problem?

The patch implementing these changes (WIP) is attached.  See also example
LaTeX file.  Use pdflatex to generate a pdf.  To debug the generated
JavaScript, I found it is helpful to copy animate.sty to the local
directory, and then edit it to add console.println statements into the
generated code.

Note: The Javascript code, which should be triggered by page load, doesn't
seem to get run until you switch to presentation mode.  To actually see the
animation, you have to switch back to regular view mode, and then quickly
toggle the zoom level.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.kde.org/pipermail/okular-devel/attachments/20150715/28caac80/attachment-0001.html>
-------------- next part --------------
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5df2289..894e012 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -124,6 +124,7 @@ install( FILES
            interfaces/printinterface.h
            interfaces/saveinterface.h
            interfaces/viewerinterface.h
+           interfaces/ocginterface.h
          DESTINATION ${INCLUDE_INSTALL_DIR}/okular/interfaces COMPONENT Devel)
 
 kde4_add_ui_files(okularcore_SRCS
diff --git a/core/script/executor_kjs.cpp b/core/script/executor_kjs.cpp
index 071da7a..7a2151b 100644
--- a/core/script/executor_kjs.cpp
+++ b/core/script/executor_kjs.cpp
@@ -41,6 +41,7 @@ class Okular::ExecutorKJSPrivate
         }
         ~ExecutorKJSPrivate()
         {
+            // This should really be a document-local cache, rather than a global cache.
             JSField::clearCachedFields();
 
             delete m_interpreter;
@@ -51,29 +52,37 @@ class Okular::ExecutorKJSPrivate
         DocumentPrivate *m_doc;
         KJSInterpreter *m_interpreter;
         KJSGlobalObject m_docObject;
+        QSharedPointer<JSDocumentPrivate> m_jsDoc;
+        QSharedPointer<JSAppPrivate> m_jsApp;
 };
 
 void ExecutorKJSPrivate::initTypes()
 {
-    m_docObject = JSDocument::wrapDocument( m_doc );
+    m_jsDoc = JSDocument::getPrivateInstance( m_doc );
+    m_docObject = JSDocument::wrapDocument( m_jsDoc.data() );
     m_interpreter = new KJSInterpreter( m_docObject );
 
     KJSContext *ctx = m_interpreter->globalContext();
 
-    JSApp::initType( ctx );
+    JSDocument::initType( ctx );
     JSFullscreen::initType( ctx );
     JSConsole::initType( ctx );
     JSData::initType( ctx );
-    JSDocument::initType( ctx );
     JSField::initType( ctx );
     JSSpell::initType( ctx );
     JSUtil::initType( ctx );
 
-    m_docObject.setProperty( ctx, "app", JSApp::object( ctx, m_doc ) );
+    m_jsApp = JSApp::getPrivateInstance( m_doc, m_interpreter );
+    m_docObject.setProperty( ctx, "app", JSApp::object( m_jsApp.data() ) );
     m_docObject.setProperty( ctx, "console", JSConsole::object( ctx ) );
     m_docObject.setProperty( ctx, "Doc", m_docObject );
     m_docObject.setProperty( ctx, "spell", JSSpell::object( ctx ) );
     m_docObject.setProperty( ctx, "util", JSUtil::object( ctx ) );
+
+    KJSObject display_obj;
+    display_obj.setProperty( ctx, "hidden", false );
+    display_obj.setProperty( ctx, "visible", true );
+    m_docObject.setProperty( ctx, "display", display_obj );
 }
 
 ExecutorKJS::ExecutorKJS( DocumentPrivate *doc )
diff --git a/core/script/kjs_app.cpp b/core/script/kjs_app.cpp
index ea79393..1f46433 100644
--- a/core/script/kjs_app.cpp
+++ b/core/script/kjs_app.cpp
@@ -10,6 +10,9 @@
 
 #include "kjs_app_p.h"
 
+#include <QtCore/QHash>
+
+#include <kjs/kjsinterpreter.h>
 #include <kjs/kjsarguments.h>
 #include <kjs/kjsobject.h>
 #include <kjs/kjsprototype.h>
@@ -20,10 +23,77 @@
 #include <klocale.h>
 
 #include "../document_p.h"
+#include "../debug_p.h"
 #include "kjs_fullscreen_p.h"
 
 using namespace Okular;
 
+JSAppPrivate::JSAppPrivate( DocumentPrivate *doc, KJSInterpreter *interpreter )
+    : m_doc( doc ), m_interpreter( interpreter ) {}
+
+JSAppPrivate::~JSAppPrivate() {}
+
+void JSAppPrivate::timerEvent( QTimerEvent *event ) {
+    KJSContext *ctx = m_interpreter->globalContext();
+    JSTimers::iterator it = timers.find( event->timerId() );
+    if ( it == timers.end() ) {
+        // Unexpected timer.
+        killTimer( event->timerId() );
+        return;
+    }
+    JSTimer &t = it.value();
+    KJSObject callback = t.callback;
+    if ( !t.repeat ) {
+        // Remove the timer before running the callback.
+        // The callback might call cancelTimer and thereby invalidate our iterator.
+        killTimer( event->timerId() );
+        timers.erase( it );
+    }
+    if ( callback.isString() ) {
+        KJSObject docObject = m_interpreter->globalObject();
+        QString callbackStr = callback.toString( ctx );
+        KJSResult result = m_interpreter->evaluate( "okular.js", 1, callbackStr,
+                                           &docObject );
+
+        if ( result.isException() || ctx->hasException() ) {
+            kDebug( OkularDebug ) << "[Timer] While trying to run:"
+                                  << callbackStr;
+            kDebug( OkularDebug ) << "[Timer] JS exception"
+                                  << result.errorMessage();
+            //        kDebug(OkularDebug) << "Exception Info:" <<
+            // result.value().toString(ctx);
+        } else {
+            kDebug( OkularDebug ) << "[Timer] result:"
+                                  << result.value().toString( ctx );
+        }
+
+        // Ignore result.
+    } else {
+        // Callback should be a function in this case.
+        // FIXME: Should invoke function directly.
+    }
+
+    // Our iterator may be invalid now.
+}
+
+// Returns a timer identifier.
+int JSAppPrivate::addTimer( KJSObject callback, int interval, bool repeat ) {
+    int id = startTimer( interval );
+    JSTimer &t = timers[id];
+    t.repeat = repeat;
+    t.callback = callback;
+    return id;
+}
+
+// Cancels the timer with the specified identifier.
+void JSAppPrivate::cancelTimer( int id ) {
+    JSTimers::iterator it = timers.find( id );
+    if ( it != timers.end() ) {
+        timers.erase( it );
+        killTimer( id );
+    }
+}
+
 static KJSPrototype *g_appProto;
 
 // the acrobat version we fake
@@ -181,7 +251,7 @@ static KJSObject appGetNthPlugInName( KJSContext *context, void *,
 static KJSObject appGoBack( KJSContext *, void *object,
                             const KJSArguments & )
 {
-    const DocumentPrivate *doc = reinterpret_cast< DocumentPrivate * >( object );
+    const DocumentPrivate *doc = reinterpret_cast< JSAppPrivate * >( object )->m_doc;
     if ( doc->m_parent->historyAtBegin() )
         return KJSUndefined();
 
@@ -192,7 +262,7 @@ static KJSObject appGoBack( KJSContext *, void *object,
 static KJSObject appGoForward( KJSContext *, void *object,
                                const KJSArguments & )
 {
-    const DocumentPrivate *doc = reinterpret_cast< DocumentPrivate * >( object );
+    const DocumentPrivate *doc = reinterpret_cast< JSAppPrivate * >( object )->m_doc;
     if ( doc->m_parent->historyAtEnd() )
         return KJSUndefined();
 
@@ -200,6 +270,53 @@ static KJSObject appGoForward( KJSContext *, void *object,
     return KJSUndefined();
 }
 
+static KJSObject appAlert( KJSContext *ctx, void *,
+                           const KJSArguments &arguments ) {
+    // Note: It is not clear how this dialog box should be handled.  For now, we just log the message and return 1 immediately.
+
+    // Returns: nButton, the type of the button that was pressed by the user:
+    // 1 - OK
+    // 2 - Cancel
+    // 3 - No
+    // 4 - Yes
+    QString cMessage = "undefined";
+    if (arguments.at(0).isString())
+        cMessage = arguments.at(0).toString(ctx);
+    else if (arguments.at(0).isObject())
+        cMessage = arguments.at(0).property(ctx, "cMsg").toString(ctx);
+    //QString cMessage = arguments.at( 0 ).toString( ctx );
+    kDebug(OkularDebug) << "JAVASCRIPT ALERT:" << cMessage;
+
+    return KJSNumber(1);
+}
+
+static KJSObject appSetTimer( KJSContext *ctx, void *object,
+                              const KJSArguments &arguments,
+                              bool repeat) {
+    JSAppPrivate *app = reinterpret_cast< JSAppPrivate * >( object );
+    int nMilliseconds = static_cast<int>(arguments.at(1).toNumber(ctx));
+    if (nMilliseconds < 1)
+        nMilliseconds = 1;
+    return KJSNumber(app->addTimer( arguments.at(0), nMilliseconds, repeat ));
+}
+
+static KJSObject appSetInterval( KJSContext *ctx, void *object,
+                                 const KJSArguments &arguments ) {
+    return appSetTimer( ctx, object, arguments, true );
+}
+
+static KJSObject appSetTimeOut( KJSContext *ctx, void *object,
+                                const KJSArguments &arguments ) {
+    return appSetTimer( ctx, object, arguments, false );
+}
+
+static KJSObject appClearTimer( KJSContext *ctx, void *object,
+                                const KJSArguments &arguments ) {
+    JSAppPrivate *app = reinterpret_cast< JSAppPrivate * >( object );
+    app->cancelTimer( static_cast<int>(arguments.at(0).toNumber(ctx)) );
+    return KJSUndefined();
+}
+
 void JSApp::initType( KJSContext *ctx )
 {
     static bool initialized = false;
@@ -224,9 +341,20 @@ void JSApp::initType( KJSContext *ctx )
     g_appProto->defineFunction( ctx, "getNthPlugInName", appGetNthPlugInName );
     g_appProto->defineFunction( ctx, "goBack", appGoBack );
     g_appProto->defineFunction( ctx, "goForward", appGoForward );
+    g_appProto->defineFunction( ctx, "alert", appAlert );
+    g_appProto->defineFunction( ctx, "setInterval", appSetInterval );
+    g_appProto->defineFunction( ctx, "setTimeOut", appSetTimeOut );
+    g_appProto->defineFunction( ctx, "clearInterval", appClearTimer );
+    g_appProto->defineFunction( ctx, "clearTimeOut", appClearTimer );
+}
+
+QSharedPointer<JSAppPrivate> JSApp::getPrivateInstance( DocumentPrivate *doc, KJSInterpreter *interpreter ) {
+    return QSharedPointer<JSAppPrivate>( new JSAppPrivate( doc, interpreter ) );
 }
 
-KJSObject JSApp::object( KJSContext *ctx, DocumentPrivate *doc )
+KJSObject JSApp::object( JSAppPrivate *doc )
 {
+    KJSContext *ctx = doc->m_interpreter->globalContext();
+    initType(ctx);
     return g_appProto->constructObject( ctx, doc );
 }
diff --git a/core/script/kjs_app_p.h b/core/script/kjs_app_p.h
index c0649df..ef2f5a4 100644
--- a/core/script/kjs_app_p.h
+++ b/core/script/kjs_app_p.h
@@ -11,18 +11,53 @@
 #ifndef OKULAR_SCRIPT_KJS_APP_P_H
 #define OKULAR_SCRIPT_KJS_APP_P_H
 
+#include <QtCore/QHash>
+#include <QtCore/QSharedPointer>
+
+#include <kjs/kjsobject.h>
+
 class KJSContext;
+class KJSInterpreter;
 class KJSObject;
 
 namespace Okular {
 
 class DocumentPrivate;
 
+// We extend QObject in order to make use of QObject's timer functions.
+class JSAppPrivate : public QObject {
+    Q_OBJECT
+
+    public:
+
+        JSAppPrivate(DocumentPrivate *doc, KJSInterpreter *interpreter);
+        virtual ~JSAppPrivate();
+
+        struct JSTimer {
+            KJSObject callback;
+            bool repeat;
+        };
+
+        void timerEvent(QTimerEvent *event);
+
+        // Returns a timer identifier.
+        int addTimer(KJSObject callback, int interval, bool repeat);
+
+        // Cancels the timer with the specified identifier.
+        void cancelTimer(int id);
+
+        typedef QHash<int,JSTimer> JSTimers;
+        JSTimers timers;
+        DocumentPrivate *m_doc;
+        KJSInterpreter *m_interpreter;
+};
+
 class JSApp
 {
     public:
         static void initType( KJSContext *ctx );
-        static KJSObject object( KJSContext *ctx, DocumentPrivate *doc );
+        static QSharedPointer<JSAppPrivate> getPrivateInstance( DocumentPrivate *doc, KJSInterpreter *interpreter );
+        static KJSObject object( JSAppPrivate *d );
 };
 
 }
diff --git a/core/script/kjs_console.cpp b/core/script/kjs_console.cpp
index 8944d73..84b5dfd 100644
--- a/core/script/kjs_console.cpp
+++ b/core/script/kjs_console.cpp
@@ -141,7 +141,7 @@ void JSConsole::initType( KJSContext *ctx )
     g_consoleProto->defineFunction( ctx, "clear", consoleClear );
     g_consoleProto->defineFunction( ctx, "hide", consoleHide );
     g_consoleProto->defineFunction( ctx, "println", consolePrintln );
-    g_consoleProto->defineFunction( ctx, "hide", consoleShow );
+    g_consoleProto->defineFunction( ctx, "show", consoleShow );
 }
 
 KJSObject JSConsole::object( KJSContext *ctx )
diff --git a/core/script/kjs_document.cpp b/core/script/kjs_document.cpp
index b8d7e1c..fb7a0af 100644
--- a/core/script/kjs_document.cpp
+++ b/core/script/kjs_document.cpp
@@ -10,15 +10,21 @@
 
 #include "kjs_document_p.h"
 
+#include <QtCore/QtAlgorithms>
+
 #include <qwidget.h>
 
 #include <kjs/kjsobject.h>
 #include <kjs/kjsprototype.h>
 #include <kjs/kjsarguments.h>
 
+#include <interfaces/saveinterface.h>
+#include <interfaces/ocginterface.h>
+
 #include <kdebug.h>
 #include <assert.h>
 
+#include "../debug_p.h"
 #include "../document_p.h"
 #include "../page.h"
 #include "../form.h"
@@ -29,10 +35,54 @@ using namespace Okular;
 
 static KJSPrototype *g_docProto;
 
+class Okular::JSDocumentPrivate {
+    public:
+        JSDocumentPrivate( DocumentPrivate *doc )
+            : m_doc( doc ), m_initialized_ocgs(false) {}
+
+        DocumentPrivate *m_doc;
+        bool m_initialized_ocgs;
+
+        struct OCG {
+            QAbstractItemModel *model;
+            QModelIndex index;
+            bool initialVisibility;
+            OCG() {}
+            OCG( QAbstractItemModel *model, QModelIndex index )
+                : model( model ), index( index ) {
+                initialVisibility = isVisible();
+            }
+
+            QString getName() const;
+
+            bool isVisible();
+            void setVisible(bool value);
+        };
+
+        QVector< OCG > m_ocgs;
+};
+
+QString JSDocumentPrivate::OCG::getName() const {
+    return model->data( index, Qt::DisplayRole ).toString();
+}
+
+bool JSDocumentPrivate::OCG::isVisible() {
+    return static_cast<Qt::CheckState>( model->data( index, Qt::CheckStateRole )
+                                            .toInt() ) == Qt::Checked;
+}
+
+void JSDocumentPrivate::OCG::setVisible(bool value) {
+    model->setData( index, QVariant( value ), Qt::CheckStateRole );
+}
+
+bool operator<(const JSDocumentPrivate::OCG &a, const JSDocumentPrivate::OCG &b) {
+    return a.getName() < b.getName();
+}
+
 // Document.numPages
 static KJSObject docGetNumPages( KJSContext *, void *object )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     return KJSNumber( doc->m_pagesVector.count() );
 }
@@ -40,7 +90,7 @@ static KJSObject docGetNumPages( KJSContext *, void *object )
 // Document.pageNum (getter)
 static KJSObject docGetPageNum( KJSContext *, void *object )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     return KJSNumber( doc->m_parent->currentPage() );
 }
@@ -49,7 +99,7 @@ static KJSObject docGetPageNum( KJSContext *, void *object )
 static void docSetPageNum( KJSContext* ctx, void* object,
                            KJSObject value )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     int page = value.toInt32( ctx );
 
@@ -62,7 +112,7 @@ static void docSetPageNum( KJSContext* ctx, void* object,
 // Document.documentFileName
 static KJSObject docGetDocumentFileName( KJSContext *, void *object )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     return KJSString( doc->m_url.fileName() );
 }
@@ -70,7 +120,7 @@ static KJSObject docGetDocumentFileName( KJSContext *, void *object )
 // Document.filesize
 static KJSObject docGetFilesize( KJSContext *, void *object )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     return KJSNumber( doc->m_docSize );
 }
@@ -78,7 +128,7 @@ static KJSObject docGetFilesize( KJSContext *, void *object )
 // Document.path
 static KJSObject docGetPath( KJSContext *, void *object )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     return KJSString( doc->m_url.pathOrUrl() );
 }
@@ -86,7 +136,7 @@ static KJSObject docGetPath( KJSContext *, void *object )
 // Document.URL
 static KJSObject docGetURL( KJSContext *, void *object )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     return KJSString( doc->m_url.prettyUrl() );
 }
@@ -100,7 +150,7 @@ static KJSObject docGetPermStatusReady( KJSContext *, void * )
 // Document.dataObjects
 static KJSObject docGetDataObjects( KJSContext *ctx, void *object )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     const QList< EmbeddedFile * > *files = doc->m_generator->embeddedFiles();
 
@@ -120,7 +170,7 @@ static KJSObject docGetDataObjects( KJSContext *ctx, void *object )
 // Document.external
 static KJSObject docGetExternal( KJSContext *, void *object )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
     QWidget *widget = doc->m_widget;
 
     const bool isShell = ( widget
@@ -132,7 +182,7 @@ static KJSObject docGetExternal( KJSContext *, void *object )
 
 static KJSObject docGetInfo( KJSContext *ctx, void *object )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     KJSObject obj;
     QSet<DocumentInfo::Key> keys;
@@ -166,7 +216,7 @@ do { \
 #define DOCINFO_GET_METHOD( key, name ) \
 static KJSObject docGet ## name( KJSContext *, void *object ) \
 { \
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object ); \
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc; \
     const DocumentInfo docinfo = doc->m_parent->documentInfo(QSet<DocumentInfo::Key>() << key ); \
     return KJSString( docinfo.get( key ) ); \
 }
@@ -184,7 +234,7 @@ DOCINFO_GET_METHOD( DocumentInfo::Subject, Subject )
 static KJSObject docGetField( KJSContext *context, void *object,
                               const KJSArguments &arguments )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     QString cName = arguments.at( 0 ).toString( context );
 
@@ -208,7 +258,7 @@ static KJSObject docGetField( KJSContext *context, void *object,
 static KJSObject docGetPageLabel( KJSContext *ctx,void *object,
                                   const KJSArguments &arguments )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
     int nPage = arguments.at( 0 ).toInt32( ctx );
     Page *p = doc->m_pagesVector.value( nPage );
     return KJSString( p ? p->label() : QString() );
@@ -218,7 +268,7 @@ static KJSObject docGetPageLabel( KJSContext *ctx,void *object,
 static KJSObject docGetPageRotation( KJSContext *ctx, void *object,
                                      const KJSArguments &arguments )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
     int nPage = arguments.at( 0 ).toInt32( ctx );
     Page *p = doc->m_pagesVector.value( nPage );
     return KJSNumber( p ? p->orientation() * 90 : 0 );
@@ -228,7 +278,7 @@ static KJSObject docGetPageRotation( KJSContext *ctx, void *object,
 static KJSObject docGotoNamedDest( KJSContext *ctx, void *object,
                                    const KJSArguments &arguments )
 {
-    DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object );
+    DocumentPrivate *doc = reinterpret_cast< JSDocumentPrivate* >( object )->m_doc;
 
     QString dest = arguments.at( 0 ).toString( ctx );
 
@@ -248,10 +298,84 @@ static KJSObject docSyncAnnotScan( KJSContext *, void *,
     return KJSUndefined();
 }
 
-void JSDocument::initType( KJSContext *ctx )
+static void fillItems(QAbstractItemModel *model, QVector<JSDocumentPrivate::OCG> &output, QModelIndex parent = QModelIndex()) {
+    int rowCount = model->rowCount(parent);
+    int colCount = model->columnCount(parent);
+    for (int row = 0; row < rowCount; ++row) {
+        for (int col = 0; col < colCount; ++col) {
+            QModelIndex index = model->index(row, col, parent);
+            output.push_back( JSDocumentPrivate::OCG( model, index ) );
+            fillItems( model, output, index );
+        }
+    }
+}
+
+static KJSPrototype *g_ocgProto = 0;
+
+static KJSObject ocgGetName( KJSContext *, void *object )
+{
+    JSDocumentPrivate::OCG *ocg = reinterpret_cast< JSDocumentPrivate::OCG * >( object );
+    return KJSString(ocg->getName());
+}
+
+static KJSObject ocgGetState( KJSContext *, void *object )
+{
+    JSDocumentPrivate::OCG *ocg = reinterpret_cast< JSDocumentPrivate::OCG * >( object );
+    return KJSBoolean(ocg->isVisible());
+}
+
+static void ocgSetState( KJSContext *context, void *object, KJSObject value )
 {
-    assert( g_docProto );
+    JSDocumentPrivate::OCG *ocg = reinterpret_cast< JSDocumentPrivate::OCG * >( object );
+    ocg->setVisible(value.toBoolean(context));
+}
+
+class JSOCG {
+    public:
+        static void initType( KJSContext *ctx ) {
+            if ( g_ocgProto )
+                return;
+
+            g_ocgProto = new KJSPrototype();
+
+            g_ocgProto->defineProperty( ctx, "name", ocgGetName );
+            g_ocgProto->defineProperty( ctx, "state", ocgGetState,
+                                          ocgSetState );
+        }
+        static KJSObject wrapField( KJSContext *ctx, JSDocumentPrivate::OCG *ocg ) {
+            initType( ctx );
+            KJSObject x = g_ocgProto->constructObject( ctx, ocg );
+            x.setProperty(ctx, "initState", ocg->initialVisibility);
+            return x;
+        }
+};
 
+// Document.getOCGs()
+static KJSObject docGetOCGs( KJSContext *context, void *object,
+                             const KJSArguments &/*arguments*/ )
+{
+    JSDocumentPrivate *d = reinterpret_cast< JSDocumentPrivate* >( object );
+
+    if (!d->m_initialized_ocgs) {
+        // Need to populate d->m_ocgs
+        d->m_initialized_ocgs = true;
+        if (Okular::OCGInterface *iface = qobject_cast<Okular::OCGInterface *>(d->m_doc->m_generator)) {
+            QAbstractItemModel *model = iface->optionalContentModel();
+            fillItems(model, d->m_ocgs);
+            // Sort by name
+            qSort( d->m_ocgs.begin(), d->m_ocgs.end() );
+        }
+    }
+
+    KJSArray result(context, d->m_ocgs.size());
+    for (int i = 0; i < d->m_ocgs.size(); ++i) {
+        result.setProperty(context, QString::number(i), JSOCG::wrapField(context, &d->m_ocgs[i]));
+    }
+    return result;
+}
+
+void JSDocument::initType( KJSContext *ctx )
+{
     static bool initialized = false;
     if ( initialized )
         return;
@@ -281,11 +405,22 @@ void JSDocument::initType( KJSContext *ctx )
     g_docProto->defineFunction( ctx, "getPageRotation", docGetPageRotation );
     g_docProto->defineFunction( ctx, "gotoNamedDest", docGotoNamedDest );
     g_docProto->defineFunction( ctx, "syncAnnotScan", docSyncAnnotScan );
+    g_docProto->defineFunction( ctx, "getOCGs", docGetOCGs );
+}
+
+QSharedPointer<JSDocumentPrivate> JSDocument::getPrivateInstance(DocumentPrivate *doc )
+{
+    return QSharedPointer<JSDocumentPrivate>( new JSDocumentPrivate( doc ) );
 }
 
-KJSGlobalObject JSDocument::wrapDocument( DocumentPrivate *doc )
+KJSGlobalObject JSDocument::wrapDocument( JSDocumentPrivate *d )
 {
+    // This is called before we have created the KJSInterpreter instance, since
+    // the interpreter will be constructed from our return value.
+    //
+    // Therefore, we just create an empty KJSPrototype, and will initialize it
+    // later when initType is called.
     if (!g_docProto)
         g_docProto = new KJSPrototype();
-    return g_docProto->constructGlobalObject( doc );
+    return g_docProto->constructGlobalObject( d );
 }
diff --git a/core/script/kjs_document_p.h b/core/script/kjs_document_p.h
index edb9d4b..d32c6ce 100644
--- a/core/script/kjs_document_p.h
+++ b/core/script/kjs_document_p.h
@@ -11,6 +11,8 @@
 #ifndef OKULAR_SCRIPT_KJS_DOCUMENT_P_H
 #define OKULAR_SCRIPT_KJS_DOCUMENT_P_H
 
+#include <QtCore/QSharedPointer>
+
 class KJSContext;
 class KJSGlobalObject;
 
@@ -18,13 +20,15 @@ namespace Okular {
 
 class DocumentPrivate;
 
+class JSDocumentPrivate;
+
 class JSDocument
 {
     public:
         static void initType( KJSContext *ctx );
-        static KJSGlobalObject wrapDocument( DocumentPrivate *doc );
+        static QSharedPointer<JSDocumentPrivate> getPrivateInstance( DocumentPrivate *doc );
+        static KJSGlobalObject wrapDocument( JSDocumentPrivate *d );
 };
-
 }
 
 #endif
diff --git a/generators/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp
index 07abf75..087e9b0 100644
--- a/generators/poppler/generator_pdf.cpp
+++ b/generators/poppler/generator_pdf.cpp
@@ -1638,6 +1638,12 @@ Okular::AnnotationProxy* PDFGenerator::annotationProxy() const
     return annotProxy;
 }
 
+QAbstractItemModel *PDFGenerator::optionalContentModel() {
+    if (!pdfdoc)
+        return 0;
+    return pdfdoc->optionalContentModel();
+}
+
 #include "generator_pdf.moc"
 
 /* kate: replace-tabs on; indent-width 4; */
diff --git a/generators/poppler/generator_pdf.h b/generators/poppler/generator_pdf.h
index 89afe18..568069a 100644
--- a/generators/poppler/generator_pdf.h
+++ b/generators/poppler/generator_pdf.h
@@ -23,6 +23,7 @@
 #include <interfaces/configinterface.h>
 #include <interfaces/printinterface.h>
 #include <interfaces/saveinterface.h>
+#include <interfaces/ocginterface.h>
 
 namespace Okular {
 class ObjectRect;
@@ -44,12 +45,13 @@ class PopplerAnnotationProxy;
  * contents from out OutputDevs when rendering finishes.
  *
  */
-class PDFGenerator : public Okular::Generator, public Okular::ConfigInterface, public Okular::PrintInterface, public Okular::SaveInterface
+class PDFGenerator : public Okular::Generator, public Okular::ConfigInterface, public Okular::PrintInterface, public Okular::SaveInterface, public Okular::OCGInterface
 {
     Q_OBJECT
     Q_INTERFACES( Okular::ConfigInterface )
     Q_INTERFACES( Okular::PrintInterface )
     Q_INTERFACES( Okular::SaveInterface )
+    Q_INTERFACES( Okular::OCGInterface )
 
     public:
         PDFGenerator( QObject *parent, const QVariantList &args );
@@ -96,6 +98,9 @@ class PDFGenerator : public Okular::Generator, public Okular::ConfigInterface, p
         bool save( const QString &fileName, SaveOptions options, QString *errorText );
         Okular::AnnotationProxy* annotationProxy() const;
 
+        // [INHERITED] OCG interface
+        QAbstractItemModel *optionalContentModel();
+
     protected:
         bool doCloseDocument();
         Okular::TextPage* textPage( Okular::Page *page );
diff --git a/interfaces/ocginterface.h b/interfaces/ocginterface.h
new file mode 100644
index 0000000..843babf
--- /dev/null
+++ b/interfaces/ocginterface.h
@@ -0,0 +1,35 @@
+/***************************************************************************
+ *   Copyright (C) 2015 by Jeremy Maitin-Shepard <jeremy at jeremyms.com>     *
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) any later version.                                   *
+ ***************************************************************************/
+
+#ifndef _OKULAR_OCGINTERFACE_H_
+#define _OKULAR_OCGINTERFACE_H_
+
+#include "../core/okular_export.h"
+
+#include <QtCore/QAbstractListModel>
+
+namespace Okular {
+
+/**
+ * @short Abstract interface for manipulating PDF-style Optional Content Groups.
+ *
+ * This interface is inherently PDF-specific, but allows us to maintain a
+ * separation between the core code and the generator code.
+ **/
+class OKULAR_EXPORT OCGInterface {
+    public:
+        virtual ~OCGInterface() {}
+        virtual QAbstractItemModel *optionalContentModel() = 0;
+};
+
+}
+
+Q_DECLARE_INTERFACE( Okular::OCGInterface, "org.kde.okular.OCGInterface/0.1" )
+
+#endif
-------------- next part --------------
A non-text attachment was scrubbed...
Name: test.tex
Type: application/x-tex
Size: 238 bytes
Desc: not available
URL: <http://mail.kde.org/pipermail/okular-devel/attachments/20150715/28caac80/attachment-0001.tex>


More information about the Okular-devel mailing list