Xdnd drop 支持错误的实现 当前的 Dolphin 问题上一期 Thunar 问题

问题描述

我前段时间在 VTK 中实现了 Xdnd drop 支持实现。除了 Thunar 文件管理器外,它工作得很好。当时所有其他文件管理器都运行良好。我们当时认为这个限制是一个 Thunar 错误

我实现的功能非常简单:

  • 将应用程序的窗口设置为 XdndAware
  • 接收位置信息并回复我们已准备好接收
  • 接收丢弃消息并请求选择
  • 接收选择通知并恢复URI
  • 将 URI 转换为我们可以使用的内容

没什么特别的,我什至没有触及列表类型。

快进几年,现在 dolphin 用户无法将文件正确放入我们的应用程序中。自 dolphin 启动以来,URI 始终是第一个删除文件。重新启动我们的应用程序没有任何效果。 pcmanfm 完全没有错误

这不是 dolphin 的错误文件可以从 dolphin 放到 Blender 或 firefox 上而不会出现问题。

所以我们的实现中肯定有一个错误,但我已经盯着代码看了一段时间,我尝试的一切都没有效果,除了完全破坏 Xdnd 支持

这是实现中有趣的部分:

//------------------------------------------------------------------------------
vtkXRenderWindowInteractor::vtkXRenderWindowInteractor()
{
  this->Internal = new vtkXRenderWindowInteractorInternals;
  this->displayId = nullptr;
  this->WindowId = 0;
  this->KillAtom = 0;
  this->XdndSource = 0;
  this->XdndPositionAtom = 0;
  this->XdndDropAtom = 0;
  this->XdndActioncopyAtom = 0;
  this->XdndStatusAtom = 0;
  this->XdndFinishedAtom = 0;
}

[...]


//------------------------------------------------------------------------------
void vtkXRenderWindowInteractor::Enable()
{
  // avoid cycles of calling Initialize() and Enable()
  if (this->Enabled)
  {
    return;
  }

  // Add the event handler to the system.
  // If we change the types of events processed by this handler,then
  // we need to change the disable() routine to match.  In order for disable()
  // to work properly,both the callback function AND the client data
  // passed to XtAddEventHandler and XtRemoveEventHandler must MATCH
  // PERFECTLY
  XSelectInput(this->displayId,this->WindowId,KeyPressMask | keyreleaseMask | ButtonPressMask | ButtonReleaseMask | ExposureMask |
      StructureNotifyMask | EnterWindowMask | LeaveWindowMask | PointerMotionHintMask |
      PointerMotionMask);

  // Setup for capturing the window deletion
  this->KillAtom = XInternAtom(this->displayId,"WM_DELETE_WINDOW",False);
  XSetWMProtocols(this->displayId,&this->KillAtom,1);

  // Enable drag and drop
  Atom xdndAwareAtom = XInternAtom(this->displayId,"XdndAware",False);
  char xdndVersion = 5;
  XChangeProperty(this->displayId,xdndAwareAtom,XA_ATOM,32,PropModeReplace,(unsigned char*)&xdndVersion,1);
  this->XdndPositionAtom = XInternAtom(this->displayId,"XdndPosition",False);
  this->XdndDropAtom = XInternAtom(this->displayId,"XdndDrop",False);
  this->XdndActioncopyAtom = XInternAtom(this->displayId,"XdndActioncopy",False);
  this->XdndStatusAtom = XInternAtom(this->displayId,"XdndStatus",False);
  this->XdndFinishedAtom = XInternAtom(this->displayId,"XdndFinished",False);

  this->Enabled = 1;

  this->Modified();
}

[...]

//------------------------------------------------------------------------------
void vtkXRenderWindowInteractor::dispatchEvent(XEvent* event)
{
  int xp,yp;

  switch (event->type)
  {
[...]

    // Selection request for drag and drop has been delivered
    case SelectionNotify:
    {
      // Sanity checks
      if (!event->xselection.property || !this->XdndSource)
      {
        return;
      }

      // Recover the dropped file
      char* data = nullptr;
      Atom actualType;
      int actualFormat;
      unsigned long itemCount,bytesAfter;
      XGetwindowProperty(this->displayId,event->xselection.requestor,event->xselection.property,LONG_MAX,False,event->xselection.target,&actualType,&actualFormat,&itemCount,&bytesAfter,(unsigned char**)&data);

      // Conversion checks
      if ((event->xselection.target != AnyPropertyType && actualType != event->xselection.target) ||
        itemCount == 0)
      {
        return;
      }

      // Recover filepaths from uris and invoke DropFilesEvent
      std::stringstream uris(data);
      std::string uri,protocol,hostname,filePath;
      std::string unused0,unused1,unused2,unused3;
      vtkNew<vtkStringArray> filePaths;
      while (std::getline(uris,uri,'\n'))
      {
        if (vtksys::SystemTools::ParseURL(
              uri,unused0,unused3,filePath,true))
        {
          if (protocol == "file" && (hostname.empty() || hostname == "localhost"))
          {
            // The uris can be crlf delimited,remove ending \r if any
            if (filePath.back() == '\r')
            {
              filePath.pop_back();
            }

            // The extracted filepath miss the first slash
            filePath.insert(0,"/");

            filePaths->InsertNextValue(filePath);
          }
        }
      }
      this->InvokeEvent(vtkCommand::DropFilesEvent,filePaths);
      XFree(data);

      // Inform the source the the drag and drop operation was sucessfull
      XEvent reply;
      memset(&reply,sizeof(reply));

      reply.type = ClientMessage;
      reply.xclient.window = event->xclient.data.l[0];
      reply.xclient.message_type = this->XdndFinishedAtom;
      reply.xclient.format = 32;
      reply.xclient.data.l[0] = this->WindowId;
      reply.xclient.data.l[1] = itemCount;
      reply.xclient.data.l[2] = this->XdndActioncopyAtom;

      XSendEvent(this->displayId,this->XdndSource,NoEventMask,&reply);
      XFlush(this->displayId);
      this->XdndSource = 0;
    }
    break;


    case ClientMessage:
    {
      if (event->xclient.message_type == this->XdndPositionAtom)
      {
        // Drag and drop event inside the window

        // Recover the position
        int xWindow,yWindow;
        int xRoot = event->xclient.data.l[2] >> 16;
        int yRoot = event->xclient.data.l[2] & 0xffff;
        Window root = DefaultRootwindow(this->displayId);
        Window child;
        XTranslateCoordinates(
          this->displayId,root,xRoot,yRoot,&xWindow,&yWindow,&child);

        // Convert it to VTK compatible location
        double location[2];
        location[0] = static_cast<double>(xWindow);
        location[1] = static_cast<double>(this->Size[1] - yWindow - 1);
        this->InvokeEvent(vtkCommand::UpdateDropLocationEvent,location);

        // Reply that we are ready to copy the dragged data
        XEvent reply;
        memset(&reply,sizeof(reply));

        reply.type = ClientMessage;
        reply.xclient.window = event->xclient.data.l[0];
        reply.xclient.message_type = this->XdndStatusAtom;
        reply.xclient.format = 32;
        reply.xclient.data.l[0] = this->WindowId;
        reply.xclient.data.l[1] = 1; // Always accept the dnd with no rectangle
        reply.xclient.data.l[2] = 0; // Specify an empty rectangle
        reply.xclient.data.l[3] = 0;
        reply.xclient.data.l[4] = this->XdndActioncopyAtom;

        XSendEvent(this->displayId,event->xclient.data.l[0],&reply);
        XFlush(this->displayId);
      }
      else if (event->xclient.message_type == this->XdndDropAtom)
      {
        // Item dropped in the window
        // Store the source of the drag and drop
        this->XdndSource = event->xclient.data.l[0];

        // Ask for a conversion of the selection. This will trigger a SelectioNotify event later.
        Atom xdndSelectionAtom = XInternAtom(this->displayId,"XdndSelection",False);
        XConvertSelection(this->displayId,xdndSelectionAtom,XInternAtom(this->displayId,"UTF8_STRING",False),CurrentTime);
      }
      else if (static_cast<Atom>(event->xclient.data.l[0]) == this->KillAtom)
      {
        this->ExitCallback();
      }
    }
    break;
  }
}
[...]

标题

#include "vtkRenderWindowInteractor.h"
#include "vtkRenderingUIModule.h" // For export macro
#include <X11/Xlib.h>             // Needed for X types in the public interface

class vtkCallbackCommand;
class vtkXRenderWindowInteractorInternals;

class VTKRENDERINGUI_EXPORT vtkXRenderWindowInteractor : public vtkRenderWindowInteractor
{
public:
  static vtkXRenderWindowInteractor* New();
  vtkTypeMacro(vtkXRenderWindowInteractor,vtkRenderWindowInteractor);
  void PrintSelf(ostream& os,vtkIndent indent) override;

  /**
   * Initializes the event handlers without an XtAppContext.  This is
   * good for when you don't have a user interface,but you still
   * want to have mouse interaction.
   */
  void Initialize() override;

  /**
   * Break the event loop on 'q','e' keypress. Want more ???
   */
  void TerminateApp() override;

  /**
   * Run the event loop and return. This is provided so that you can
   * implement your own event loop but yet use the vtk event handling as
   * well.
   */
  void ProcessEvents() override;

  ///@{
  /**
   * Enable/disable interactions.  By default interactors are enabled when
   * initialized.  Initialize() must be called prior to enabling/disabling
   * interaction. These methods are used when a window/widget is being
   * shared by multiple renderers and interactors.  This allows a "modal"
   * display where one interactor is active when its data is to be displayed
   * and all other interactors associated with the widget are disabled
   * when their data is not displayed.
   */
  void Enable() override;
  void disable() override;
  ///@}

  /**
   * Update the Size data member and set the associated RenderWindow's
   * size.
   */
  void UpdateSize(int,int) override;

  /**
   * Re-defines virtual function to get mouse position by querying X-server.
   */
  void GetMousePosition(int* x,int* y) override;

  void dispatchEvent(XEvent*);

protected:
  vtkXRenderWindowInteractor();
  ~vtkXRenderWindowInteractor() override;

  /**
   * Update the Size data member and set the associated RenderWindow's
   * size but do not resize the XWindow.
   */
  void UpdateSizeNoXResize(int,int);

  // Using static here to avoid destroying context when many apps are open:
  static int NumAppInitialized;

  display* displayId;
  Window WindowId;
  Atom KillAtom;
  int PositionBeforeStereo[2];
  vtkXRenderWindowInteractorInternals* Internal;

  // Drag and drop related
  Window XdndSource;
  Atom XdndPositionAtom;
  Atom XdndDropAtom;
  Atom XdndActioncopyAtom;
  Atom XdndStatusAtom;
  Atom XdndFinishedAtom;

  ///@{
  /**
   * X-specific internal timer methods. See the superclass for detailed
   * documentation.
   */
  int InternalCreateTimer(int timerId,int timerType,unsigned long duration) override;
  int InternalDestroyTimer(int platformTimerId) override;
  ///@}

  void FireTimers();

  /**
   * This will start up the X event loop and never return. If you
   * call this method it will loop processing X events until the
   * application is exited.
   */
  void StartEventLoop() override;

private:
  vtkXRenderWindowInteractor(const vtkXRenderWindowInteractor&) = delete;
  void operator=(const vtkXRenderWindowInteractor&) = delete;
};

#endif

完整的文件可以在这里看到: https://gitlab.kitware.com/vtk/vtk/-/blob/master/Rendering/UI/vtkXRenderWindowInteractor.cxx

你可以在这里按照我的思路和调试: https://gitlab.kitware.com/f3d/f3d/-/issues/228

要测试此代码一个简单的方法是使用 F3D 使用已删除文件,但一个简单的 VTK 应用程序也应该可以工作: https://gitlab.kitware.com/f3d/f3d

解决方法

当前的 Dolphin 问题

从一些测试来看,问题在于处理 XdndFinished 事件时 ClientMessage SelectionNotify 的准备和发送回拖放源。

代替:

reply.xclient.window = event->xclient.data.l[0];

该行应该是:

reply.xclient.window = this->XdndSource;

这会将 XClientMessageEvent window 成员与目标窗口参数对齐为 XSendEvent。这可能是一个简单的复制粘贴错误,因为 xclientSelectionNotify 事件类型无效。很可能之前未检查 window 的实际值,但最近已更改,因此出现错误。

spec 很好地涵盖了这一点,还提出了一些其他需要考虑的事项:

  • 对于 data.l[1]:“如果当前目标接受放置并成功执行接受的放置操作,则设置位 0。(版本 5 中的新增功能)”,因此在技术上使用 itemCount 作为值计数为偶数时不正确
  • 如果 XdndPosition 的处理不需要实际跟踪当前位置的位置(即,如果您只是将整个窗口用作放置目标),则您可以避免发送XdndStatus 响应 XdndEnter 消息

上一期 Thunar 问题

进一步研究这个问题,我对 Thunar 的前一个问题进行了一些故障排除,它归结为代码处理 XdndDrop,假设传入数据的格式可以转换为 UTF8_STRING。 GLFW 的此 diff 处理几乎完全相同的问题。

如果在处理 XdndEnter 消息时检查 xclient.data.l[2]xclient.data.l[4] 的值,您可以看到 Dolphin 报告支持以下格式:

  • text/uri-list
  • text/x-moz-url
  • text/plain

而 Thunar 仅支持以下内容:

  • text/uri-list

最简单的解决方案是:

  • 在处理 XdndEnter 时跟踪支持的格式
  • 在处理 XConvertSelection(而不是 XdndDrop)时将此格式提供给 UTF8_STRING
  • 在处理 SelectionNotify 事件时适当处理格式

更完整地说,如果在 xclient.data.l[1] 消息上设置了 XdndEnter 的第 0 位,您应该获取拖放源窗口的 XdndTypeList 属性并基于格式选择在此之上,而不是消息本身中包含的格式。