BsLinuxDragAndDrop.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. #include "String/BsUnicode.h"
  4. #include "Platform/BsPlatform.h"
  5. #include "RenderAPI/BsRenderWindow.h"
  6. #include "BsLinuxDragAndDrop.h"
  7. #include "BsLinuxWindow.h"
  8. #include "BsLinuxPlatform.h"
  9. #include <X11/Xatom.h>
  10. #include <X11/Xlib.h>
  11. namespace bs
  12. {
  13. ::Display* LinuxDragAndDrop::sXDisplay = nullptr;
  14. bool LinuxDragAndDrop::sDragActive = false;
  15. Vector<OSDropTarget*> LinuxDragAndDrop::sTargets;
  16. Mutex LinuxDragAndDrop::sMutex;
  17. INT32 LinuxDragAndDrop::sDNDVersion = 0;
  18. ::Window LinuxDragAndDrop::sDNDSource = None;
  19. Vector2I LinuxDragAndDrop::sDragPosition;
  20. Vector<LinuxDragAndDrop::DragAndDropOp> LinuxDragAndDrop::sQueuedOperations;
  21. Vector<OSDropTarget*> LinuxDragAndDrop::sTargetsToRegister;
  22. Vector<OSDropTarget*> LinuxDragAndDrop::sTargetsToUnregister;
  23. Atom LinuxDragAndDrop::sXdndAware;
  24. Atom LinuxDragAndDrop::sXdndSelection;
  25. Atom LinuxDragAndDrop::sXdndEnter;
  26. Atom LinuxDragAndDrop::sXdndLeave;
  27. Atom LinuxDragAndDrop::sXdndPosition;
  28. Atom LinuxDragAndDrop::sXdndStatus;
  29. Atom LinuxDragAndDrop::sXdndDrop;
  30. Atom LinuxDragAndDrop::sXdndFinished;
  31. Atom LinuxDragAndDrop::sXdndActionCopy;
  32. Atom LinuxDragAndDrop::sXdndTypeList;
  33. struct X11Property
  34. {
  35. UINT8* data;
  36. INT32 format;
  37. UINT32 count;
  38. Atom type;
  39. };
  40. // Results must be freed using XFree
  41. X11Property readX11Property(::Display *display, ::Window window, Atom type)
  42. {
  43. X11Property output;
  44. unsigned long bytesLeft;
  45. int bytesToFetch = 0;
  46. do
  47. {
  48. if(output.data != nullptr)
  49. XFree(output.data);
  50. XGetWindowProperty(display, window, type, 0, bytesToFetch, False, AnyPropertyType,
  51. &output.type, &output.format, (unsigned long*)&output.count, &bytesLeft, &output.data);
  52. bytesToFetch += bytesLeft;
  53. } while(bytesLeft != 0);
  54. return output;
  55. }
  56. /**
  57. * Decodes percent (%) encoded characters in an URI to actual characters. Characters are decoded into the input string.
  58. */
  59. void decodeURI(char* uri)
  60. {
  61. if(uri == nullptr)
  62. return;
  63. UINT32 length = (UINT32)strlen(uri);
  64. UINT8 decodedChar = '\0';
  65. UINT32 writeIdx = 0;
  66. UINT32 decodeState = 0; // 0 - Not decoding, 1 - found a % char, 2- found a % and following char
  67. for(UINT32 i = 0; i < length; i++)
  68. {
  69. // Not currently decoding, check for % or write as-is
  70. if(decodeState == 0)
  71. {
  72. // Potentially an encoded char, start decode
  73. if(uri[i] == '%')
  74. {
  75. decodedChar = '\0';
  76. decodeState = 1;
  77. }
  78. else // Normal char, write as-is
  79. {
  80. uri[writeIdx] = uri[i];
  81. writeIdx++;
  82. }
  83. }
  84. else // Currently decoding, check if following chars are valid
  85. {
  86. char isa = uri[i] >= 'a' && uri[i] <= 'f';
  87. char isA = uri[i] >= 'A' && uri[i] <= 'F';
  88. char isn = uri[i] >= '0' && uri[i] <= '9';
  89. bool isHex = isa || isA || isn;
  90. if(!isHex)
  91. {
  92. // If not a hex, this can't be an encoded character. Write the last "decodeState" characters as normal
  93. for(UINT32 j = decodeState; j > 0; j--)
  94. {
  95. uri[writeIdx] = uri[i - j];
  96. writeIdx++;
  97. }
  98. decodeState = 0;
  99. }
  100. else
  101. {
  102. // Decode the hex character into a number
  103. char offset = '\0';
  104. if(isn)
  105. offset = 0 - '0';
  106. else if(isa)
  107. offset = 10 - 'a';
  108. else if(isA)
  109. offset = 10 - 'A';
  110. decodedChar |= (uri[i] + offset) << ((2 - decodeState) * 4);
  111. // Check if done decoding and write
  112. if(decodeState == 2)
  113. {
  114. uri[writeIdx] = decodedChar;
  115. writeIdx++;
  116. decodeState = 0;
  117. }
  118. else // decodeState == 1
  119. decodeState = 2;
  120. }
  121. }
  122. }
  123. uri[writeIdx] = '\0';
  124. }
  125. char* convertURIToLocalPath(char* uri)
  126. {
  127. if (memcmp(uri, "file:/", 6) == 0)
  128. uri += 6;
  129. else if (strstr(uri, ":/") != nullptr)
  130. return nullptr;
  131. bool isLocal = uri[0] != '/' || (uri[0] != '\0' && uri[1] == '/');
  132. // Ignore hostname
  133. if (!isLocal && uri[0] == '/' && uri[2] != '/')
  134. {
  135. char* hostnameEnd = strchr(uri+1, '/');
  136. if (hostnameEnd != nullptr)
  137. {
  138. char hostname[257];
  139. if (gethostname(hostname, 255) == 0)
  140. {
  141. hostname[256] = '\0';
  142. if (memcmp(uri+1, hostname, hostnameEnd - (uri+1)) == 0)
  143. {
  144. uri = hostnameEnd + 1;
  145. isLocal = true;
  146. }
  147. }
  148. }
  149. }
  150. if (isLocal)
  151. {
  152. decodeURI(uri);
  153. if (uri[1] == '/')
  154. uri++;
  155. else
  156. uri--;
  157. return uri;
  158. }
  159. return nullptr;
  160. }
  161. void LinuxDragAndDrop::startUp(::Display* xDisplay)
  162. {
  163. #define INIT_ATOM(name) s##name = XInternAtom(xDisplay, #name, False);
  164. INIT_ATOM(XdndAware)
  165. INIT_ATOM(XdndSelection)
  166. INIT_ATOM(XdndEnter)
  167. INIT_ATOM(XdndLeave)
  168. INIT_ATOM(XdndPosition)
  169. INIT_ATOM(XdndStatus)
  170. INIT_ATOM(XdndDrop)
  171. INIT_ATOM(XdndFinished)
  172. INIT_ATOM(XdndActionCopy)
  173. INIT_ATOM(XdndTypeList)
  174. INIT_ATOM(PRIMARY)
  175. #undef INIT_ATOM
  176. }
  177. void LinuxDragAndDrop::shutDown()
  178. {
  179. // Do nothing
  180. }
  181. void LinuxDragAndDrop::makeDNDAware(::Window xWindow)
  182. {
  183. UINT32 dndVersion = 5;
  184. XChangeProperty(sXDisplay, xWindow, sXdndAware, XA_ATOM, 32, PropModeReplace, (unsigned char*)&dndVersion, 1);
  185. }
  186. OSDropTarget& LinuxDragAndDrop::createDropTarget(const RenderWindow* window, INT32 x, INT32 y, UINT32 width,
  187. UINT32 height)
  188. {
  189. Lock lock(sMutex);
  190. OSDropTarget* newDropTarget = bs_new<OSDropTarget>(window, x, y, width, height);
  191. sTargetsToRegister.push_back(newDropTarget);
  192. return *newDropTarget;
  193. }
  194. void LinuxDragAndDrop::destroyDropTarget(OSDropTarget& target)
  195. {
  196. Lock lock(sMutex);
  197. sTargetsToUnregister.push_back(&target);
  198. }
  199. bool LinuxDragAndDrop::handleClientMessage(XClientMessageEvent& event)
  200. {
  201. // First handle any queued registration/unregistration
  202. {
  203. Lock lock(sMutex);
  204. for(auto& target : sTargetsToRegister)
  205. sTargets.push_back(target);
  206. for(auto& target : sTargetsToUnregister)
  207. {
  208. // Remove any operations queued for this target
  209. for(auto iter = sQueuedOperations.begin(); iter !=sQueuedOperations.end();)
  210. {
  211. if(iter->target == target)
  212. iter = sQueuedOperations.erase(iter);
  213. else
  214. ++iter;
  215. }
  216. auto iterFind = std::find(sTargets.begin(), sTargets.end(), target);
  217. if (iterFind != sTargets.end())
  218. sTargets.erase(iterFind);
  219. bs_delete(target);
  220. }
  221. }
  222. // Source window notifies us a drag has just entered our window area
  223. if(event.message_type == sXdndEnter)
  224. {
  225. sDNDSource = (::Window)event.data.l[0];
  226. bool isList = (event.data.l[1] & 1) != 0;
  227. sDNDVersion = (INT32)(event.data.l[1] >> 24);
  228. // Get a list of properties are determine if there are any relevant ones
  229. Atom* propertyList = nullptr;
  230. UINT32 numProperties = 0;
  231. // If more than 3 properties we need to read the list property to get them all
  232. if(isList)
  233. {
  234. X11Property property = readX11Property(sXDisplay, sDNDSource, sXdndTypeList);
  235. propertyList = (Atom*)property.data;
  236. numProperties = property.count;
  237. }
  238. else
  239. {
  240. propertyList = bs_stack_alloc<Atom>(3);
  241. for(int i = 0; i < 3; i++)
  242. {
  243. if(event.data.l[2 + i])
  244. {
  245. propertyList[i] = (Atom)event.data.l[2 + i];
  246. numProperties++;
  247. }
  248. }
  249. }
  250. // Scan the list for URI list (file list), which is the only one we support (currently)
  251. bool foundSupportedType = false;
  252. for (UINT32 i = 0; i < numProperties; ++i)
  253. {
  254. char* name = XGetAtomName(sXDisplay, propertyList[i]);
  255. if(strcmp(name, "text/uri-list") == 0)
  256. {
  257. sDNDType = propertyList[i];
  258. XFree(name);
  259. foundSupportedType = true;
  260. break;
  261. }
  262. XFree(name);
  263. }
  264. // Free the property list
  265. if(isList)
  266. XFree(propertyList);
  267. else
  268. bs_stack_free(propertyList);
  269. sDragActive = foundSupportedType;
  270. }
  271. // Cursor moved while drag is active (also includes the initial cursor activity when drag entered)
  272. else if(event.message_type == sXdndPosition)
  273. {
  274. ::Window source = (::Window)event.data.l[0];
  275. sDragPosition.x = (INT32)((event.data.l[2] >> 16) & 0xFFFF);
  276. sDragPosition.y = (INT32)((event.data.l[2]) & 0xFFFF);
  277. // Respond with a status message, we either accept or reject the dnd
  278. XClientMessageEvent response;
  279. bs_zero_out(response);
  280. response.type = ClientMessage;
  281. response.display = event.display;
  282. response.window = source;
  283. response.message_type = sXdndStatus;
  284. response.format = 32;
  285. response.data.l[0] = event.window;
  286. response.data.l[1] = 0; // Reject drop by default
  287. response.data.l[2] = 0; // Empty rectangle
  288. response.data.l[3] = 0; // Empty rectangle
  289. response.data.l[4] = sXdndActionCopy;
  290. if(sDragActive)
  291. {
  292. for(auto& dropTarget : sTargets)
  293. {
  294. LinuxWindow* linuxWindow;
  295. dropTarget->_getOwnerWindow()->getCustomAttribute("WINDOW", &linuxWindow);
  296. ::Window xWindow = linuxWindow->_getXWindow();
  297. if(xWindow == event.window)
  298. {
  299. Vector2I windowPos = linuxWindow->screenToWindowPos(sDragPosition);
  300. if(dropTarget->_isInside(windowPos))
  301. {
  302. // Accept drop
  303. response.data.l[1] = 1;
  304. if(dropTarget->_isActive())
  305. {
  306. Lock lock(sMutex);
  307. sQueuedOperations.push_back(DragAndDropOp(DragAndDropOpType::DragOver, dropTarget,
  308. windowPos));
  309. }
  310. else
  311. {
  312. Lock lock(sMutex);
  313. sQueuedOperations.push_back(DragAndDropOp(DragAndDropOpType::Enter, dropTarget,
  314. windowPos));
  315. }
  316. dropTarget->_setActive(true);
  317. }
  318. else
  319. {
  320. // Cursor left previously active target's area
  321. if(dropTarget->_isActive())
  322. {
  323. {
  324. Lock lock(sMutex);
  325. sQueuedOperations.push_back(DragAndDropOp(DragAndDropOpType::Leave, dropTarget));
  326. }
  327. dropTarget->_setActive(false);
  328. }
  329. }
  330. }
  331. }
  332. }
  333. XSendEvent(sXDisplay, source, False, NoEventMask, (XEvent*)&response);
  334. XFlush(sXDisplay);
  335. }
  336. // Cursor left the target window, or the drop was rejected
  337. else if(event.message_type == sXdndLeave)
  338. {
  339. for(auto& dropTarget : sTargets)
  340. {
  341. if(dropTarget->_isActive())
  342. {
  343. {
  344. Lock lock(sMutex);
  345. sQueuedOperations.push_back(DragAndDropOp(DragAndDropOpType::Leave, dropTarget));
  346. }
  347. dropTarget->_setActive(false);
  348. }
  349. }
  350. sDragActive = false;
  351. }
  352. else if(event.message_type == sXdndDrop)
  353. {
  354. ::Window source = (::Window)event.data.l[0];
  355. bool dropAccepted = false;
  356. if(sDragActive)
  357. {
  358. for (auto& dropTarget : sTargets)
  359. {
  360. if (dropTarget->_isActive())
  361. dropAccepted = true;
  362. }
  363. }
  364. if(dropAccepted)
  365. {
  366. Time timestamp;
  367. if(sDNDVersion >= 1)
  368. timestamp = (Time)event.data.l[2];
  369. else
  370. timestamp = CurrentTime;
  371. XConvertSelection(sXDisplay, sXdndSelection, sDNDType, sPRIMARY, LinuxPlatform::getMainXWindow(), timestamp);
  372. // Now we wait for SelectionNotify
  373. }
  374. else
  375. {
  376. // Respond with a status message that we reject the drop
  377. XClientMessageEvent response;
  378. bs_zero_out(response);
  379. response.type = ClientMessage;
  380. response.display = event.display;
  381. response.window = source;
  382. response.message_type = sXdndFinished;
  383. response.format = 32;
  384. response.data.l[0] = LinuxPlatform::getMainXWindow();
  385. response.data.l[1] = 0;
  386. response.data.l[2] = 0;
  387. response.data.l[3] = None;
  388. response.data.l[4] = None;
  389. XSendEvent(sXDisplay, source, False, NoEventMask, (XEvent*)&response);
  390. XFlush(sXDisplay);
  391. sDragActive = false;
  392. }
  393. }
  394. else
  395. return false;
  396. return true;
  397. }
  398. bool LinuxDragAndDrop::handleSelectionNotify(XSelectionEvent& event)
  399. {
  400. if(event.target != sDNDType)
  401. return false;
  402. if(!sDragActive)
  403. return true;
  404. // Read data
  405. X11Property property = readX11Property(sXDisplay, LinuxPlatform::getMainXWindow(), sXdndTypeList);
  406. if(property.format == 8)
  407. {
  408. // Assuming this is a file list, since we rejected any other drop type
  409. Vector<WString> filePaths;
  410. char* token = strtok((char*)property.data, "\r\n");
  411. while(token != nullptr)
  412. {
  413. char* filePath = convertURIToLocalPath(token);
  414. if(filePath != nullptr)
  415. filePaths.push_back(UTF8::toWide(filePath));
  416. token = strtok(nullptr, "\r\n");
  417. }
  418. for(auto& target : sTargets)
  419. {
  420. if(!target->_isActive())
  421. continue;
  422. LinuxWindow* linuxWindow;
  423. target->_getOwnerWindow()->getCustomAttribute("WINDOW", &linuxWindow);
  424. Vector2I windowPos = linuxWindow->screenToWindowPos(sDragPosition);
  425. Lock lock(sMutex);
  426. sQueuedOperations.push_back(DragAndDropOp(DragAndDropOpType::Drop, target, windowPos, filePaths));
  427. target->_setActive(false);
  428. }
  429. }
  430. XFree(property.data);
  431. // Respond with a status message that we accepted the drop
  432. XClientMessageEvent response;
  433. bs_zero_out(response);
  434. response.type = ClientMessage;
  435. response.display = event.display;
  436. response.window = sDNDSource;
  437. response.message_type = sXdndFinished;
  438. response.format = 32;
  439. response.data.l[0] = LinuxPlatform::getMainXWindow();
  440. response.data.l[1] = 1;
  441. response.data.l[2] = sXdndActionCopy;
  442. response.data.l[3] = None;
  443. response.data.l[4] = None;
  444. XSendEvent(sXDisplay, sDNDSource, False, NoEventMask, (XEvent*)&response);
  445. XFlush(sXDisplay);
  446. sDragActive = false;
  447. return true;
  448. }
  449. void LinuxDragAndDrop::update()
  450. {
  451. Vector<DragAndDropOp> operations;
  452. {
  453. Lock lock(sMutex);
  454. std::swap(operations, sQueuedOperations);
  455. }
  456. for(auto& op : operations)
  457. {
  458. switch(op.type)
  459. {
  460. case DragAndDropOpType::Enter:
  461. op.target->onEnter(op.position.x, op.position.y);
  462. break;
  463. case DragAndDropOpType::DragOver:
  464. op.target->onDragOver(op.position.x, op.position.y);
  465. break;
  466. case DragAndDropOpType::Drop:
  467. op.target->_setFileList(op.fileList);
  468. op.target->onDrop(op.position.x, op.position.y);
  469. break;
  470. case DragAndDropOpType::Leave:
  471. op.target->_clear();
  472. op.target->onLeave();
  473. break;
  474. }
  475. }
  476. }
  477. }