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 | |
16 | namespace 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 | |