desktopGrid.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. /* Desktop Icons GNOME Shell extension
  2. *
  3. * Copyright (C) 2017 Carlos Soriano <csoriano@redhat.com>
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. const Gtk = imports.gi.Gtk;
  19. const Clutter = imports.gi.Clutter;
  20. const St = imports.gi.St;
  21. const Gio = imports.gi.Gio;
  22. const GLib = imports.gi.GLib;
  23. const GObject = imports.gi.GObject;
  24. const Shell = imports.gi.Shell;
  25. const Layout = imports.ui.layout;
  26. const Main = imports.ui.main;
  27. const BoxPointer = imports.ui.boxpointer;
  28. const PopupMenu = imports.ui.popupMenu;
  29. const GrabHelper = imports.ui.grabHelper;
  30. const Config = imports.misc.config;
  31. const ExtensionUtils = imports.misc.extensionUtils;
  32. const Me = ExtensionUtils.getCurrentExtension();
  33. const CreateFolderDialog = Me.imports.createFolderDialog;
  34. const Extension = Me.imports.extension;
  35. const FileItem = Me.imports.fileItem;
  36. const Prefs = Me.imports.prefs;
  37. const DBusUtils = Me.imports.dbusUtils;
  38. const DesktopIconsUtil = Me.imports.desktopIconsUtil;
  39. const Util = imports.misc.util;
  40. const Clipboard = St.Clipboard.get_default();
  41. const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD;
  42. const Gettext = imports.gettext.domain('desktop-icons');
  43. const _ = Gettext.gettext;
  44. /* From NautilusFileUndoManagerState */
  45. var UndoStatus = {
  46. NONE: 0,
  47. UNDO: 1,
  48. REDO: 2,
  49. };
  50. var StoredCoordinates = {
  51. PRESERVE: 0,
  52. OVERWRITE:1,
  53. ASSIGN:2,
  54. };
  55. var Placeholder = GObject.registerClass({
  56. GTypeName: 'DesktopIcons_Placeholder',
  57. }, class Placeholder extends St.Bin {
  58. });
  59. var DesktopGrid = GObject.registerClass({
  60. GTypeName: 'DesktopIcons_DesktopGrid',
  61. }, class DesktopGrid extends St.Widget {
  62. _init(bgManager) {
  63. this._bgManager = bgManager;
  64. this._fileItemHandlers = new Map();
  65. this._fileItems = [];
  66. this.layout = new Clutter.GridLayout({
  67. orientation: Clutter.Orientation.VERTICAL,
  68. column_homogeneous: true,
  69. row_homogeneous: true
  70. });
  71. this._actorLayout = new Clutter.BinLayout({
  72. x_align: Clutter.BinAlignment.FIXED,
  73. y_align: Clutter.BinAlignment.FIXED
  74. });
  75. super._init({
  76. layout_manager: this._actorLayout
  77. });
  78. this._delegate = this;
  79. this.connect('style-changed', () => {
  80. if (this.isFirst)
  81. Extension.desktopManager.scheduleReLayoutChildren();
  82. });
  83. this._grid = new St.Widget({
  84. name: 'DesktopGrid',
  85. layout_manager: this.layout,
  86. reactive: true,
  87. x_expand: true,
  88. y_expand: true,
  89. can_focus: true,
  90. opacity: 255
  91. });
  92. this.add_child(this._grid);
  93. this._menuManager = new PopupMenu.PopupMenuManager(this);
  94. this._bgManager._container.add_child(this);
  95. let monitorIndex = bgManager._monitorIndex;
  96. this._monitorConstraint = new Layout.MonitorConstraint({
  97. index: monitorIndex,
  98. work_area: true
  99. });
  100. this.add_constraint(this._monitorConstraint);
  101. this._addDesktopBackgroundMenu();
  102. this._bgDestroyedId = bgManager.backgroundActor.connect('destroy',
  103. () => this._backgroundDestroyed());
  104. this._grid.connect('button-press-event', (actor, event) => this._onPressButton(actor, event));
  105. this._grid.connect('key-press-event', this._onKeyPress.bind(this));
  106. this._grid.connect('notify::allocation', () => Extension.desktopManager.scheduleReLayoutChildren());
  107. let themeNodeBase = this.get_theme_node();
  108. let themeContext = St.ThemeContext.get_for_stage(global.stage);
  109. let themeNode = St.ThemeNode.new(themeContext, null,
  110. themeNodeBase.get_theme(), themeNodeBase.get_element_type(),
  111. null, "file-item", null, "fake-id {}");
  112. this._extra_width = themeNode.get_margin(St.Side.LEFT) +
  113. themeNode.get_margin(St.Side.RIGHT) +
  114. themeNode.get_border_width(St.Side.LEFT) +
  115. themeNode.get_border_width(St.Side.RIGHT) +
  116. themeNode.get_horizontal_padding();
  117. this._extra_height = themeNode.get_margin(St.Side.TOP) +
  118. themeNode.get_margin(St.Side.BOTTOM) +
  119. themeNode.get_border_width(St.Side.TOP) +
  120. themeNode.get_border_width(St.Side.BOTTOM) +
  121. themeNode.get_vertical_padding();
  122. this.connect('destroy', () => this._onDestroy());
  123. }
  124. _onKeyPress(actor, event) {
  125. if (global.stage.get_key_focus() != actor)
  126. return Clutter.EVENT_PROPAGATE;
  127. let symbol = event.get_key_symbol();
  128. let isCtrl = (event.get_state() & Clutter.ModifierType.CONTROL_MASK) != 0;
  129. let isShift = (event.get_state() & Clutter.ModifierType.SHIFT_MASK) != 0;
  130. if (isCtrl && isShift && [Clutter.KEY_Z, Clutter.KEY_z].indexOf(symbol) > -1) {
  131. this._doRedo();
  132. return Clutter.EVENT_STOP;
  133. }
  134. else if (isCtrl && [Clutter.KEY_Z, Clutter.KEY_z].indexOf(symbol) > -1) {
  135. this._doUndo();
  136. return Clutter.EVENT_STOP;
  137. }
  138. else if (isCtrl && [Clutter.KEY_C, Clutter.KEY_c].indexOf(symbol) > -1) {
  139. Extension.desktopManager.doCopy();
  140. return Clutter.EVENT_STOP;
  141. }
  142. else if (isCtrl && [Clutter.KEY_X, Clutter.KEY_x].indexOf(symbol) > -1) {
  143. Extension.desktopManager.doCut();
  144. return Clutter.EVENT_STOP;
  145. }
  146. else if (isCtrl && [Clutter.KEY_V, Clutter.KEY_v].indexOf(symbol) > -1) {
  147. this._doPaste();
  148. return Clutter.EVENT_STOP;
  149. }
  150. else if (symbol == Clutter.KEY_Return) {
  151. Extension.desktopManager.doOpen();
  152. return Clutter.EVENT_STOP;
  153. }
  154. else if (symbol == Clutter.KEY_Delete) {
  155. Extension.desktopManager.doTrash();
  156. return Clutter.EVENT_STOP;
  157. } else if (symbol == Clutter.KEY_F2) {
  158. // Support renaming other grids file items.
  159. Extension.desktopManager.doRename();
  160. return Clutter.EVENT_STOP;
  161. } else if (symbol == Clutter.KEY_space) {
  162. Extension.desktopManager.doPreview();
  163. return Clutter.EVENT_STOP;
  164. }
  165. return Clutter.EVENT_PROPAGATE;
  166. }
  167. _backgroundDestroyed() {
  168. this._bgDestroyedId = 0;
  169. if (this._bgManager == null)
  170. return;
  171. if (this._bgManager._backgroundSource) {
  172. this._bgDestroyedId = this._bgManager.backgroundActor.connect('destroy',
  173. () => this._backgroundDestroyed());
  174. } else {
  175. this.destroy();
  176. }
  177. }
  178. _onDestroy() {
  179. if (this._bgDestroyedId && this._bgManager.backgroundActor)
  180. this._bgManager.backgroundActor.disconnect(this._bgDestroyedId);
  181. this._bgDestroyedId = 0;
  182. this._bgManager = null;
  183. this._menuManager = null;
  184. }
  185. _onNewFolderClicked() {
  186. let dialog = new CreateFolderDialog.CreateFolderDialog();
  187. dialog.connect('response', (dialog, name) => {
  188. let dir = DesktopIconsUtil.getDesktopDir().get_child(name);
  189. DBusUtils.NautilusFileOperationsProxy.CreateFolderRemote(dir.get_uri(),
  190. (result, error) => {
  191. if (error)
  192. throw new Error('Error creating new folder: ' + error.message);
  193. }
  194. );
  195. });
  196. dialog.open();
  197. }
  198. _parseClipboardText(text) {
  199. if (text === null)
  200. return [false, false, null];
  201. let lines = text.split('\n');
  202. let [mime, action, ...files] = lines;
  203. if (mime != 'x-special/nautilus-clipboard')
  204. return [false, false, null];
  205. if (!(['copy', 'cut'].includes(action)))
  206. return [false, false, null];
  207. let isCut = action == 'cut';
  208. /* Last line is empty due to the split */
  209. if (files.length <= 1)
  210. return [false, false, null];
  211. /* Remove last line */
  212. files.pop();
  213. return [true, isCut, files];
  214. }
  215. _doPaste() {
  216. Clipboard.get_text(CLIPBOARD_TYPE,
  217. (clipboard, text) => {
  218. let [valid, is_cut, files] = this._parseClipboardText(text);
  219. if (!valid)
  220. return;
  221. let desktopDir = `${DesktopIconsUtil.getDesktopDir().get_uri()}`;
  222. if (is_cut) {
  223. DBusUtils.NautilusFileOperationsProxy.MoveURIsRemote(files, desktopDir,
  224. (result, error) => {
  225. if (error)
  226. throw new Error('Error moving files: ' + error.message);
  227. }
  228. );
  229. } else {
  230. DBusUtils.NautilusFileOperationsProxy.CopyURIsRemote(files, desktopDir,
  231. (result, error) => {
  232. if (error)
  233. throw new Error('Error copying files: ' + error.message);
  234. }
  235. );
  236. }
  237. }
  238. );
  239. }
  240. _onPasteClicked() {
  241. this._doPaste();
  242. }
  243. _doUndo() {
  244. DBusUtils.NautilusFileOperationsProxy.UndoRemote(
  245. (result, error) => {
  246. if (error)
  247. throw new Error('Error performing undo: ' + error.message);
  248. }
  249. );
  250. }
  251. _onUndoClicked() {
  252. this._doUndo();
  253. }
  254. _doRedo() {
  255. DBusUtils.NautilusFileOperationsProxy.RedoRemote(
  256. (result, error) => {
  257. if (error)
  258. throw new Error('Error performing redo: ' + error.message);
  259. }
  260. );
  261. }
  262. _onRedoClicked() {
  263. this._doRedo();
  264. }
  265. _onOpenDesktopInFilesClicked() {
  266. Gio.AppInfo.launch_default_for_uri_async(DesktopIconsUtil.getDesktopDir().get_uri(),
  267. null, null,
  268. (source, result) => {
  269. try {
  270. Gio.AppInfo.launch_default_for_uri_finish(result);
  271. } catch (e) {
  272. log('Error opening Desktop in Files: ' + e.message);
  273. }
  274. }
  275. );
  276. }
  277. _onOpenTerminalClicked() {
  278. let desktopPath = DesktopIconsUtil.getDesktopDir().get_path();
  279. DesktopIconsUtil.launchTerminal(desktopPath);
  280. }
  281. _syncUndoRedo() {
  282. this._undoMenuItem.visible = DBusUtils.NautilusFileOperationsProxy.UndoStatus == UndoStatus.UNDO;
  283. this._redoMenuItem.visible = DBusUtils.NautilusFileOperationsProxy.UndoStatus == UndoStatus.REDO;
  284. }
  285. _undoStatusChanged(proxy, properties, test) {
  286. if ('UndoStatus' in properties.deep_unpack())
  287. this._syncUndoRedo();
  288. }
  289. _createDesktopBackgroundMenu() {
  290. let menu = new PopupMenu.PopupMenu(Main.layoutManager.dummyCursor,
  291. 0, St.Side.TOP);
  292. menu.addAction(_("New Folder"), () => this._onNewFolderClicked());
  293. this._submenu = new PopupMenu.PopupSubMenuMenuItem(_("New Document"), false);
  294. menu.addMenuItem(this._submenu);
  295. menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
  296. this._pasteMenuItem = menu.addAction(_("Paste"), () => this._onPasteClicked());
  297. this._undoMenuItem = menu.addAction(_("Undo"), () => this._onUndoClicked());
  298. this._redoMenuItem = menu.addAction(_("Redo"), () => this._onRedoClicked());
  299. menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
  300. menu.addAction(_("Show Desktop in Files"), () => this._onOpenDesktopInFilesClicked());
  301. menu.addAction(_("Open in Terminal"), () => this._onOpenTerminalClicked());
  302. menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
  303. menu.addSettingsAction(_("Change Background…"), 'gnome-background-panel.desktop');
  304. menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
  305. menu.addSettingsAction(_("Display Settings"), 'gnome-display-panel.desktop');
  306. menu.addSettingsAction(_("Settings"), 'gnome-control-center.desktop');
  307. menu.actor.add_style_class_name('background-menu');
  308. Main.layoutManager.uiGroup.add_child(menu.actor);
  309. menu.actor.hide();
  310. menu._propertiesChangedId = DBusUtils.NautilusFileOperationsProxy.connect('g-properties-changed',
  311. this._undoStatusChanged.bind(this));
  312. this._syncUndoRedo();
  313. menu.connect('destroy',
  314. () => DBusUtils.NautilusFileOperationsProxy.disconnect(menu._propertiesChangedId));
  315. menu.connect('open-state-changed',
  316. (popupm, isOpen) => {
  317. if (isOpen) {
  318. Clipboard.get_text(CLIPBOARD_TYPE,
  319. (clipBoard, text) => {
  320. let [valid, is_cut, files] = this._parseClipboardText(text);
  321. this._pasteMenuItem.setSensitive(valid);
  322. }
  323. );
  324. }
  325. }
  326. );
  327. this._pasteMenuItem.setSensitive(false);
  328. return menu;
  329. }
  330. _openMenu(x, y) {
  331. Main.layoutManager.setDummyCursorGeometry(x, y, 0, 0);
  332. this._submenu.menu.removeAll();
  333. let templates = Extension.templateManager.getTemplates();
  334. if (templates.length == 0) {
  335. this._submenu.hide();
  336. } else {
  337. for(let template of templates) {
  338. this._submenu.menu.addAction(template["name"], () => {this._newDocument(template)}, template["icon"]);
  339. }
  340. this._submenu.show();
  341. }
  342. this._desktopBackgroundMenu.open(BoxPointer.PopupAnimation.NONE);
  343. /* Since the handler is in the press event it needs to ignore the release event
  344. * to not immediately close the menu on release
  345. */
  346. this._menuManager.ignoreRelease();
  347. }
  348. _newDocument(template) {
  349. let file = Extension.templateManager.getTemplateFile(template["file"]);
  350. if (file == null)
  351. return;
  352. let counter = 0;
  353. let finalName = `${template["name"]}${template["extension"]}`;
  354. let destination;
  355. do {
  356. if (counter != 0) {
  357. finalName = `${template["name"]} ${counter}${template["extension"]}`
  358. }
  359. destination = Gio.File.new_for_path(
  360. GLib.build_filenamev([GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP),
  361. finalName])
  362. );
  363. counter++;
  364. } while(destination.query_exists(null));
  365. file.copy_async(destination, Gio.FileCopyFlags.NONE, GLib.PRIORITY_DEFAULT, null, null, (source, result) => {
  366. try {
  367. source.copy_finish(result);
  368. } catch(e) {
  369. log(`Failed to create template ${e.message}`);
  370. }
  371. });
  372. }
  373. _addFileItemTo(fileItem, column, row, coordinatesAction) {
  374. let placeholder = this.layout.get_child_at(column, row);
  375. placeholder.child = fileItem;
  376. this._fileItems.push(fileItem);
  377. let fileItemHandler = {};
  378. fileItemHandler.selectedId = fileItem.connect('selected',
  379. this._onFileItemSelected.bind(this));
  380. fileItemHandler.renameId = fileItem.connect('rename-clicked',
  381. this._onRenameClicked.bind(this));
  382. fileItemHandler.destroyId = fileItem.connect('destroy', () => {
  383. this._fileItemHandlers.delete(fileItem);
  384. });
  385. this._fileItemHandlers.set(fileItem, fileItemHandler);
  386. /* If this file is new in the Desktop and hasn't yet
  387. * fixed coordinates, store the new possition to ensure
  388. * that the next time it will be shown in the same possition.
  389. * Also store the new possition if it has been moved by the user,
  390. * and not triggered by a screen change.
  391. */
  392. if ((fileItem.savedCoordinates == null) || (coordinatesAction == StoredCoordinates.OVERWRITE)) {
  393. let [fileX, fileY] = placeholder.get_transformed_position();
  394. fileItem.savedCoordinates = [Math.round(fileX), Math.round(fileY)];
  395. }
  396. }
  397. addFileItemCloseTo(fileItem, x, y, coordinatesAction) {
  398. let [column, row] = this._getEmptyPlaceClosestTo(x, y, coordinatesAction);
  399. fileItem.set_margins(this._extra_width, this._extra_height);
  400. this._addFileItemTo(fileItem, column, row, coordinatesAction);
  401. }
  402. _getEmptyPlaceClosestTo(x, y, coordinatesAction) {
  403. let [actorX, actorY] = this._grid.get_transformed_position();
  404. let actorWidth = this._grid.allocation.x2 - this._grid.allocation.x1;
  405. let actorHeight = this._grid.allocation.y2 - this._grid.allocation.y1;
  406. let placeX = Math.round((x - actorX) * this._columns / actorWidth);
  407. let placeY = Math.round((y - actorY) * this._rows / actorHeight);
  408. placeX = DesktopIconsUtil.clamp(placeX, 0, this._columns - 1);
  409. placeY = DesktopIconsUtil.clamp(placeY, 0, this._rows - 1);
  410. if (this.layout.get_child_at(placeX, placeY).child == null)
  411. return [placeX, placeY];
  412. let found = false;
  413. let resColumn = null;
  414. let resRow = null;
  415. let minDistance = Infinity;
  416. for (let column = 0; column < this._columns; column++) {
  417. for (let row = 0; row < this._rows; row++) {
  418. let placeholder = this.layout.get_child_at(column, row);
  419. if (placeholder.child != null)
  420. continue;
  421. let [proposedX, proposedY] = placeholder.get_transformed_position();
  422. if (coordinatesAction == StoredCoordinates.ASSIGN)
  423. return [column, row];
  424. let distance = DesktopIconsUtil.distanceBetweenPoints(proposedX, proposedY, x, y);
  425. if (distance < minDistance) {
  426. found = true;
  427. minDistance = distance;
  428. resColumn = column;
  429. resRow = row;
  430. }
  431. }
  432. }
  433. if (!found)
  434. throw new Error(`Not enough place at monitor ${this._bgManager._monitorIndex}`);
  435. return [resColumn, resRow];
  436. }
  437. removeFileItem(fileItem) {
  438. let index = this._fileItems.indexOf(fileItem);
  439. if (index > -1)
  440. this._fileItems.splice(index, 1);
  441. else
  442. throw new Error('Error removing children from container');
  443. let [column, row] = this._getPosOfFileItem(fileItem);
  444. let placeholder = this.layout.get_child_at(column, row);
  445. placeholder.child = null;
  446. let fileItemHandler = this._fileItemHandlers.get(fileItem);
  447. Object.values(fileItemHandler).forEach(id => fileItem.disconnect(id));
  448. this._fileItemHandlers.delete(fileItem);
  449. }
  450. _fillPlaceholders() {
  451. this._rows = this._getMaxRows();
  452. this._columns = this._getMaxColumns();
  453. for (let column = 0; column < this._columns; column++) {
  454. for (let row = 0; row < this._rows; row++) {
  455. this.layout.attach(new Placeholder(), column, row, 1, 1);
  456. }
  457. }
  458. }
  459. usingNewAllocationAPI() {
  460. return (this.get_fixed_position !== undefined);
  461. }
  462. reset() {
  463. let tmpFileItemsCopy = this._fileItems.slice();
  464. for (let fileItem of tmpFileItemsCopy)
  465. this.removeFileItem(fileItem);
  466. this._grid.remove_all_children();
  467. this._fillPlaceholders();
  468. if (this.usingNewAllocationAPI())
  469. this.allocate_preferred_size(0, 0);
  470. }
  471. _onStageMotion(actor, event) {
  472. if (this._drawingRubberBand) {
  473. let [x, y] = event.get_coords();
  474. this._updateRubberBand(x, y);
  475. this._selectFromRubberband(x, y);
  476. }
  477. return Clutter.EVENT_PROPAGATE;
  478. }
  479. _onPressButton(actor, event) {
  480. let button = event.get_button();
  481. let [x, y] = event.get_coords();
  482. this._grid.grab_key_focus();
  483. if (button == 1) {
  484. let shiftPressed = !!(event.get_state() & Clutter.ModifierType.SHIFT_MASK);
  485. let controlPressed = !!(event.get_state() & Clutter.ModifierType.CONTROL_MASK);
  486. if (!shiftPressed && !controlPressed)
  487. Extension.desktopManager.clearSelection();
  488. let [gridX, gridY] = this._grid.get_transformed_position();
  489. Extension.desktopManager.startRubberBand(x, y, gridX, gridY);
  490. return Clutter.EVENT_STOP;
  491. }
  492. if (button == 3) {
  493. this._openMenu(x, y);
  494. return Clutter.EVENT_STOP;
  495. }
  496. return Clutter.EVENT_PROPAGATE;
  497. }
  498. _addDesktopBackgroundMenu() {
  499. this._desktopBackgroundMenu = this._createDesktopBackgroundMenu();
  500. this._menuManager.addMenu(this._desktopBackgroundMenu);
  501. this.connect('destroy', () => {
  502. this._desktopBackgroundMenu.destroy();
  503. this._desktopBackgroundMenu = null;
  504. });
  505. }
  506. _getMaxColumns() {
  507. let gridWidth = this._grid.allocation.x2 - this._grid.allocation.x1;
  508. let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
  509. let desiredWidth = Prefs.getDesiredWidth(scaleFactor, this._extra_width);
  510. return Math.floor(gridWidth / desiredWidth);
  511. }
  512. _getMaxRows() {
  513. let gridHeight = this._grid.allocation.y2 - this._grid.allocation.y1;
  514. let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
  515. let desiredHeight = Prefs.getDesiredHeight(scaleFactor, this._extra_height);
  516. return Math.floor(gridHeight / desiredHeight);
  517. }
  518. acceptDrop(source, actor, x, y, time) {
  519. /* Coordinates are relative to the grid, we want to transform them to
  520. * absolute coordinates to work across monitors */
  521. let [gridX, gridY] = this.get_transformed_position();
  522. let [absoluteX, absoluteY] = [x + gridX, y + gridY];
  523. return Extension.desktopManager.acceptDrop(absoluteX, absoluteY);
  524. }
  525. _getPosOfFileItem(itemToFind) {
  526. if (itemToFind == null)
  527. throw new Error('Error at _getPosOfFileItem: child cannot be null');
  528. let found = false;
  529. let column = 0;
  530. let row = 0;
  531. for (column = 0; column < this._columns; column++) {
  532. for (row = 0; row < this._rows; row++) {
  533. let item = this.layout.get_child_at(column, row);
  534. if (item.child && item.child.file.equal(itemToFind.file)) {
  535. found = true;
  536. break;
  537. }
  538. }
  539. if (found)
  540. break;
  541. }
  542. if (!found)
  543. throw new Error('Position of file item was not found');
  544. return [column, row];
  545. }
  546. _onFileItemSelected(fileItem, keepCurrentSelection, rubberBandSelection, addToSelection) {
  547. this._grid.grab_key_focus();
  548. }
  549. _onRenameClicked(fileItem) {
  550. if (fileItem.menu && fileItem.menu.isOpen) {
  551. let id = fileItem.menu.connect('menu-closed', () => {
  552. fileItem.menu.disconnect(id);
  553. this.doRename(fileItem);
  554. });
  555. } else {
  556. this.doRename(fileItem);
  557. }
  558. }
  559. doRename(fileItem) {
  560. let renamePopup = new RenamePopup(fileItem);
  561. this._menuManager.addMenu(renamePopup);
  562. this._menuManager.ignoreRelease();
  563. renamePopup.connect('menu-closed', () => renamePopup.destroy());
  564. renamePopup.open();
  565. }
  566. });
  567. var RenamePopupMenuItem = GObject.registerClass(
  568. class RenamePopupMenuItem extends PopupMenu.PopupBaseMenuItem {
  569. _init(fileItem) {
  570. super._init({
  571. style_class: 'rename-popup-item',
  572. reactive: false,
  573. });
  574. if (PopupMenu.Ornament.HIDDEN !== undefined)
  575. this.setOrnament(PopupMenu.Ornament.HIDDEN);
  576. else /* Support version prior 3.34.1 */
  577. this._ornamentLabel.visible = false;
  578. this._fileItem = fileItem;
  579. // Entry
  580. this._entry = new St.Entry({
  581. x_expand: true,
  582. width: 200,
  583. });
  584. this.add_child(this._entry);
  585. this._entry.clutter_text.connect(
  586. 'notify::text', this._validate.bind(this));
  587. this._entry.clutter_text.connect(
  588. 'activate', this._onRenameAccepted.bind(this));
  589. // Rename button
  590. this._button = new St.Button({
  591. style_class: 'button',
  592. reactive: true,
  593. button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO,
  594. can_focus: true,
  595. label: _('Rename'),
  596. });
  597. this.add_child(this._button);
  598. this._button.connect('clicked', this._onRenameAccepted.bind(this));
  599. }
  600. vfunc_map() {
  601. this._entry.text = this._fileItem.displayName;
  602. this._entry.clutter_text.set_selection(0, -1);
  603. super.vfunc_map();
  604. }
  605. vfunc_key_focus_in() {
  606. super.vfunc_key_focus_in();
  607. this._entry.clutter_text.grab_key_focus();
  608. }
  609. _isValidFolderName() {
  610. let newName = this._entry.text.trim();
  611. return newName.length > 0 && newName.indexOf('/') === -1 &&
  612. newName != this._fileItem.displayName;
  613. }
  614. _validate() {
  615. this._button.reactive = this._isValidFolderName();
  616. }
  617. _onRenameAccepted() {
  618. if (!this._isValidFolderName())
  619. return;
  620. DBusUtils.NautilusFileOperationsProxy.RenameFileRemote(
  621. this._fileItem.file.get_uri(),
  622. this._entry.text.trim(),
  623. (result, error) => {
  624. if (error)
  625. throw new Error('Error renaming file: ' + error.message);
  626. }
  627. );
  628. this.activate(Clutter.get_current_event());
  629. }
  630. });
  631. var RenamePopup = class RenameFolderMenu extends PopupMenu.PopupMenu {
  632. constructor(fileItem) {
  633. super(fileItem, 0.5, St.Side.TOP);
  634. this.actor.add_style_class_name('rename-popup');
  635. // We want to keep the item hovered while the menu is up
  636. this.blockSourceEvents = true;
  637. let menuItem = new RenamePopupMenuItem(fileItem);
  638. this.addMenuItem(menuItem);
  639. if (this.focusActor !== undefined) {
  640. // Focus the text entry on menu pop-up, works starting 3.34.1
  641. this.focusActor = menuItem;
  642. } else {
  643. this.connect('open-state-changed', (_menu, state) => {
  644. if (state)
  645. this._openId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
  646. delete this._openId;
  647. menuItem.grab_key_focus()
  648. });
  649. else if (this._openId)
  650. GLib.source_remove(this._openId);
  651. });
  652. }
  653. // Chain our visibility and lifecycle to that of the fileItem source
  654. this._fileItemMappedId = fileItem.connect('notify::mapped', () => {
  655. if (!fileItem.mapped)
  656. this.close();
  657. });
  658. fileItem.connect('destroy', () => {
  659. fileItem.disconnect(this._fileItemMappedId);
  660. this.destroy();
  661. });
  662. Main.uiGroup.add_actor(this.actor);
  663. }
  664. };