1//************************************ bs::framework - Copyright 2018 Marko Pintera **************************************//
2//*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********//
3#include "String/BsUnicode.h"
4#include "Platform/BsPlatform.h"
5#include "Platform/BsDropTarget.h"
6#include "RenderAPI/BsRenderWindow.h"
7#include "Math/BsRect2I.h"
8#include "Private/Linux/BsLinuxDropTarget.h"
9#include "Private/Linux/BsLinuxWindow.h"
10#include "Private/Linux/BsLinuxPlatform.h"
11#include <X11/Xatom.h>
12#include <X11/Xlib.h>
13
14#undef None
15
16namespace bs
17{
18 ::Display* LinuxDragAndDrop::sXDisplay = nullptr;
19 bool LinuxDragAndDrop::sDragActive = false;
20 Vector<LinuxDragAndDrop::DropArea> LinuxDragAndDrop::sDropAreas;
21 Mutex LinuxDragAndDrop::sMutex;
22 INT32 LinuxDragAndDrop::sDNDVersion = 0;
23 Atom LinuxDragAndDrop::sDNDType = 0;
24 ::Window LinuxDragAndDrop::sDNDSource = 0;
25 Vector2I LinuxDragAndDrop::sDragPosition;
26 Vector<LinuxDragAndDrop::DragAndDropOp> LinuxDragAndDrop::sQueuedOperations;
27 Vector<LinuxDragAndDrop::DropAreaOp> LinuxDragAndDrop::sQueuedAreaOperations;
28
29 Atom LinuxDragAndDrop::sXdndAware;
30 Atom LinuxDragAndDrop::sXdndSelection;
31 Atom LinuxDragAndDrop::sXdndEnter;
32 Atom LinuxDragAndDrop::sXdndLeave;
33 Atom LinuxDragAndDrop::sXdndPosition;
34 Atom LinuxDragAndDrop::sXdndStatus;
35 Atom LinuxDragAndDrop::sXdndDrop;
36 Atom LinuxDragAndDrop::sXdndFinished;
37 Atom LinuxDragAndDrop::sXdndActionCopy;
38 Atom LinuxDragAndDrop::sXdndTypeList;
39 Atom LinuxDragAndDrop::sPRIMARY;
40
41 struct X11Property
42 {
43 UINT8* data;
44 INT32 format;
45 UINT32 count;
46 Atom type;
47 };
48
49 // Results must be freed using XFree
50 X11Property readX11Property(::Display *display, ::Window window, Atom type)
51 {
52 X11Property output;
53 output.data = nullptr;
54
55 unsigned long bytesLeft;
56 int bytesToFetch = 0;
57
58 do
59 {
60 if(output.data != nullptr)
61 XFree(output.data);
62
63 XGetWindowProperty(display, window, type, 0, bytesToFetch, False, AnyPropertyType,
64 &output.type, &output.format, (unsigned long*)&output.count, &bytesLeft, &output.data);
65 bytesToFetch += bytesLeft;
66
67 } while(bytesLeft != 0);
68
69 return output;
70 }
71
72 /**
73 * Decodes percent (%) encoded characters in an URI to actual characters. Characters are decoded into the input string.
74 */
75 void decodeURI(char* uri)
76 {
77 if(uri == nullptr)
78 return;
79
80 UINT32 length = (UINT32)strlen(uri);
81 UINT8 decodedChar = '\0';
82 UINT32 writeIdx = 0;
83 UINT32 decodeState = 0; // 0 - Not decoding, 1 - found a % char, 2- found a % and following char
84
85 for(UINT32 i = 0; i < length; i++)
86 {
87 // Not currently decoding, check for % or write as-is
88 if(decodeState == 0)
89 {
90 // Potentially an encoded char, start decode
91 if(uri[i] == '%')
92 {
93 decodedChar = '\0';
94 decodeState = 1;
95 }
96 else // Normal char, write as-is
97 {
98 uri[writeIdx] = uri[i];
99 writeIdx++;
100 }
101 }
102 else // Currently decoding, check if following chars are valid
103 {
104 char isa = uri[i] >= 'a' && uri[i] <= 'f';
105 char isA = uri[i] >= 'A' && uri[i] <= 'F';
106 char isn = uri[i] >= '0' && uri[i] <= '9';
107
108 bool isHex = isa || isA || isn;
109 if(!isHex)
110 {
111 // If not a hex, this can't be an encoded character. Write the last "decodeState" characters as normal
112 for(UINT32 j = decodeState; j > 0; j--)
113 {
114 uri[writeIdx] = uri[i - j];
115 writeIdx++;
116 }
117
118 decodeState = 0;
119 }
120 else
121 {
122 // Decode the hex character into a number
123 char offset = '\0';
124 if(isn)
125 offset = 0 - '0';
126 else if(isa)
127 offset = 10 - 'a';
128 else if(isA)
129 offset = 10 - 'A';
130
131 decodedChar |= (uri[i] + offset) << ((2 - decodeState) * 4);
132
133 // Check if done decoding and write
134 if(decodeState == 2)
135 {
136 uri[writeIdx] = decodedChar;
137 writeIdx++;
138 decodeState = 0;
139 }
140 else // decodeState == 1
141 decodeState = 2;
142 }
143 }
144 }
145
146 uri[writeIdx] = '\0';
147 }
148
149 char* convertURIToLocalPath(char* uri)
150 {
151 if (memcmp(uri, "file:/", 6) == 0)
152 uri += 6;
153 else if (strstr(uri, ":/") != nullptr)
154 return nullptr;
155
156 bool isLocal = uri[0] != '/' || (uri[0] != '\0' && uri[1] == '/');
157
158 // Ignore hostname
159 if (!isLocal && uri[0] == '/' && uri[2] != '/')
160 {
161 char* hostnameEnd = strchr(uri+1, '/');
162 if (hostnameEnd != nullptr)
163 {
164 char hostname[257];
165 if (gethostname(hostname, 255) == 0)
166 {
167 hostname[256] = '\0';
168 if (memcmp(uri+1, hostname, hostnameEnd - (uri+1)) == 0)
169 {
170 uri = hostnameEnd + 1;
171 isLocal = true;
172 }
173 }
174 }
175 }
176
177 if (isLocal)
178 {
179 decodeURI(uri);
180 if (uri[1] == '/')
181 uri++;
182 else
183 uri--;
184
185 return uri;
186 }
187
188 return nullptr;
189 }
190
191 DropTarget::DropTarget(const RenderWindow* ownerWindow, const Rect2I& area)
192 : mArea(area), mActive(false), mOwnerWindow(ownerWindow), mDropType(DropTargetType::None)
193 {
194 LinuxDragAndDrop::registerDropTarget(this);
195 }
196
197 DropTarget::~DropTarget()
198 {
199 LinuxDragAndDrop::unregisterDropTarget(this);
200
201 _clear();
202 }
203
204 void DropTarget::setArea(const Rect2I& area)
205 {
206 mArea = area;
207
208 LinuxDragAndDrop::updateDropTarget(this);
209 }
210
211 void LinuxDragAndDrop::startUp(::Display* xDisplay)
212 {
213 sXDisplay = xDisplay;
214
215#define INIT_ATOM(name) s##name = XInternAtom(xDisplay, #name, False);
216
217 INIT_ATOM(XdndAware)
218 INIT_ATOM(XdndSelection)
219 INIT_ATOM(XdndEnter)
220 INIT_ATOM(XdndLeave)
221 INIT_ATOM(XdndPosition)
222 INIT_ATOM(XdndStatus)
223 INIT_ATOM(XdndDrop)
224 INIT_ATOM(XdndFinished)
225 INIT_ATOM(XdndActionCopy)
226 INIT_ATOM(XdndTypeList)
227 INIT_ATOM(PRIMARY)
228
229#undef INIT_ATOM
230 }
231
232 void LinuxDragAndDrop::shutDown()
233 {
234 sXDisplay = nullptr;
235 }
236
237 void LinuxDragAndDrop::makeDNDAware(::Window xWindow)
238 {
239 UINT32 dndVersion = 5;
240 XChangeProperty(sXDisplay, xWindow, sXdndAware, XA_ATOM, 32, PropModeReplace, (unsigned char*)&dndVersion, 1);
241 }
242
243 void LinuxDragAndDrop::registerDropTarget(DropTarget* target)
244 {
245 Lock lock(sMutex);
246 sQueuedAreaOperations.push_back(DropAreaOp(target, DropAreaOpType::Register, target->getArea()));
247 }
248
249 void LinuxDragAndDrop::unregisterDropTarget(DropTarget* target)
250 {
251 Lock lock(sMutex);
252 sQueuedAreaOperations.push_back(DropAreaOp(target, DropAreaOpType::Unregister));
253 }
254
255 void LinuxDragAndDrop::updateDropTarget(DropTarget* target)
256 {
257 Lock lock(sMutex);
258 sQueuedAreaOperations.push_back(DropAreaOp(target, DropAreaOpType::Update, target->getArea()));
259 }
260
261 bool LinuxDragAndDrop::handleClientMessage(XClientMessageEvent& event)
262 {
263 // First handle any queued registration/unregistration
264 {
265 Lock lock(sMutex);
266
267 for(auto& entry : sQueuedAreaOperations)
268 {
269 switch(entry.type)
270 {
271 case DropAreaOpType::Register:
272 sDropAreas.push_back(DropArea(entry.target, entry.area));
273 break;
274 case DropAreaOpType::Unregister:
275 // Remove any operations queued for this target
276 for(auto iter = sQueuedOperations.begin(); iter !=sQueuedOperations.end();)
277 {
278 if(iter->target == entry.target)
279 iter = sQueuedOperations.erase(iter);
280 else
281 ++iter;
282 }
283
284 // Remove the area
285 {
286 auto iterFind = std::find_if(sDropAreas.begin(), sDropAreas.end(), [&](const DropArea& area)
287 {
288 return area.target == entry.target;
289 });
290
291 sDropAreas.erase(iterFind);
292 }
293
294 break;
295 case DropAreaOpType::Update:
296 {
297 auto iterFind = std::find_if(sDropAreas.begin(), sDropAreas.end(), [&](const DropArea& area)
298 {
299 return area.target == entry.target;
300 });
301
302 if (iterFind != sDropAreas.end())
303 iterFind->area = entry.area;
304 }
305 break;
306 }
307 }
308
309 sQueuedAreaOperations.clear();
310 }
311
312 // Source window notifies us a drag has just entered our window area
313 if(event.message_type == sXdndEnter)
314 {
315 sDNDSource = (::Window)event.data.l[0];
316 bool isList = (event.data.l[1] & 1) != 0;
317 sDNDVersion = (INT32)(event.data.l[1] >> 24);
318
319 // Get a list of properties are determine if there are any relevant ones
320 Atom* propertyList = nullptr;
321 UINT32 numProperties = 0;
322
323 // If more than 3 properties we need to read the list property to get them all
324 if(isList)
325 {
326 X11Property property = readX11Property(sXDisplay, sDNDSource, sXdndTypeList);
327
328 propertyList = (Atom*)property.data;
329 numProperties = property.count;
330 }
331 else
332 {
333 propertyList = bs_stack_alloc<Atom>(3);
334
335 for(int i = 0; i < 3; i++)
336 {
337 if(event.data.l[2 + i])
338 {
339 propertyList[i] = (Atom)event.data.l[2 + i];
340 numProperties++;
341 }
342 }
343 }
344
345 // Scan the list for URI list (file list), which is the only one we support (currently)
346 bool foundSupportedType = false;
347 for (UINT32 i = 0; i < numProperties; ++i)
348 {
349 char* name = XGetAtomName(sXDisplay, propertyList[i]);
350 if(strcmp(name, "text/uri-list") == 0)
351 {
352 sDNDType = propertyList[i];
353
354 XFree(name);
355 foundSupportedType = true;
356 break;
357 }
358
359 XFree(name);
360 }
361
362 // Free the property list
363 if(isList)
364 XFree(propertyList);
365 else
366 bs_stack_free(propertyList);
367
368 sDragActive = foundSupportedType;
369 }
370 // Cursor moved while drag is active (also includes the initial cursor activity when drag entered)
371 else if(event.message_type == sXdndPosition)
372 {
373 ::Window source = (::Window)event.data.l[0];
374
375 sDragPosition.x = (INT32)((event.data.l[2] >> 16) & 0xFFFF);
376 sDragPosition.y = (INT32)((event.data.l[2]) & 0xFFFF);
377
378 // Respond with a status message, we either accept or reject the dnd
379 XClientMessageEvent response;
380 bs_zero_out(response);
381
382 response.type = ClientMessage;
383 response.display = event.display;
384 response.window = source;
385 response.message_type = sXdndStatus;
386 response.format = 32;
387 response.data.l[0] = event.window;
388 response.data.l[1] = 0; // Reject drop by default
389 response.data.l[2] = 0; // Empty rectangle
390 response.data.l[3] = 0; // Empty rectangle
391 response.data.l[4] = sXdndActionCopy;
392
393 if(sDragActive)
394 {
395 for(auto& dropArea : sDropAreas)
396 {
397 LinuxWindow* linuxWindow;
398 dropArea.target->_getOwnerWindow()->getCustomAttribute("LINUX_WINDOW", &linuxWindow);
399 ::Window xWindow = linuxWindow->_getXWindow();
400
401 if(xWindow == event.window)
402 {
403 Vector2I windowPos = linuxWindow->screenToWindowPos(sDragPosition);
404 if(dropArea.area.contains(windowPos))
405 {
406 // Accept drop
407 response.data.l[1] = 1;
408
409 if(dropArea.target->_isActive())
410 {
411 Lock lock(sMutex);
412 sQueuedOperations.push_back(DragAndDropOp(DragAndDropOpType::DragOver, dropArea.target,
413 windowPos));
414 }
415 else
416 {
417 Lock lock(sMutex);
418 sQueuedOperations.push_back(DragAndDropOp(DragAndDropOpType::Enter, dropArea.target,
419 windowPos));
420 }
421
422 dropArea.target->_setActive(true);
423 }
424 else
425 {
426 // Cursor left previously active target's area
427 if(dropArea.target->_isActive())
428 {
429 {
430 Lock lock(sMutex);
431 sQueuedOperations.push_back(DragAndDropOp(DragAndDropOpType::Leave, dropArea.target));
432 }
433
434 dropArea.target->_setActive(false);
435 }
436 }
437 }
438 }
439 }
440
441 XSendEvent(sXDisplay, source, False, NoEventMask, (XEvent*)&response);
442 XFlush(sXDisplay);
443 }
444 // Cursor left the target window, or the drop was rejected
445 else if(event.message_type == sXdndLeave)
446 {
447 for(auto& dropArea : sDropAreas)
448 {
449 if(dropArea.target->_isActive())
450 {
451 {
452 Lock lock(sMutex);
453 sQueuedOperations.push_back(DragAndDropOp(DragAndDropOpType::Leave, dropArea.target));
454 }
455
456 dropArea.target->_setActive(false);
457 }
458 }
459
460 sDragActive = false;
461 }
462 else if(event.message_type == sXdndDrop)
463 {
464 ::Window source = (::Window)event.data.l[0];
465 bool dropAccepted = false;
466
467 if(sDragActive)
468 {
469 for (auto& dropArea : sDropAreas)
470 {
471 if (dropArea.target->_isActive())
472 dropAccepted = true;
473 }
474 }
475
476 if(dropAccepted)
477 {
478 ::Time timestamp;
479 if(sDNDVersion >= 1)
480 timestamp = (::Time)event.data.l[2];
481 else
482 timestamp = CurrentTime;
483
484 XConvertSelection(sXDisplay, sXdndSelection, sDNDType, sPRIMARY, LinuxPlatform::getMainXWindow(), timestamp);
485
486 // Now we wait for SelectionNotify
487 }
488 else
489 {
490 // Respond with a status message that we reject the drop
491 XClientMessageEvent response;
492 bs_zero_out(response);
493
494 response.type = ClientMessage;
495 response.display = event.display;
496 response.window = source;
497 response.message_type = sXdndFinished;
498 response.format = 32;
499 response.data.l[0] = LinuxPlatform::getMainXWindow();
500 response.data.l[1] = 0;
501 response.data.l[2] = 0;
502 response.data.l[3] = 0;
503 response.data.l[4] = 0;
504
505 XSendEvent(sXDisplay, source, False, NoEventMask, (XEvent*)&response);
506 XFlush(sXDisplay);
507
508 sDragActive = false;
509 }
510 }
511 else
512 return false;
513
514 return true;
515 }
516
517 bool LinuxDragAndDrop::handleSelectionNotify(XSelectionEvent& event)
518 {
519 if(event.target != sDNDType)
520 return false;
521
522 if(!sDragActive)
523 return true;
524
525 // Read data
526 X11Property property = readX11Property(sXDisplay, LinuxPlatform::getMainXWindow(), sPRIMARY);
527 if(property.format == 8)
528 {
529 // Assuming this is a file list, since we rejected any other drop type
530 Vector<Path> filePaths;
531
532 char* token = strtok((char*)property.data, "\r\n");
533 while(token != nullptr)
534 {
535 char* filePath = convertURIToLocalPath(token);
536 if(filePath != nullptr)
537 filePaths.push_back(String(filePath));
538
539 token = strtok(nullptr, "\r\n");
540 }
541
542 for(auto& dropArea : sDropAreas)
543 {
544 if(!dropArea.target->_isActive())
545 continue;
546
547 LinuxWindow* linuxWindow;
548 dropArea.target->_getOwnerWindow()->getCustomAttribute("LINUX_WINDOW", &linuxWindow);
549
550 Vector2I windowPos = linuxWindow->screenToWindowPos(sDragPosition);
551
552 Lock lock(sMutex);
553 sQueuedOperations.push_back(DragAndDropOp(DragAndDropOpType::Drop, dropArea.target, windowPos, filePaths));
554
555 dropArea.target->_setActive(false);
556 }
557 }
558
559 XFree(property.data);
560
561 // Respond with a status message that we accepted the drop
562 XClientMessageEvent response;
563 bs_zero_out(response);
564
565 response.type = ClientMessage;
566 response.display = event.display;
567 response.window = sDNDSource;
568 response.message_type = sXdndFinished;
569 response.format = 32;
570 response.data.l[0] = LinuxPlatform::getMainXWindow();
571 response.data.l[1] = 1;
572 response.data.l[2] = sXdndActionCopy;
573 response.data.l[3] = 0;
574 response.data.l[4] = 0;
575
576 XSendEvent(sXDisplay, sDNDSource, False, NoEventMask, (XEvent*)&response);
577 XFlush(sXDisplay);
578
579 sDragActive = false;
580
581 return true;
582 }
583
584 void LinuxDragAndDrop::update()
585 {
586 Vector<DragAndDropOp> operations;
587
588 {
589 Lock lock(sMutex);
590 std::swap(operations, sQueuedOperations);
591 }
592
593 for(auto& op : operations)
594 {
595 switch(op.type)
596 {
597 case DragAndDropOpType::Enter:
598 op.target->onEnter(op.position.x, op.position.y);
599 break;
600 case DragAndDropOpType::DragOver:
601 op.target->onDragOver(op.position.x, op.position.y);
602 break;
603 case DragAndDropOpType::Drop:
604 op.target->_setFileList(op.fileList);
605 op.target->onDrop(op.position.x, op.position.y);
606 break;
607 case DragAndDropOpType::Leave:
608 op.target->_clear();
609 op.target->onLeave();
610 break;
611 }
612 }
613 }
614}
615