问题描述
我前段时间在 VTK 中实现了 Xdnd drop 支持实现。除了 Thunar 文件管理器外,它工作得很好。当时所有其他文件管理器都运行良好。我们当时认为这个限制是一个 Thunar 错误。
我实现的功能非常简单:
没什么特别的,我什至没有触及列表类型。
快进几年,现在 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
。这可能是一个简单的复制粘贴错误,因为 xclient
对 SelectionNotify
事件类型无效。很可能之前未检查 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
属性并基于格式选择在此之上,而不是消息本身中包含的格式。