Xdnd drop 支持错误的实现

Mat*_*hal 5 c++ drag-and-drop xorg drop

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

我实现的功能非常简单:

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

没什么特别的,我什至没有碰过列表类型。

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

这不是 Dolphin 错误,文件可以从 Dolphin 放入搅拌机或 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;
  }
}
[...]
Run Code Online (Sandbox Code Playgroud)

和标题:

#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
Run Code Online (Sandbox Code Playgroud)

完整的文件可以在这里看到: 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

msb*_*bit 2

当前的海豚问题

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

代替:

reply.xclient.window = event->xclient.data.l[0];
Run Code Online (Sandbox Code Playgroud)

该行应该是:

reply.xclient.window = this->XdndSource;
Run Code Online (Sandbox Code Playgroud)

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

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

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

上一期 Thunar 问题

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

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

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

而 Thunar 仅支持以下内容:

  • text/uri-list

最简单的解决方案是:

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

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