dragAndDrop.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. // dragAndDrop.js
  2. // GPLv3
  3. const DND = imports.ui.dnd;
  4. const AppDisplay = imports.ui.appDisplay;
  5. const Clutter = imports.gi.Clutter;
  6. const St = imports.gi.St;
  7. const Main = imports.ui.main;
  8. const Mainloop = imports.mainloop;
  9. const ExtensionUtils = imports.misc.extensionUtils;
  10. const Me = ExtensionUtils.getCurrentExtension();
  11. const Convenience = Me.imports.convenience;
  12. const Extension = Me.imports.extension;
  13. const CHANGE_PAGE_TIMEOUT = 400;
  14. const Gettext = imports.gettext.domain('appfolders-manager');
  15. const _ = Gettext.gettext;
  16. //-------------------------------------------------
  17. var OVERLAY_MANAGER;
  18. /* This method is called by extension.js' enable function. It does code injections
  19. * to AppDisplay.AppIcon, connecting it to DND-related signals.
  20. */
  21. function initDND () {
  22. OVERLAY_MANAGER = new OverlayManager();
  23. }
  24. //--------------------------------------------------------------
  25. /* Amazing! A singleton! It allows easy (and safer?) access to general methods,
  26. * managing other objects: it creates/updates/deletes all overlays (for folders,
  27. * pages, creation, removing).
  28. */
  29. class OverlayManager {
  30. constructor () {
  31. this.addActions = [];
  32. this.removeAction = new FolderActionArea('remove');
  33. this.createAction = new FolderActionArea('create');
  34. this.upAction = new NavigationArea('up');
  35. this.downAction = new NavigationArea('down');
  36. this.next_drag_should_recompute = true;
  37. this.current_width = 0;
  38. }
  39. on_drag_begin () {
  40. this.ensurePopdowned();
  41. this.ensureFolderOverlayActors();
  42. this.updateFoldersVisibility();
  43. this.updateState(true);
  44. }
  45. on_drag_end () {
  46. // force to compute new positions if a drop occurs
  47. this.next_drag_should_recompute = true;
  48. this.updateState(false);
  49. }
  50. on_drag_cancelled () {
  51. this.updateState(false);
  52. }
  53. updateArrowVisibility () {
  54. let grid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
  55. if (grid.currentPage == 0) {
  56. this.upAction.setActive(false);
  57. } else {
  58. this.upAction.setActive(true);
  59. }
  60. if (grid.currentPage == grid._nPages -1) {
  61. this.downAction.setActive(false);
  62. } else {
  63. this.downAction.setActive(true);
  64. }
  65. this.upAction.show();
  66. this.downAction.show();
  67. }
  68. updateState (isDragging) {
  69. if (isDragging) {
  70. this.removeAction.show();
  71. if (this.openedFolder == null) {
  72. this.removeAction.setActive(false);
  73. } else {
  74. this.removeAction.setActive(true);
  75. }
  76. this.createAction.show();
  77. this.updateArrowVisibility();
  78. } else {
  79. this.hideAll();
  80. }
  81. }
  82. hideAll () {
  83. this.removeAction.hide();
  84. this.createAction.hide();
  85. this.upAction.hide();
  86. this.downAction.hide();
  87. this.hideAllFolders();
  88. }
  89. hideAllFolders () {
  90. for (var i = 0; i < this.addActions.length; i++) {
  91. this.addActions[i].hide();
  92. }
  93. }
  94. updateActorsPositions () {
  95. let monitor = Main.layoutManager.primaryMonitor;
  96. this.topOfTheGrid = Main.overview.viewSelector.actor.get_parent().get_parent().get_allocation_box().y1;
  97. let temp = Main.overview.viewSelector.appDisplay._views[1].view.actor.get_parent();
  98. let bottomOfTheGrid = this.topOfTheGrid + temp.get_allocation_box().y2;
  99. let _availHeight = bottomOfTheGrid - this.topOfTheGrid;
  100. let _availWidth = Main.overview.viewSelector.appDisplay._views[1].view._grid.actor.width;
  101. let sideMargin = (monitor.width - _availWidth) / 2;
  102. let xMiddle = ( monitor.x + monitor.width ) / 2;
  103. let yMiddle = ( monitor.y + monitor.height ) / 2;
  104. // Positions of areas
  105. this.removeAction.setPosition( xMiddle , bottomOfTheGrid );
  106. this.createAction.setPosition( xMiddle, Main.overview._panelGhost.height );
  107. this.upAction.setPosition( 0, Main.overview._panelGhost.height );
  108. this.downAction.setPosition( 0, bottomOfTheGrid );
  109. // Sizes of areas
  110. this.removeAction.setSize(xMiddle, monitor.height - bottomOfTheGrid);
  111. this.createAction.setSize(xMiddle, this.topOfTheGrid - Main.overview._panelGhost.height);
  112. this.upAction.setSize(xMiddle, this.topOfTheGrid - Main.overview._panelGhost.height);
  113. this.downAction.setSize(xMiddle, monitor.height - bottomOfTheGrid);
  114. this.updateArrowVisibility();
  115. }
  116. ensureFolderOverlayActors () {
  117. // A folder was opened, and just closed.
  118. if (this.openedFolder != null) {
  119. this.updateActorsPositions();
  120. this.computeFolderOverlayActors();
  121. this.next_drag_should_recompute = true;
  122. return;
  123. }
  124. // The grid "moved" or the whole shit needs forced updating
  125. let allAppsGrid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
  126. let new_width = allAppsGrid.actor.allocation.get_width();
  127. if (new_width != this.current_width || this.next_drag_should_recompute) {
  128. this.next_drag_should_recompute = false;
  129. this.updateActorsPositions();
  130. this.computeFolderOverlayActors();
  131. }
  132. }
  133. computeFolderOverlayActors () {
  134. let monitor = Main.layoutManager.primaryMonitor;
  135. let xMiddle = ( monitor.x + monitor.width ) / 2;
  136. let yMiddle = ( monitor.y + monitor.height ) / 2;
  137. let allAppsGrid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
  138. let nItems = 0;
  139. let indexes = [];
  140. let folders = [];
  141. let x, y;
  142. Main.overview.viewSelector.appDisplay._views[1].view._allItems.forEach(function(icon) {
  143. if (icon.actor.visible) {
  144. if (icon instanceof AppDisplay.FolderIcon) {
  145. indexes.push(nItems);
  146. folders.push(icon);
  147. }
  148. nItems++;
  149. }
  150. });
  151. this.current_width = allAppsGrid.actor.allocation.get_width();
  152. let x_correction = (monitor.width - this.current_width)/2;
  153. let availHeightPerPage = (allAppsGrid.actor.height)/(allAppsGrid._nPages);
  154. for (var i = 0; i < this.addActions.length; i++) {
  155. this.addActions[i].actor.destroy();
  156. }
  157. for (var i = 0; i < indexes.length; i++) {
  158. let inPageIndex = indexes[i] % allAppsGrid._childrenPerPage;
  159. let page = Math.floor(indexes[i] / allAppsGrid._childrenPerPage);
  160. x = folders[i].actor.get_allocation_box().x1;
  161. y = folders[i].actor.get_allocation_box().y1;
  162. // Invalid coords (example: when dragging out of the folder) should
  163. // not produce a visible overlay, a negative page number is an easy
  164. // way to be sure it stays hidden.
  165. if (x == 0) {
  166. page = -1;
  167. }
  168. x = Math.floor(x + x_correction);
  169. y = y + this.topOfTheGrid;
  170. y = y - (page * availHeightPerPage);
  171. this.addActions[i] = new FolderArea(folders[i].id, x, y, page);
  172. }
  173. }
  174. updateFoldersVisibility () {
  175. let appView = Main.overview.viewSelector.appDisplay._views[1].view;
  176. for (var i = 0; i < this.addActions.length; i++) {
  177. if ((this.addActions[i].page == appView._grid.currentPage) && (!appView._currentPopup)) {
  178. this.addActions[i].show();
  179. } else {
  180. this.addActions[i].hide();
  181. }
  182. }
  183. }
  184. ensurePopdowned () {
  185. let appView = Main.overview.viewSelector.appDisplay._views[1].view;
  186. if (appView._currentPopup) {
  187. this.openedFolder = appView._currentPopup._source.id;
  188. appView._currentPopup.popdown();
  189. } else {
  190. this.openedFolder = null;
  191. }
  192. }
  193. goToPage (nb) {
  194. Main.overview.viewSelector.appDisplay._views[1].view.goToPage( nb );
  195. this.updateArrowVisibility();
  196. this.hideAllFolders();
  197. this.updateFoldersVisibility(); //load folders of the new page
  198. }
  199. destroy () {
  200. for (let i = 0; i > this.addActions.length; i++) {
  201. this.addActions[i].destroy();
  202. }
  203. this.removeAction.destroy();
  204. this.createAction.destroy();
  205. this.upAction.destroy();
  206. this.downAction.destroy();
  207. //log('OverlayManager destroyed');
  208. }
  209. };
  210. //-------------------------------------------------------
  211. // Abstract overlay with very generic methods
  212. class DroppableArea {
  213. constructor (id) {
  214. this.id = id;
  215. this.styleClass = 'folderArea';
  216. this.actor = new St.BoxLayout ({
  217. width: 10,
  218. height: 10,
  219. visible: false,
  220. });
  221. this.actor._delegate = this;
  222. this.lock = true;
  223. this.use_frame = Convenience.getSettings('org.gnome.shell.extensions.appfolders-manager').get_boolean('debug');
  224. }
  225. setPosition (x, y) {
  226. let monitor = Main.layoutManager.primaryMonitor;
  227. this.actor.set_position(monitor.x + x, monitor.y + y);
  228. }
  229. setSize (w, h) {
  230. this.actor.width = w;
  231. this.actor.height = h;
  232. }
  233. hide () {
  234. this.actor.visible = false;
  235. this.lock = true;
  236. }
  237. show () {
  238. this.actor.visible = true;
  239. }
  240. setActive (active) {
  241. this._active = active;
  242. if (this._active) {
  243. this.actor.style_class = this.styleClass;
  244. } else {
  245. this.actor.style_class = 'insensitiveArea';
  246. }
  247. }
  248. destroy () {
  249. this.actor.destroy();
  250. }
  251. }
  252. /* Overlay representing an "action". Actions can be creating a folder, or
  253. * removing an app from a folder. These areas accept drop, and display a label.
  254. */
  255. class FolderActionArea extends DroppableArea {
  256. constructor (id) {
  257. super(id);
  258. let x, y, label;
  259. switch (this.id) {
  260. case 'create':
  261. label = _("Create a new folder");
  262. this.styleClass = 'shadowedAreaTop';
  263. break;
  264. case 'remove':
  265. label = '';
  266. this.styleClass = 'shadowedAreaBottom';
  267. break;
  268. default:
  269. label = 'invalid id';
  270. break;
  271. }
  272. if (this.use_frame) {
  273. this.styleClass = 'framedArea';
  274. }
  275. this.actor.style_class = this.styleClass;
  276. this.label = new St.Label({
  277. text: label,
  278. style_class: 'dropAreaLabel',
  279. x_expand: true,
  280. y_expand: true,
  281. x_align: Clutter.ActorAlign.CENTER,
  282. y_align: Clutter.ActorAlign.CENTER,
  283. });
  284. this.actor.add(this.label);
  285. this.setPosition(10, 10);
  286. Main.layoutManager.overviewGroup.add_actor(this.actor);
  287. }
  288. getRemoveLabel () {
  289. let label;
  290. if (OVERLAY_MANAGER.openedFolder == null) {
  291. label = '…';
  292. } else {
  293. let folder_schema = Extension.folderSchema (OVERLAY_MANAGER.openedFolder);
  294. label = folder_schema.get_string('name');
  295. }
  296. return (_("Remove from %s")).replace('%s', label);
  297. }
  298. setActive (active) {
  299. super.setActive(active);
  300. if (this.id == 'remove') {
  301. this.label.text = this.getRemoveLabel();
  302. }
  303. }
  304. handleDragOver (source, actor, x, y, time) {
  305. if (source instanceof AppDisplay.AppIcon && this._active) {
  306. return DND.DragMotionResult.MOVE_DROP;
  307. }
  308. Main.overview.endItemDrag(this);
  309. return DND.DragMotionResult.NO_DROP;
  310. }
  311. acceptDrop (source, actor, x, y, time) {
  312. if ((source instanceof AppDisplay.AppIcon) && (this.id == 'create')) {
  313. Extension.createNewFolder(source);
  314. Main.overview.endItemDrag(this);
  315. return true;
  316. }
  317. if ((source instanceof AppDisplay.AppIcon) && (this.id == 'remove')) {
  318. this.removeApp(source);
  319. Main.overview.endItemDrag(this);
  320. return true;
  321. }
  322. Main.overview.endItemDrag(this);
  323. return false;
  324. }
  325. removeApp (source) {
  326. let id = source.app.get_id();
  327. Extension.removeFromFolder(id, OVERLAY_MANAGER.openedFolder);
  328. OVERLAY_MANAGER.updateState(false);
  329. Main.overview.viewSelector.appDisplay._views[1].view._redisplay();
  330. }
  331. destroy () {
  332. this.label.destroy();
  333. super.destroy();
  334. }
  335. };
  336. /* Overlay reacting to hover, but isn't droppable. The goal is to go to an other
  337. * page of the grid while dragging an app.
  338. */
  339. class NavigationArea extends DroppableArea {
  340. constructor (id) {
  341. super(id);
  342. let x, y, i;
  343. switch (this.id) {
  344. case 'up':
  345. i = 'pan-up-symbolic';
  346. this.styleClass = 'shadowedAreaTop';
  347. break;
  348. case 'down':
  349. i = 'pan-down-symbolic';
  350. this.styleClass = 'shadowedAreaBottom';
  351. break;
  352. default:
  353. i = 'dialog-error-symbolic';
  354. break;
  355. }
  356. if (this.use_frame) {
  357. this.styleClass = 'framedArea';
  358. }
  359. this.actor.style_class = this.styleClass;
  360. this.actor.add(new St.Icon({
  361. icon_name: i,
  362. icon_size: 24,
  363. style_class: 'system-status-icon',
  364. x_expand: true,
  365. y_expand: true,
  366. x_align: Clutter.ActorAlign.CENTER,
  367. y_align: Clutter.ActorAlign.CENTER,
  368. }));
  369. this.setPosition(x, y);
  370. Main.layoutManager.overviewGroup.add_actor(this.actor);
  371. }
  372. handleDragOver (source, actor, x, y, time) {
  373. if (this.id == 'up' && this._active) {
  374. this.pageUp();
  375. return DND.DragMotionResult.CONTINUE;
  376. }
  377. if (this.id == 'down' && this._active) {
  378. this.pageDown();
  379. return DND.DragMotionResult.CONTINUE;
  380. }
  381. Main.overview.endItemDrag(this);
  382. return DND.DragMotionResult.NO_DROP;
  383. }
  384. pageUp () {
  385. if(this.lock && !this.timeoutSet) {
  386. this._timeoutId = Mainloop.timeout_add(CHANGE_PAGE_TIMEOUT, this.unlock.bind(this));
  387. this.timeoutSet = true;
  388. }
  389. if(!this.lock) {
  390. let currentPage = Main.overview.viewSelector.appDisplay._views[1].view._grid.currentPage;
  391. this.lock = true;
  392. OVERLAY_MANAGER.goToPage(currentPage - 1);
  393. }
  394. }
  395. pageDown () {
  396. if(this.lock && !this.timeoutSet) {
  397. this._timeoutId = Mainloop.timeout_add(CHANGE_PAGE_TIMEOUT, this.unlock.bind(this));
  398. this.timeoutSet = true;
  399. }
  400. if(!this.lock) {
  401. let currentPage = Main.overview.viewSelector.appDisplay._views[1].view._grid.currentPage;
  402. this.lock = true;
  403. OVERLAY_MANAGER.goToPage(currentPage + 1);
  404. }
  405. }
  406. acceptDrop (source, actor, x, y, time) {
  407. Main.overview.endItemDrag(this);
  408. return false;
  409. }
  410. unlock () {
  411. this.lock = false;
  412. this.timeoutSet = false;
  413. Mainloop.source_remove(this._timeoutId);
  414. }
  415. };
  416. /* This overlay is the area upon a folder. Position and visibility of the actor
  417. * is handled by exterior functions.
  418. * "this.id" is the folder's id, a string, as written in the gsettings key.
  419. * Dropping an app on this folder will add it to the folder
  420. */
  421. class FolderArea extends DroppableArea {
  422. constructor (id, asked_x, asked_y, page) {
  423. super(id);
  424. this.page = page;
  425. let grid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
  426. this.actor.width = grid._getHItemSize();
  427. this.actor.height = grid._getVItemSize();
  428. if (this.use_frame) {
  429. this.styleClass = 'framedArea';
  430. this.actor.add(new St.Label({
  431. text: this.id,
  432. x_expand: true,
  433. y_expand: true,
  434. x_align: Clutter.ActorAlign.CENTER,
  435. y_align: Clutter.ActorAlign.CENTER,
  436. }));
  437. } else {
  438. this.styleClass = 'folderArea';
  439. this.actor.add(new St.Icon({
  440. icon_name: 'list-add-symbolic',
  441. icon_size: 24,
  442. style_class: 'system-status-icon',
  443. x_expand: true,
  444. y_expand: true,
  445. x_align: Clutter.ActorAlign.CENTER,
  446. y_align: Clutter.ActorAlign.CENTER,
  447. }));
  448. }
  449. if (this.use_frame) {
  450. this.styleClass = 'framedArea';
  451. }
  452. this.actor.style_class = this.styleClass;
  453. this.setPosition(asked_x, asked_y);
  454. Main.layoutManager.overviewGroup.add_actor(this.actor);
  455. }
  456. handleDragOver (source, actor, x, y, time) {
  457. if (source instanceof AppDisplay.AppIcon) {
  458. return DND.DragMotionResult.MOVE_DROP;
  459. }
  460. Main.overview.endItemDrag(this);
  461. return DND.DragMotionResult.NO_DROP;
  462. }
  463. acceptDrop (source, actor, x, y, time) { //FIXME recharger la vue ou au minimum les miniatures des dossiers
  464. if ((source instanceof AppDisplay.AppIcon) &&
  465. !Extension.isInFolder(source.id, this.id)) {
  466. Extension.addToFolder(source, this.id);
  467. Main.overview.endItemDrag(this);
  468. return true;
  469. }
  470. Main.overview.endItemDrag(this);
  471. return false;
  472. }
  473. };