I have implemented a Xdnd drop support implementation in VTK some time ago. It was working great except with Thunar file manager. All other file managers were working fine at the time. We dismissed this limitation a Thunar bug at the time.
The feature I implemented was very simple:
Set the window of the application to be XdndAware
Receive the position message and respond that we are ready to receive
Receive the drop mesage and request a selection
Receive the selection notify and recover the URI
Convert the URI into something we can work with
Nothing fancy, I did not even touch the list type.
Fast forward a few years and now dolphin users cannot drop files correctly into our application. The URI is always the first file dropped since dolphin was started. Restarting our application has no effect. No bug at all with pcmanfm.
This is not a dolphin bug and files can be dropped on blender or firefox from dolphin without issues.
So there must be a bug in our implementation, but I've been staring at the code for some time and everything I tried had no effect, except for breaking Xdnd support completely.
Here are the interesting part of the implementation:
//------------------------------------------------------------------------------
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;
}
}
[...]
And 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
The complete file can be seen here:
https://gitlab.kitware.com/vtk/vtk/-/blob/master/Rendering/UI/vtkXRenderWindowInteractor.cxx
You can follow my train of thoughts and debugs here:
https://gitlab.kitware.com/f3d/f3d/-/issues/228
To test this code, a simple way is to use F3D has it is using the dropped file, but a simple VTK application should work as well:
https://gitlab.kitware.com/f3d/f3d
Current Dolphin issue
From some testing, the issue is with the preparation and sending of the XdndFinished ClientMessage back to the drag and drop source when handling the SelectionNotify event.
Instead of:
reply.xclient.window = event->xclient.data.l[0];
the line should be:
reply.xclient.window = this->XdndSource;
This aligns the XClientMessageEvent window member with the target window argument to XSendEvent. This is probably a simple copy-paste error, as xclient isn't valid for the SelectionNotify event type. Most likely the actual value of window was not previously being checked, but that has been changed recently, hence the error.
The spec covers this quite well, and also raises a couple of other things to consider:
For data.l[1]: "Bit 0 is set if the current target accepted the drop and successfully performed the accepted drop action. (new in version 5)", so using itemCount as a value will technically be incorrect whenever the count is even
If the handling of XdndPosition doesn't need to actually track where the current position is (i.e. if you are just using the entire window as a drop target), you may be able to get away with sending the XdndStatus in response to the XdndEnter message
Previous Thunar issue
Looking into this further, I did some troubleshooting regarding the previous issue with Thunar, and it boils down to the code handling XdndDrop assuming that the format of the incoming data can be converted to UTF8_STRING. This diff for GLFW handles almost the exact same issue.
If, when handling the XdndEnter message, you inspect the values of xclient.data.l[2] through xclient.data.l[4] you can see that Dolphin reports supporting the following formats:
text/uri-list
text/x-moz-url
text/plain
whereas Thunar only supports the following:
text/uri-list
The simplest solution is to:
Keep track of a supported format when handling XdndEnter
Provide this format to XConvertSelection when handling XdndDrop (instead of UTF8_STRING)
Handle the format appropriately when handling the SelectionNotify event
To be more complete, if bit 0 of xclient.data.l[1] is set on the XdndEnter message, you should get the XdndTypeList property of the drag and drop source window and based the format selection on that, instead of the formats contained in the message itself.
Related
I am using QT and QCustomPlot to create a real time plotting tool, and the data of the plot is read from the Arduino UNO board. My application succeeded in plotting while the data is a total mess. Here is my code below (Some code is from QCustomPlot website):
void Dialog::realtimeDataSlot()
{
bool currentPortNameChanged = false;
QString currentPortName;
if (currentPortName != portName) {
currentPortName = portName;
currentPortNameChanged = true;
}
QString currentRequest = request;
QSerialPort serial;
if (currentPortNameChanged) {
serial.close();
serial.setPortName(currentPortName);
if (!serial.open(QIODevice::ReadOnly)) {
return;
}
}
static QTime time(QTime::currentTime());
// calculate two new data points:
double key = time.elapsed()/1000.0;
static double lastPointKey = 0;
if (key-lastPointKey > 0.002) // at most add point every 2 ms
{
// add data to lines:
if(serial.waitForReadyRead(-1)){
data = serial.readAll();
QTextStream(stdout) << "HERE:" << data.toDouble() << endl;
customPlot->graph(0)->addData(key, data.toDouble());
customPlot->graph(0)->rescaleValueAxis(); //rescale value (vertical) axis to fit the current data:
lastPointKey = key;
customPlot->xAxis->setRange(key, 8, Qt::AlignRight);
customPlot->replot();
static double lastFpsKey;
static int frameCount;
++frameCount;
if (key-lastFpsKey > 2) // average fps over 2 seconds
{
lastFpsKey = key;
frameCount = 0;
}
}
}
// calculate frames per second:
if (currentPortName != portName) {
currentPortName = portName;
currentPortNameChanged = true;
} else {
currentPortNameChanged = false;
}
}
When I Tried to print out the data I read from the serial port, I found the following:
HERE:1
HERE:15
HERE:150
HERE:149
HERE:149
HERE:149
HERE:150
HERE:150
HERE:15
HERE:150
HERE:149
HERE:49
HERE:150
HERE:150
HERE:1
HERE:150
The values around 150 are normal while the value that are 0, 1 to others are not. Also it is not print out at a stable speed. I don't know what happened to this, and thanks to whoever may help, and I would appreciate it if there is any better ways to implement this.
The problem here is that it is not guaranteed that the serial transmission is received all at once. So it is better to let the serial to be processed somewhere else, for instance:
// in the class definition
QSerialPort serialPort;
private slots:
void handleReadyRead();
private:
QByteArray serialBuffer;
volatile double lastSerialValue;
// In the initialization part (not the realtimeDataSlot function)
lastSerialValue = qQNaN();
serialPort.setPortName(currentPortName);
connect(&serialPort, &QSerialPort::readyRead, this, &Dialog::handleReadyRead, Qt::UniqueConnection);
if (!serialPort.open(QIODevice::ReadOnly)) {
return;
}
serialBuffer.clear();
// Other functions:
void Dialog::realtimeDataSlot()
{
...
if (key-lastPointKey > 0.002) // at most add point every 2 ms
{
if (!qIsNaN(lastSerialData))
{
// use lastSerialValue as the data.toDouble() you had before, then, at the end
lastSerialValue = qQNaN();
}
...
}
void Dialog::handleReadyRead()
{
serialBuffer.append(serialPort.readAll());
int serPos;
while ((serPos = serialBuffer.indexOf('\n')) >= 0)
{
bool ok;
double tempValue = QString::fromLatin1(serialBuffer.left(serPos)).toDouble(&ok);
if (ok) lastSerialValue = tempValue;
serialBuffer = serialBuffer.mid(serPos+1);
}
}
Explanation: whenever you receive something from the arduino the bytes are appended to a buffer. Then the byte array is parsed looking for a terminator, and if found the byte array is split and analysed. When the other function needs the data, it simply pulls the most recent one saved in the variable.
NOTE 1: I saw that you used a binary transmission. The problem is that you do not have any way to determine where the data begins and end in this way. For instance, if you receive 0x01 0x02 0x03 0x04 and you know that there are 3 bytes, are they 01..03 or 02..04 or 03, 04 and a missing one or...? The version I implemented requires you to send data in string format with a new-line terminator (simplest version, you just have to write Serial.println(doubleValue); in the arduino code), but if you need the binary version I can give you some hints
NOTE 2: The code I wrote is NOT thread safe. It will work only if the realtimeDataSlot and the handleReadyRead are called in the same thread. Note that if they belong to the same object and are called through signals this is guaranteed.
Now, this should work. But I highly discourage you from doing this. I don't know who needs to call the realtimeDataSlot(), but I think that the most correct version is something like this:
// in the class definition
QSerialPort serialPort;
private slots:
void handleReadyRead();
void receivedData(double val);
private:
QByteArray serialBuffer;
signals:
void newData(double data);
// In the initialization part (not the realtimeDataSlot function)
serialPort.setPortName(currentPortName);
connect(&serialPort, &QSerialPort::readyRead, this, &Dialog::handleReadyRead, Qt::UniqueConnection);
connect(this, &Dialog::newData, this, &Dialog::receivedData, Qt::UniqueConnection);
if (!serialPort.open(QIODevice::ReadOnly)) {
return;
}
serialBuffer.clear();
// Other functions:
void Dialog::receivedData(double val)
{
double key = time.elapsed()/1000.0;
static double lastPointKey = 0;
if (key-lastPointKey > 0.002) // at most add point every 2 ms
{
QTextStream(stdout) << "HERE:" << data.toDouble() << endl;
customPlot->graph(0)->addData(key, data.toDouble());
customPlot->graph(0)->rescaleValueAxis();
...
}
}
void Dialog::handleReadyRead()
{
serialBuffer.append(serialPort.readAll());
int serPos;
while ((serPos = serialBuffer.indexOf('\n')) >= 0)
{
bool ok;
double tempValue = QString::fromLatin1(serialBuffer.left(serPos)).toDouble(&ok);
if (ok) emit newData(tempValue);
serialBuffer = serialBuffer.mid(serPos+1);
}
}
So keep the graph responsive to events (received a new data) instead of to a timer.
One more thing: I removed the port change on purpose. I suggest you to handle it in another way: put a button to start and stop the serial, and when the serial port is started prevent the user from changing the port name. This way the user will explicitely need to shut it down when he needs to change the port. If you want your version, however, don't include it in your code, but make a slot on its own to call whenever you need to change the port name:
void changeSerialPortName(QString newName)
{
if (newName != serialPort.portName()) {
if (serialPort.isOpen())
serialPort.close();
serialPort.setPortName(newName);
if (!serialPort.open(QIODevice::ReadOnly)) {
return;
}
}
}
Has anyone found a way to revert a value on an editbox if it fails validation? If the value is invalid, it harasses the user with message boxes until they fix it.
void MyDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT_FOO, foo);
DDV_MinMaxFloat(pDX, foo, 0.001f, 300.0f);
}
I was able to do this by writing custom DDX_ handlers. The application I worked on used a custom control (MCReal derived from CEdit) that would only accept decimal values between an acceptable range defined in the control. When the user entered a non-decimal value, or, a value outside of the range, the code would pop up a custom message and revert the value entered into the dialog field.
This was accomplished by creating a custom control and a custom validation handler. Here’s what the DDX_ routine looked like:
void AFXAPI_EXPORT DDX_ProcessEditReal(CDataExchange* pDX, int nIDC, MCReal& mcr)
{
// prepare edit control
HWND hWndCtrl = pDX->PrepareEditCtrl(nIDC);
// does control exist yet?
if (!IsWindow(mcr.m_hWnd))
{
// subclass the control
if (!mcr.SubclassWindow(hWndCtrl))
{
ASSERT(false);
// possibly trying to subclass twice?
AfxThrowNotSupportedException();
}
return;
}
if (!ValidateMCRealCtrl (mcr, pDX->m_pDlgWnd, (pDX->m_bSaveAndValidate == TRUE)))
{
pDX->Fail ();
}
}
I used the standard DDX_ routines as a starting point to write a custom version. The real work is done in ValidateMCRealCtrl():
bool ValidateMCRealCtrl (MCReal &mcRealCtrl, CWnd *pParentWnd, bool bSaveAndValidate)
{
CString ctext;
double val = 0.0, r = 0.0;
double unit_factor = 0.0;
bool bDmsrg = false;
bool rc = false;
bool ret;
...
if (bSaveAndValidate) // Move from dialog to data
{
if (pParentWnd != nullptr && mcRealCtrl.CancelButtonClicked (pParentWnd))
{
return true;
}
if (!mcRealCtrl.IsWindowEnabled () || !mcRealCtrl.IsWindowVisible ())
{
return true;; // don't update if not enabled
}
mcRealCtrl.GetWindowText (ctext);
...
// base field validation.
ret = mcRealCtrl.Validate ();
if (!ret)
{
make_mcreal_str (r, ctext.GetBuffer (mcRealCtrl.maxlen), mcRealCtrl.maxlen, prec, mcRealCtrl.add_plus,
mcRealCtrl.m_strip_trailing == TRUE);
ctext.ReleaseBuffer ();
InvalidRealField (mcRealCtrl); // Bad value
return false; // Reset Focus
}
...
ctext.ReleaseBuffer ();
mcRealCtrl.SetWindowText (ctext);
}
else // Move from data to dialog
{
if (mcRealCtrl.angle) // If angle field...
{
val = mcRealCtrl.value * R2D; // Convert to degrees
}
else
{
val = mcRealCtrl.value; // Use data value
}
make_mcreal_str (val, ctext.GetBuffer (mcRealCtrl.maxlen), mcRealCtrl.maxlen, prec, mcRealCtrl.add_plus,
mcRealCtrl.m_strip_trailing == TRUE);
ctext.ReleaseBuffer ();
mcRealCtrl.SetWindowText (ctext);
mcRealCtrl.SetLimitText (mcRealCtrl.maxlen);
}
...
return true;
}
(Note: I've replaced code that does not pertain to your question with "...")
The work to revert the field value occurs in InvalidRealField (). That code displays a pop up message and uses the previous value of the field (saved within the actual MCReal control class), before it was changed, to revert the value.
This framework was not written specifically to revert incorrect dialog field values. It provides much more than that since the control class provides some extra capabilities. However, having the control defined in a custom class allowed me to provide custom validation.
I have a wxwidget application that uses dragandrop and when I close the application, it crashes on this line:
virtual ~wxDropTargetBase()
{ delete m_dataObject; }
I setup the drapand drop in this way:
MainWindow::MainWindow() : MainWindowTemplate(NULL), m_fileDropTarget(textSourceFolder)
{
// connect events
this->Connect(wxEVT_IDLE, wxIdleEventHandler(MainWindow::OnIdle));
// set window minimum size - work around bug that ignores outer border and sets min size slightly too small
wxSize minSize = sizerOuter->GetMinSize();
minSize.SetWidth(minSize.GetWidth() + 16);
minSize.SetHeight(minSize.GetHeight() + 16);
SetMinSize(minSize);
Layout();
// set file drop target
SetDropTarget(&m_fileDropTarget);
}
and the source code for my
class MyFileDropTarget : public wxFileDropTarget
{
public:
MyFileDropTarget(wxTextCtrl *textCtrl)
{
m_fileTextCtrl = textCtrl;
}
virtual bool wxFileDropTarget::OnDropFiles (wxCoord x, wxCoord y, const wxArrayString &filenames)
{
if (filenames.size() > 0)
{
m_fileTextCtrl->SetValue(filenames.Item(0));
return true;
}
return false;
}
private:
wxTextCtrl *m_fileTextCtrl;
};
what is the problem and how I can fix it?
It looks like your m_fileDropTarget is an object, in which case it is deleted twice, as when you call SetDropTarget() it takes ownership of the pointer passed to it.
I have a Qt app which grabs a key with XGrabKey. I want my app to show and hide, if I press the hotkey. Additionally it has to hide if it loses focus. This works well with one drawback: The XGrabKeyboard which is used by XGrabKey, generates a FocusIn ans FocusOut event. This implies that, if I press the hotkey when the app is visible, the app receives the FocusOut event, hides and immendiately after that receives the hotkeyevent and shows again.
Can I somehow avoid the X server to generate these focus events?
Diggin deeper for about a few hours I tinkered a solution that feels nice. It uses Qt QWidget::nativeEvent and libxcb. libxcb seems to be the next gen libX11 wrapper. But it is horribly undocumented. Uncool that Qt does not provide the mode of a QFocusEvent. But I guess thats the bane in everything wanting to be platform agnostic.
Note: This is Qt5, Qt4 had stuff like QWidget::x11info()
.h
class Class : public QWidget
{
Q_OBJECT
public:
Class(QWidget *parent = 0);
~Class();
protected:
virtual bool nativeEvent(const QByteArray &eventType, void *message, long *) override;
};
.cpp
/**************************************************************************//**
* #brief Class::nativeEvent
* This special event handler can be reimplemented in a subclass to receive
* native platform events identified by eventType which are passed in the
* message parameter. In your reimplementation of this function, if you want to
* stop the event being handled by Qt, return true and set result. If you
* return false, this native event is passed back to Qt, which translates the
* event into a Qt event and sends it to the widget.
*
* This method is called for every native event. On X11, eventType is set to
* "xcb_generic_event_t", and the message can be casted to a
* xcb_generic_event_t pointer.
*
* #param eventType
* #param message
* #return Indicator if this event shall be stoped being handled further.
*/
bool Class::nativeEvent(const QByteArray &eventType, void *message, long *)
{
if (eventType == "xcb_generic_event_t")
{
xcb_generic_event_t* event = static_cast<xcb_generic_event_t *>(message);
switch (event->response_type & ~0x80)
{
case XCB_FOCUS_IN: {
xcb_focus_in_event_t *fe = (xcb_focus_in_event_t *)event;
if (fe->mode & (XCB_NOTIFY_MODE_GRAB|XCB_NOTIFY_MODE_UNGRAB)){
return true; // Ignore this events
}
break;
}
case XCB_FOCUS_OUT: {
xcb_focus_out_event_t *fe = (xcb_focus_out_event_t *)event;
if (fe->mode & (XCB_NOTIFY_MODE_GRAB|XCB_NOTIFY_MODE_UNGRAB)){
return true; // Ignore this events
}
break;
}
}
}
return false;
}
I don't think so, but you can check the "mode" field in the XFocusChangeEvent. It can be NotifyNormal, NotifyWhileGrabbed, NotifyGrab, or NotifyUngrab. Grabbing keys should generate events with the last two modes, and so you may choose to ignore events with these modes, though I'm not sure how you would do that with Qt.
Currently, we use QFileSystemWatcher, belonging to Qt. Due to the limited support in Mac OS X, it can only notify us two events: directory changed, or file changed.
However, the latter event (file changed) is triggered multiple times when its size is slightly larger and the writing to disk takes slightly longer time.
Our workaround is to set up a timer to check the file in 1 second. If more signals about the file come before timer expires, we reset the timer.
Is there a way to get the notification when the file is written to the disk (finished writing)? There is no need to limit to Qt, any library could do.
We are aware of the kqueue monitoring method, but that's too low level and we don't want to do that for each file, as we are monitoring a large portion of the file system..
I have the same problem in a project and finally I decide to implement a native watcher. It's pretty easy:
In the .h:
class OSXWatcher : public Watcher
{
public:
OSXWatcher(const QString& strDirectory);
virtual ~OSXWatcher();
virtual bool Start();
virtual bool Stop();
private:
/**
* Callback function of the OS X FSEvent API.
*/
static void fileSystemEventCallback(ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]);
FSEventStreamRef stream;
};
The .cpp:
bool OSXWatcher::Start()
{
CFStringRef pathToWatchCF = CFStringCreateWithCString(NULL, this->dirToWatch.toUtf8().constData(), kCFStringEncodingUTF8);
CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void **)&pathToWatchCF, 1, NULL);
FSEventStreamContext context;
context.version = 0;
context.info = this;
context.retain = NULL;
context.release = NULL;
context.copyDescription = NULL;
stream = FSEventStreamCreate(NULL, &OSXWatcher::fileSystemEventCallback, &context, pathsToWatch, kFSEventStreamEventIdSinceNow, 3.0, kFSEventStreamCreateFlagFileEvents);
FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamStart(stream);
CFRelease(pathToWatchCF);
// Read the folder content to protect any unprotected or pending file
ReadFolderContent();
}
bool OSXWatcher::Stop()
{
FSEventStreamStop(stream);
FSEventStreamInvalidate(stream);
FSEventStreamRelease(stream);
}
void OSXWatcher::fileSystemEventCallback(ConstFSEventStreamRef /*streamRef*/, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[])
{
char **paths = (char **)eventPaths;
for (size_t i=0; i<numEvents; i++) {
// When a file is created we receive first a kFSEventStreamEventFlagItemCreated and second a (kFSEventStreamEventFlagItemCreated & kFSEventStreamEventFlagItemModified)
// when the file is finally copied. Catch this second event.
if (eventFlags[i] & kFSEventStreamEventFlagItemCreated
&& eventFlags[i] & kFSEventStreamEventFlagItemModified
&& !(eventFlags[i] & kFSEventStreamEventFlagItemIsDir)
&& !(eventFlags[i] & kFSEventStreamEventFlagItemIsSymlink)
&& !(eventFlags[i] & kFSEventStreamEventFlagItemFinderInfoMod)) {
OSXWatcher *watcher = (OSXWatcher *)clientCallBackInfo;
if (watcher->FileValidator(paths[i]))
emit watcher->yourSignalHere();
}
}
}
I have the same problem but with folder. When you copy many files to the folder too many singals are emitted but I need only one. So I have the following solution:
void folderChanged(const QString& folder)
{
m_pTimerForChanges->start();
}
folderChanged is a slot for directoryChanged() signal. And timer has another connection for timeout, so when time is out then processing should be done. Timer has 1s interval. Idea behind it is that folder should not be updated more frequent than interval I have and if it sends signals more frequently than I need then I don't need to process them immediately. Rather I restart timer every time the signal is emmited and with all of it I have the only one processing of changes. I think you can apply the same approach.
Another approach which may work for you also is to check file modification date in your processing and if its current modification date is within some epsilon(small interval) with your last modification date then you have repeating signal and should not react on it. Store this modification date and proceed.