Xdnd drop 支持错误的实现

Xdnd drop support faulty implementation

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

我实现的功能很简单:

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

快进几年,现在海豚用户无法将文件正确地放入我们的应用程序中。 URI 始终是 dolphin 启动后删除的第一个文件。重新启动我们的应用程序没有任何效果。 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->WindowId, &this->KillAtom, 1);

  // Enable drag and drop
  Atom xdndAwareAtom = XInternAtom(this->DisplayId, "XdndAware", False);
  char xdndVersion = 5;
  XChangeProperty(this->DisplayId, this->WindowId, 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,
        0, 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, protocol, unused0, unused1, hostname, 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, 0, 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, False, 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, this->WindowId, 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, 0, 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], False, NoEventMask, &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), xdndSelectionAtom, this->WindowId,
          CurrentTime);
      }
      else if (static_cast<Atom>(event->xclient.data.l[0]) == this->KillAtom)
      {
        this->ExitCallback();
      }
    }
    break;
  }
}
[...]

和header:

#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 问题

根据一些测试,问题出在处理 SelectionNotify 事件时准备和发送 XdndFinished ClientMessage 回拖放源。

而不是:

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

该行应该是:

reply.xclient.window = this->XdndSource;

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

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

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

上一个 Thunar 问题

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

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

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

而 Thunar 仅支持以下内容:

  • text/uri-list

最简单的解决方案是:

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

为了更完整,如果在XdndEnter消息上设置了xclient.data.l[1]的位0,您应该得到拖放源的XdndTypeList 属性 window 并以此为基础选择格式,而不是邮件本身包含的格式。