[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-devel
Subject:    QXEmbed and its many variants
From:       Leon Bottou <leonb () nec-labs ! com>
Date:       2003-02-21 17:11:24
[Download RAW message or body]

EXECUTIVE SUMMARY

There are two embedding cases: (a) embedding XEMBED aware applications,
and (b) embedding applications that know nothing about XEMBED.
With time these two cases have diverged.  Implementations of the first case 
no longer handle the second case properly. This is the root cause of the 
proliferation of QXEmbed derivatives.

The attached patch for QXEmbed implements a choice of protocols.
One can use XEMBED for embedding XEMBED aware applications
or XPLAIN for non XEMBED aware applications. 

Together with minor patches, this change fixes bug 54375.  
Most importantly it provides a way to rationalize the many variants of QXEmbed.
It does not solve all issues but clearly defines a framework for addressing them.

MOTIVATION

Some (all) netscape plugins no longer get keyboard events under kde-3.1.  
See bug <http://bugs.kde.org/show_bug.cgi?id=54375>

After some research I found that the source of the problem
is the replacement of the old knspluginembed class
by the  standard embedding class qxembed.

The problem is that qxembed implements the XEMBED protocol
despite the fact that the embedded application, nspluginviewer,
is completely unaware of this protocol.  This stems from the fact
that the netscape plugin api dictates that nspluginviewer must 
essentially be a Xt/Motif application.

The same mismatch occurs with java embedding.  Also I found
several mailing list messages from people experiencing trouble
embedding emacs, vim, or other non XEMBED aware applications.

ANALYSIS

The very first embedding codes were only relying on the X specification
and were pretty much able to embed anything.  There were rough edges.
Focus, for instance, was handled by the default X focus behavior which
no longer matches the expectations set by modern toolkits.

The QXEmbed code, and later the XEMBED specification, were
designed to provide these missing features.  Several changes
are introduced to do so:
- The embeding application uses an invisible focus proxy window
  to basically disable the default X focus behavior.
- The embedded application is required to cooperate with
  the embedding widget to obtain the keyboard focus.
  Otherwise it never gets it.

We must now recognize the presence of two different cases.
Either we embed an XEMBED aware application, or we embed
an application with no XEMBED support.  The embedding class
must behave differently in these two cases.

RESULTS

The attached patches define the notion of protocol in the qxembed class.
Two protocols are supported:
- The default protocol is XEMBED and behaves as usual.
- The second protocol is named XPLAIN and is designed to 
  support embedding of application that are not aware of embedding.
  Of course this comes at the price of reduced functionalities.
  Tab focus, for instance, is not supported.

The only thing you have to do is to call    
  embedwidget->setProtocol(QXEmbed::XPLAIN);
before calling embed().

I chose not to implement a second class because I believe that 
the XEMBED protocol version 2 provides a way to test whether
an application supports the XEMBED protocol.  We will then be
able to determine automatically whether we should use XEMBED
ot XPLAIN.  There will be no need then to use the setProtocol().

I tested this new class with the nspluginviewer and the djvu plugin.
To make it work I had to perform minor changes in nsplugins (attached)
and slightly more changes in the djvu plugin nsdejavu.so (discussed later).
Everything works nicely with these changes in place.

I believe that these changes also simplify the embedding 
code in KJavaAppletWidget because the XPLAIN protocol
borrows a lot from this work.  

Most importantly I believe that these changes provide a clean
way to eliminate the many semi-buggy variants of QXEmbed
and to consolidate all the bug fixes in a single code base.


UNRESOLVED ISSUES

A still unresolved issue bites us when the embedded application
(i.e. nspluginviewer) itself embeds a third application 
(i.e. djview, acroread, mplayer) using an ad-hoc protocol.

The XEMBED specification demands that toolkits call 
XSetInputFocus() to redirect the X11 focus to an invisible
focus proxy window.  Doing so completely disables
the default focus mechanisms of X11.  These default
mechanisms were the cause of much trouble for XEMBED.
But these defaults mechanisms were relied upon by
the ad-hoc embedding protocols.  

Currently the XPLAIN protocol makes sure that the toplevel window 
of the embedded application receives the KeyPress and KeyRelease
events it needs.  The application then must explicitly forward them to 
the relevant widgets.  Some applications however relied on the
default X11 focus mechanisms to do that.  These must be modified.

For instance you can see explicit fowarding code in my changes
to the nspluginviewer (see attachement) and also in my changes to the djvu plugin. 
(see <http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/djvu/djvulibre-3.5/gui/nsdejavu/nsdejavu.c.diff?r1=1.17&r2=1.18>)


There might be a way to solve this problem by temporarily setting the
X11 focus to the embedding widget when this widget has the logical input focus.
But to do so we need to define a method for negotiating this operation with 
the toolkit of the embedding application.  More precisely I know how 
to take the X11 input focus but I do not know how to put it back.

Should such a negotiation method be defined, we would simply update
the implementation of the XPLAIN protocol in QXEmbed and
forget about the above problems. 

CONCLUSION

This QXEmbed change solve a few pending bugs.
Most importantly it provides a way to rationalize the many variants of QXEmbed.
It does not solve all issues but clearly defines a framework for addressing them.


- Leon Bottou



P.S. -- [historical comments]

My first experience with embedding dates from the first djvu plugin.
Here is an interesting email exchange.  I wonder sometimes...

-----------------------
Date: Tue, 06 Oct 1998 16:01:17 -0400
From: Leon Bottou <leonb@research.att.com>
To: info@troll.no
Subject: [SUGGESTION] About QXtWidget in a mostly Qt application (qt-1.40)

Your doc (QXtWidget) says that
"Note that the parent must be a QXtWidget (possibly NULL). "
"This is necessary since all Xt widgets must have Xt       "
"widgets as ancestors up to the top-level widget.          "
"WWA: This restriction may be avoidable by reimplementing  "
"the low-level Qt window creation/destruction functions.   "

This is not exactly right. There is a (strange) way
to create a Xt widget whose ancestor (in the window tree)
is not a Xt widget but a regular X window.  
The trick involves creating a toplevel widget and reparenting 
its window before it ever gets mapped.

Here is the procedure ....

n=0;
XtSetArg(args[n], XmNwidth, width); n++;
XtSetArg(args[n], XmNheight, height); n++;
XtSetArg(args[n], XmNoverrideRedirect, True); n++;  // avoid WM interaction
XtSetArg(args[n], XmNmappedWhenManaged, False); n++;
XtSetArg(args[n], XmNvisual, visual); n++;
XtSetArg(args[n], XmNcolormap, colormap); n++;
XtSetArg(args[n], XmNdepth, depth); n++;
shell=XtAppCreateShell("coolwidget", "coolwidget", 
			topLevelShellWidgetClass,
			display, args, n);
XtRealizeWidget(shell);
XSync(displ, False);	// I want all windows to be created now
XReparentWindow(displ, XtWindow(shell), parentwindow, 0, 0);
XtSetMappedWhenManaged(shell, True);
XtMapWidget(shell);

You can then build a complete Xt widget hierarchy
[assumming that your event loop properly dispatches events]
You must of course make sure to unmap the widget
and reparent it back to the rootwindow 
before destroying these windows.
Hope you can use this ...
Sincerely,

-----------------------
To: Leon Bottou <leonb@research.att.com>
Date: Thu, 08 Oct 1998 19:11:01 +0200
From: Warwick Allison <warwick@troll.no>
Subject: Re: [SUGGESTION] About QXtWidget in a mostly Qt application (qt-1.40)

Leon Bottou wrote:
> This is not exactly right. There is a (strange) way
Interesting stuff.  I'm looking into using this technique - thanks!
Warwick


["qxembed.diff" (text/x-diff)]

--- kdelibs/kdeui/qxembed.h	2002-08-16 21:58:50.000000000 -0400
+++ qxembed.h	2003-02-21 08:48:13.000000000 -0500
@@ -78,8 +78,41 @@
      */
     ~QXEmbed();
 
+    /**
+     * Embedded applications should call this function to make sure 
+     * they support the XEMBED protocol. It is called automatically
+     * when you use @ref #embedClientIntoWindow() or 
+     * @ref #processClientCmdline(). Clients might have to call it
+     * manually when you use @ref #embed().
+     */
     static void initialize();
 
+    enum Protocol { XEMBED, XPLAIN };
+
+    /** 
+     * Sets the protocol used for embedding windows.
+     * This function must be called before embedding a window.
+     * Protocol XEMBED provides maximal functionality (focus, tabs, etc)
+     * but requires explicit cooperation from the embedded window.  
+     * Protocol XPLAIN provides maximal compatibility with 
+     * embedded applications that do not support the XEMBED protocol.
+     * The default is XEMBED.  
+     *
+     * Future work:
+     * Create a protocol AUTO that selects the best option.  
+     * This will be possible with the XEMBED v2 specification.
+     */
+
+    void setProtocol( Protocol proto );
+
+    /** 
+     * Returns the protocol used for embedding the current window.
+     *
+     * @return the protocol used by QXEmbed.
+     */
+
+    Protocol protocol();
+
     /**
      * Embeds the window with the identifier w into this xembed widget.
      * 
@@ -88,8 +121,9 @@
      * about its target embedder. In that case, it is not necessary to call
      * embed(). Instead, the client will call the static function
      * @ref #embedClientIntoWindow().
-     * 
+     *
      * @param w the identifier of the window to embed
+     * @param proto is the embedding protocol to use
      * @see #embeddedWinId()
      */
     void embed( WId w );
@@ -181,6 +215,7 @@
 private:
     WId window;
     QXEmbedData* d;
+    void checkGrab();
     void sendSyntheticConfigureNotifyEvent();
 };
 
--- kdelibs/kdeui/qxembed.cpp	2002-11-21 16:59:25.000000000 -0500
+++ qxembed.cpp	2003-02-21 08:51:47.000000000 -0500
@@ -97,15 +97,18 @@
 public:
     QXEmbedData(){ 
         autoDelete = TRUE;
+        xplain = FALSE;
+        xgrab = FALSE;
         lastPos = QPoint(0,0);
     }
     ~QXEmbedData(){};
 
     
     bool autoDelete;
+    bool xplain;
+    bool xgrab;
     QWidget* focusProxy;
     QPoint lastPos;
-    
 };
 
 class QXEmbedAppFilter : public QObject
@@ -153,7 +156,8 @@
     XSendEvent(qt_xdisplay(), window, FALSE, NoEventMask, &ev);
 }
 
-static void sendClientMessage(Window window, Atom a, long x){
+static void sendClientMessage(Window window, Atom a, long x)
+{
   XEvent ev;
   memset(&ev, 0, sizeof(ev));
   ev.xclient.type = ClientMessage;
@@ -165,6 +169,17 @@
   XSendEvent(qt_xdisplay(), window, FALSE, NoEventMask, &ev);
 }
 
+static void sendFocusMessage(Window window, int type, int mode, int detail)
+{
+  XEvent ev;
+  memset(&ev, 0, sizeof(ev));
+  ev.xfocus.type = type;
+  ev.xfocus.window = window;
+  ev.xfocus.mode = mode;
+  ev.xfocus.detail = detail;
+  XSendEvent(qt_xdisplay(), window, FALSE, FocusChangeMask, &ev);
+}
+
 
 bool QXEmbedAppFilter::eventFilter( QObject *o, QEvent * e)
 {
@@ -456,6 +471,10 @@
     topLevelWidget()->installEventFilter( this );
     qApp->installEventFilter( this );
 
+    if (isActiveWindow())
+      if ( !((QPublicWidget*) topLevelWidget())->topData()->embedded )
+        XSetInputFocus( qt_xdisplay(), d->focusProxy->winId(), RevertToParent, \
qt_x_time ); +
     setAcceptDrops( TRUE );
 }
 
@@ -468,7 +487,9 @@
     if ( window != 0 ) {
         if ( autoDelete() )
             XUnmapWindow( qt_xdisplay(), window );
-
+        if ( d && d->xgrab)
+          XUngrabButton( qt_xdisplay(), AnyButton, AnyModifier, window );
+        
         XReparentWindow(qt_xdisplay(), window, qt_xrootwin(), 0, 0);
         XSync(qt_xdisplay(), FALSE);
 
@@ -490,6 +511,40 @@
 }
 
 
+/*!
+ Sets the protocol used for embedding windows.
+ This function must be called before embedding a window.
+ Protocol XEMBED provides maximal functionality (focus, tabs, etc)
+ but requires explicit cooperation from the embedded window.  
+ Protocol XPLAIN provides maximal compatibility with 
+ embedded applications that do not support the XEMBED protocol.
+ The default is XEMBED.  
+
+ Future work:
+ Create a protocol AUTO that selects the best option.  
+ This will be possible with the XEMBED v2 specification.
+*/
+void QXEmbed::setProtocol( Protocol proto )
+{
+    if (window == 0) {
+        d->xplain = FALSE;
+        if (proto == XPLAIN)
+            d->xplain = TRUE;
+    }
+}
+
+/*! 
+  Returns the protocol used for embedding the current window.
+*/
+
+QXEmbed::Protocol QXEmbed::protocol()
+{
+  if (d->xplain)
+    return XPLAIN;
+  return XEMBED;
+}
+
+
 /*!\reimp
  */
 void QXEmbed::resizeEvent(QResizeEvent*)
@@ -504,7 +559,6 @@
 {
     if (window != 0)
         XMapRaised(qt_xdisplay(), window);
-
 }
 
 
@@ -518,12 +572,19 @@
         if ( o == topLevelWidget() ) {
             if ( !((QPublicWidget*) topLevelWidget())->topData()->embedded )
                 XSetInputFocus( qt_xdisplay(), d->focusProxy->winId(), \
                RevertToParent, qt_x_time );
-            send_xembed_message( window, XEMBED_WINDOW_ACTIVATE );
+            if (d->xplain)
+                checkGrab();
+            else
+                send_xembed_message( window, XEMBED_WINDOW_ACTIVATE );
         }
         break;
     case QEvent::WindowDeactivate:
-        if ( o == topLevelWidget() )
-            send_xembed_message( window, XEMBED_WINDOW_DEACTIVATE );
+        if ( o == topLevelWidget() ) {
+            if (d->xplain)
+                checkGrab();
+            else
+                send_xembed_message( window, XEMBED_WINDOW_DEACTIVATE );
+        }
         break;
     case QEvent::Move:
         {
@@ -553,7 +614,7 @@
     if (!window)
         return;
     last_key_event.window = window;
-    XSendEvent(qt_xdisplay(), window, FALSE, NoEventMask, (XEvent*)&last_key_event);
+    XSendEvent(qt_xdisplay(), window, FALSE, KeyPressMask, \
(XEvent*)&last_key_event);  
 }
 
@@ -564,7 +625,7 @@
     if (!window)
         return;
     last_key_event.window = window;
-    XSendEvent(qt_xdisplay(), window, FALSE, NoEventMask, (XEvent*)&last_key_event);
+    XSendEvent(qt_xdisplay(), window, FALSE, KeyReleaseMask, \
(XEvent*)&last_key_event);  }
 
 /*!\reimp
@@ -572,10 +633,15 @@
 void QXEmbed::focusInEvent( QFocusEvent * e ){
     if (!window)
         return;
-    int detail = XEMBED_FOCUS_CURRENT;
-    if ( e->reason() == QFocusEvent::Tab )
-        detail = tabForward?XEMBED_FOCUS_FIRST:XEMBED_FOCUS_LAST;
-    send_xembed_message( window, XEMBED_FOCUS_IN, detail);
+    if (d->xplain) {
+        checkGrab();
+        sendFocusMessage(window, XFocusIn, NotifyNormal, NotifyPointer );
+    } else {
+        int detail = XEMBED_FOCUS_CURRENT;
+        if ( e->reason() == QFocusEvent::Tab )
+            detail = tabForward?XEMBED_FOCUS_FIRST:XEMBED_FOCUS_LAST;
+        send_xembed_message( window, XEMBED_FOCUS_IN, detail);
+    }
 }
 
 /*!\reimp
@@ -583,7 +649,12 @@
 void QXEmbed::focusOutEvent( QFocusEvent * ){
     if (!window)
         return;
-    send_xembed_message( window, XEMBED_FOCUS_OUT );
+    if (d->xplain) {
+        checkGrab();
+        sendFocusMessage(window, XFocusOut, NotifyNormal, NotifyPointer );
+    } else {
+        send_xembed_message( window, XEMBED_FOCUS_OUT );
+    }
 }
 
 
@@ -675,10 +746,16 @@
         QApplication::postEvent( parent(), layoutHint );
     }
     windowChanged( window );
-    send_xembed_message( window, XEMBED_EMBEDDED_NOTIFY, 0, (long) winId() );
-    send_xembed_message( window, isActiveWindow() ? XEMBED_WINDOW_ACTIVATE : \
                XEMBED_WINDOW_DEACTIVATE );
-    if ( hasFocus() )
-        send_xembed_message( window, XEMBED_FOCUS_IN );
+    if (d->xplain) {
+        checkGrab();
+        if ( hasFocus() )
+            sendFocusMessage(window, XFocusIn, NotifyNormal, NotifyPointer );
+    } else {
+        send_xembed_message( window, XEMBED_EMBEDDED_NOTIFY, 0, (long) winId() );
+        send_xembed_message( window, isActiveWindow() ? XEMBED_WINDOW_ACTIVATE : \
XEMBED_WINDOW_DEACTIVATE ); +        if ( hasFocus() )
+            send_xembed_message( window, XEMBED_FOCUS_IN );
+    }
 }
 
 
@@ -730,6 +807,19 @@
             embed( window );
         }
         break;
+    case ButtonPress:
+        if (d->xplain) {
+            QFocusEvent::setReason( QFocusEvent::Mouse );
+            setFocus();
+            QFocusEvent::resetReason();
+            XAllowEvents(qt_xdisplay(), ReplayPointer, CurrentTime);
+            return TRUE;
+        }
+        break;
+    case ButtonRelease:
+        if (d->xplain) 
+            XAllowEvents(qt_xdisplay(), SyncPointer, CurrentTime);
+        break;
     case MapRequest:
         if ( window && e->xmaprequest.window == window )
             XMapRaised(qt_xdisplay(), window );
@@ -918,7 +1008,25 @@
     return TRUE;
 }
 
-void QXEmbed::sendSyntheticConfigureNotifyEvent() {
+
+void QXEmbed::checkGrab() 
+{
+    if (d->xplain && isActiveWindow() && !hasFocus()) {
+        if (! d->xgrab)
+            XGrabButton(qt_xdisplay(), AnyButton, AnyModifier, winId(),
+                        FALSE, ButtonPressMask, GrabModeSync, GrabModeAsync,
+                        None, None );
+        d->xgrab = TRUE;
+    } else {
+        if (d->xgrab)
+            XUngrabButton( qt_xdisplay(), AnyButton, AnyModifier, window );
+        d->xgrab = FALSE;
+    }
+}
+
+
+void QXEmbed::sendSyntheticConfigureNotifyEvent() 
+{
     QPoint globalPos = mapToGlobal(QPoint(0,0));
     if (window) {
         // kdDebug(6100) << "*************** sendSyntheticConfigureNotify \
******************" << endl;


["nsplugin.diff" (text/x-diff)]

Index: nspluginloader.cpp
===================================================================
RCS file: /home/kde/kdebase/nsplugins/nspluginloader.cpp,v
retrieving revision 1.30
diff -u -3 -p -r1.30 nspluginloader.cpp
--- nspluginloader.cpp	27 Oct 2002 18:31:00 -0000	1.30
+++ nspluginloader.cpp	21 Feb 2003 14:16:44 -0000
@@ -56,6 +56,7 @@ NSPluginInstance::NSPluginInstance(QWidg
     shown = false;
     _loader = NSPluginLoader::instance();
     setBackgroundMode(QWidget::NoBackground);
+    setProtocol(QXEmbed::XPLAIN);
     embed( NSPluginInstanceIface_stub::winId() );
     displayPlugin();
     shown = true;
Index: viewer/nsplugin.cpp
===================================================================
RCS file: /home/kde/kdebase/nsplugins/viewer/nsplugin.cpp,v
retrieving revision 1.77
diff -u -3 -p -r1.77 nsplugin.cpp
--- viewer/nsplugin.cpp	22 Aug 2002 16:39:59 -0000	1.77
+++ viewer/nsplugin.cpp	21 Feb 2003 14:16:45 -0000
@@ -322,6 +322,20 @@ NPError g_NPN_SetValue(NPP /*instance*/,
 
 /******************************************************************/
 
+void
+NSPluginInstance::forwarder(Widget w, XtPointer cl_data, XEvent * event, Boolean * cont)
+{
+  NSPluginInstance *inst = (NSPluginInstance*)cl_data;
+  *cont = True;
+  if (inst->_area == 0 || event->xkey.window == XtWindow(inst->_area))
+    return;
+  *cont = False;
+  event->xkey.window = XtWindow(inst->_area);
+  event->xkey.subwindow = None;
+  XtDispatchEvent(event);
+}
+
+
 NSPluginInstance::NSPluginInstance(NPP privateData, NPPluginFuncs *pluginFuncs,
                                    KLibrary *handle, int width, int height,
                                    QString src, QString /*mime*/,
@@ -381,6 +395,12 @@ NSPluginInstance::NSPluginInstance(NPP p
    _area = XmCreateDrawingArea( _form, (char*)("drawingArea"), args, nargs);
    XtRealizeWidget(_area);
    XtMapWidget(_area);
+   
+   // Register forwarder
+   XtAddEventHandler(_toplevel, (KeyPressMask|KeyReleaseMask), 
+                     False, forwarder, (XtPointer)this );
+   XtAddEventHandler(_form, (KeyPressMask|KeyReleaseMask), 
+                     False, forwarder, (XtPointer)this );
 }
 
 NSPluginInstance::~NSPluginInstance()
@@ -429,6 +449,10 @@ void NSPluginInstance::destroy()
         if (saved)
           g_NPN_MemFree(saved);
 
+        XtRemoveEventHandler(_form, (KeyPressMask|KeyReleaseMask), 
+                             False, forwarder, (XtPointer)this);
+        XtRemoveEventHandler(_toplevel, (KeyPressMask|KeyReleaseMask), 
+                             False, forwarder, (XtPointer)this);
         XtDestroyWidget(_area);
         XtDestroyWidget(_form);
         XtDestroyWidget(_toplevel);
Index: viewer/nsplugin.h
===================================================================
RCS file: /home/kde/kdebase/nsplugins/viewer/nsplugin.h,v
retrieving revision 1.32
diff -u -3 -p -r1.32 nsplugin.h
--- viewer/nsplugin.h	9 Jul 2002 04:18:52 -0000	1.32
+++ viewer/nsplugin.h	21 Feb 2003 14:16:45 -0000
@@ -201,6 +201,8 @@ private slots:
 private:
   friend class NSPluginStreamBase;
 
+  static void forwarder(Widget, XtPointer, XEvent *, Boolean*);
+
   void destroy();
 
   bool _destroyed;

>> Visit http://mail.kde.org/mailman/listinfo/kde-devel#unsub to unsubscribe <<

[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic