appfolderDialog.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. // appfolderDialog.js
  2. // GPLv3
  3. const Clutter = imports.gi.Clutter;
  4. const Gio = imports.gi.Gio;
  5. const St = imports.gi.St;
  6. const Main = imports.ui.main;
  7. const ModalDialog = imports.ui.modalDialog;
  8. const PopupMenu = imports.ui.popupMenu;
  9. const ShellEntry = imports.ui.shellEntry;
  10. const Signals = imports.signals;
  11. const Gtk = imports.gi.Gtk;
  12. const ExtensionUtils = imports.misc.extensionUtils;
  13. const Me = ExtensionUtils.getCurrentExtension();
  14. const Convenience = Me.imports.convenience;
  15. const Extension = Me.imports.extension;
  16. const Gettext = imports.gettext.domain('appfolders-manager');
  17. const _ = Gettext.gettext;
  18. let FOLDER_SCHEMA;
  19. let FOLDER_LIST;
  20. //--------------------------------------------------------------
  21. // This is a modal dialog for creating a new folder, or renaming or modifying
  22. // categories of existing folders.
  23. var AppfolderDialog = class AppfolderDialog {
  24. // build a new dialog. If folder is null, the dialog will be for creating a new
  25. // folder, else app is null, and the dialog will be for editing an existing folder
  26. constructor (folder, app, id) {
  27. this._folder = folder;
  28. this._app = app;
  29. this._id = id;
  30. this.super_dialog = new ModalDialog.ModalDialog({ destroyOnClose: true });
  31. FOLDER_SCHEMA = new Gio.Settings({ schema_id: 'org.gnome.desktop.app-folders' });
  32. FOLDER_LIST = FOLDER_SCHEMA.get_strv('folder-children');
  33. let nameSection = this._buildNameSection();
  34. let categoriesSection = this._buildCategoriesSection();
  35. this.super_dialog.contentLayout.style = 'spacing: 20px';
  36. this.super_dialog.contentLayout.add(nameSection, {
  37. x_fill: false,
  38. x_align: St.Align.START,
  39. y_align: St.Align.START
  40. });
  41. if ( Convenience.getSettings('org.gnome.shell.extensions.appfolders-manager').get_boolean('categories') ) {
  42. this.super_dialog.contentLayout.add(categoriesSection, {
  43. x_fill: false,
  44. x_align: St.Align.START,
  45. y_align: St.Align.START
  46. });
  47. }
  48. if (this._folder == null) {
  49. this.super_dialog.setButtons([
  50. { action: this.destroy.bind(this),
  51. label: _("Cancel"),
  52. key: Clutter.Escape },
  53. { action: this._apply.bind(this),
  54. label: _("Create"),
  55. key: Clutter.Return }
  56. ]);
  57. } else {
  58. this.super_dialog.setButtons([
  59. { action: this.destroy.bind(this),
  60. label: _("Cancel"),
  61. key: Clutter.Escape },
  62. { action: this._deleteFolder.bind(this),
  63. label: _("Delete"),
  64. key: Clutter.Delete },
  65. { action: this._apply.bind(this),
  66. label: _("Apply"),
  67. key: Clutter.Return }
  68. ]);
  69. }
  70. this._nameEntryText.connect('key-press-event', (o, e) => {
  71. let symbol = e.get_key_symbol();
  72. if (symbol == Clutter.Return || symbol == Clutter.KP_Enter) {
  73. this.super_dialog.popModal();
  74. this._apply();
  75. }
  76. });
  77. }
  78. // build the section of the UI handling the folder's name and returns it.
  79. _buildNameSection () {
  80. let nameSection = new St.BoxLayout({
  81. style: 'spacing: 5px;',
  82. vertical: true,
  83. x_expand: true,
  84. natural_width_set: true,
  85. natural_width: 350,
  86. });
  87. let nameLabel = new St.Label({
  88. text: _("Folder's name:"),
  89. style: 'font-weight: bold;',
  90. });
  91. nameSection.add(nameLabel, { y_align: St.Align.START });
  92. this._nameEntry = new St.Entry({
  93. x_expand: true,
  94. });
  95. this._nameEntryText = null; ///???
  96. this._nameEntryText = this._nameEntry.clutter_text;
  97. nameSection.add(this._nameEntry, { y_align: St.Align.START });
  98. ShellEntry.addContextMenu(this._nameEntry);
  99. this.super_dialog.setInitialKeyFocus(this._nameEntryText);
  100. if (this._folder != null) {
  101. this._nameEntryText.set_text(this._folder.get_string('name'));
  102. }
  103. return nameSection;
  104. }
  105. // build the section of the UI handling the folder's categories and returns it.
  106. _buildCategoriesSection () {
  107. let categoriesSection = new St.BoxLayout({
  108. style: 'spacing: 5px;',
  109. vertical: true,
  110. x_expand: true,
  111. natural_width_set: true,
  112. natural_width: 350,
  113. });
  114. let categoriesLabel = new St.Label({
  115. text: _("Categories:"),
  116. style: 'font-weight: bold;',
  117. });
  118. categoriesSection.add(categoriesLabel, {
  119. x_fill: false,
  120. x_align: St.Align.START,
  121. y_align: St.Align.START,
  122. });
  123. let categoriesBox = new St.BoxLayout({
  124. style: 'spacing: 5px;',
  125. vertical: false,
  126. x_expand: true,
  127. });
  128. // at the left, how to add categories
  129. let addCategoryBox = new St.BoxLayout({
  130. style: 'spacing: 5px;',
  131. vertical: true,
  132. x_expand: true,
  133. });
  134. this._categoryEntry = new St.Entry({
  135. can_focus: true,
  136. x_expand: true,
  137. hint_text: _("Other category?"),
  138. secondary_icon: new St.Icon({
  139. icon_name: 'list-add-symbolic',
  140. icon_size: 16,
  141. style_class: 'system-status-icon',
  142. y_align: Clutter.ActorAlign.CENTER,
  143. }),
  144. });
  145. ShellEntry.addContextMenu(this._categoryEntry, null);
  146. this._categoryEntry.connect('secondary-icon-clicked', this._addCategory.bind(this));
  147. this._categoryEntryText = null; ///???
  148. this._categoryEntryText = this._categoryEntry.clutter_text;
  149. this._catSelectButton = new SelectCategoryButton(this);
  150. addCategoryBox.add(this._catSelectButton.actor, { y_align: St.Align.CENTER });
  151. addCategoryBox.add(this._categoryEntry, { y_align: St.Align.START });
  152. categoriesBox.add(addCategoryBox, {
  153. x_fill: true,
  154. x_align: St.Align.START,
  155. y_align: St.Align.START,
  156. });
  157. // at the right, a list of categories
  158. this.listContainer = new St.BoxLayout({
  159. vertical: true,
  160. x_expand: true,
  161. });
  162. this.noCatLabel = new St.Label({ text: _("No category") });
  163. this.listContainer.add_actor(this.noCatLabel);
  164. categoriesBox.add(this.listContainer, {
  165. x_fill: true,
  166. x_align: St.Align.END,
  167. y_align: St.Align.START,
  168. });
  169. categoriesSection.add(categoriesBox, {
  170. x_fill: true,
  171. x_align: St.Align.START,
  172. y_align: St.Align.START,
  173. });
  174. // Load categories is necessary even if no this._folder,
  175. // because it initializes the value of this._categories
  176. this._loadCategories();
  177. return categoriesSection;
  178. }
  179. open () {
  180. this.super_dialog.open();
  181. }
  182. // returns if a folder id already exists
  183. _alreadyExists (folderId) {
  184. for(var i = 0; i < FOLDER_LIST.length; i++) {
  185. if (FOLDER_LIST[i] == folderId) {
  186. // this._showError( _("This appfolder already exists.") );
  187. return true;
  188. }
  189. }
  190. return false;
  191. }
  192. destroy () {
  193. if ( Convenience.getSettings('org.gnome.shell.extensions.appfolders-manager').get_boolean('debug') ) {
  194. log('[AppfolderDialog v2] destroying dialog');
  195. }
  196. this._catSelectButton.destroy(); // TODO ?
  197. this.super_dialog.destroy(); //XXX crée des erreurs reloues ???
  198. }
  199. // Generates a valid folder id, which as no space, no dot, no slash, and which
  200. // doesn't already exist.
  201. _folderId (newName) {
  202. let tmp0 = newName.split(" ");
  203. let folderId = "";
  204. for(var i = 0; i < tmp0.length; i++) {
  205. folderId += tmp0[i];
  206. }
  207. tmp0 = folderId.split(".");
  208. folderId = "";
  209. for(var i = 0; i < tmp0.length; i++) {
  210. folderId += tmp0[i];
  211. }
  212. tmp0 = folderId.split("/");
  213. folderId = "";
  214. for(var i = 0; i < tmp0.length; i++) {
  215. folderId += tmp0[i];
  216. }
  217. if(this._alreadyExists(folderId)) {
  218. folderId = this._folderId(folderId+'_');
  219. }
  220. return folderId;
  221. }
  222. // creates a folder from the data filled by the user (with no properties)
  223. _create () {
  224. let folderId = this._folderId(this._nameEntryText.get_text());
  225. FOLDER_LIST.push(folderId);
  226. FOLDER_SCHEMA.set_strv('folder-children', FOLDER_LIST);
  227. this._folder = new Gio.Settings({
  228. schema_id: 'org.gnome.desktop.app-folders.folder',
  229. path: '/org/gnome/desktop/app-folders/folders/' + folderId + '/'
  230. });
  231. // this._folder.set_string('name', this._nameEntryText.get_text()); //superflu
  232. // est-il nécessaire d'initialiser la clé apps à [] ??
  233. this._addToFolder();
  234. }
  235. // sets the name to the folder
  236. _applyName () {
  237. let newName = this._nameEntryText.get_text();
  238. this._folder.set_string('name', newName); // génère un bug ?
  239. return Clutter.EVENT_STOP;
  240. }
  241. // loads categories, as set in gsettings, to the UI
  242. _loadCategories () {
  243. if (this._folder == null) {
  244. this._categories = [];
  245. } else {
  246. this._categories = this._folder.get_strv('categories');
  247. if ((this._categories == null) || (this._categories.length == 0)) {
  248. this._categories = [];
  249. } else {
  250. this.noCatLabel.visible = false;
  251. }
  252. }
  253. this._categoriesButtons = [];
  254. for (var i = 0; i < this._categories.length; i++) {
  255. this._addCategoryBox(i);
  256. }
  257. }
  258. _addCategoryBox (i) {
  259. let aCategory = new AppCategoryBox(this, i);
  260. this.listContainer.add_actor(aCategory.super_box);
  261. }
  262. // adds a category to the UI (will be added to gsettings when pressing "apply" only)
  263. _addCategory (entry, new_cat_name) {
  264. if (new_cat_name == null) {
  265. new_cat_name = this._categoryEntryText.get_text();
  266. }
  267. if (this._categories.indexOf(new_cat_name) != -1) {
  268. return;
  269. }
  270. if (new_cat_name == '') {
  271. return;
  272. }
  273. this._categories.push(new_cat_name);
  274. this._categoryEntryText.set_text('');
  275. this.noCatLabel.visible = false;
  276. this._addCategoryBox(this._categories.length-1);
  277. }
  278. // adds all categories to gsettings
  279. _applyCategories () {
  280. this._folder.set_strv('categories', this._categories);
  281. return Clutter.EVENT_STOP;
  282. }
  283. // Apply everything by calling methods above, and reload the view
  284. _apply () {
  285. if (this._app != null) {
  286. this._create();
  287. // this._addToFolder();
  288. }
  289. this._applyCategories();
  290. this._applyName();
  291. this.destroy();
  292. //-----------------------
  293. Main.overview.viewSelector.appDisplay._views[1].view._redisplay();
  294. if ( Convenience.getSettings('org.gnome.shell.extensions.appfolders-manager').get_boolean('debug') ) {
  295. log('[AppfolderDialog v2] reload the view');
  296. }
  297. }
  298. // initializes the folder with its first app. This is not optional since empty
  299. // folders are not displayed. TODO use the equivalent method from extension.js
  300. _addToFolder () {
  301. let content = this._folder.get_strv('apps');
  302. content.push(this._app);
  303. this._folder.set_strv('apps', content);
  304. }
  305. // Delete the folder, using the extension.js method
  306. _deleteFolder () {
  307. if (this._folder != null) {
  308. Extension.deleteFolder(this._id);
  309. }
  310. this.destroy();
  311. }
  312. };
  313. //------------------------------------------------
  314. // Very complex way to have a menubutton for displaying a menu with standard
  315. // categories. Button part.
  316. class SelectCategoryButton {
  317. constructor (dialog) {
  318. this._dialog = dialog;
  319. let catSelectBox = new St.BoxLayout({
  320. vertical: false,
  321. x_expand: true,
  322. });
  323. let catSelectLabel = new St.Label({
  324. text: _("Select a category…"),
  325. x_align: Clutter.ActorAlign.START,
  326. y_align: Clutter.ActorAlign.CENTER,
  327. x_expand: true,
  328. });
  329. let catSelectIcon = new St.Icon({
  330. icon_name: 'pan-down-symbolic',
  331. icon_size: 16,
  332. style_class: 'system-status-icon',
  333. x_expand: false,
  334. x_align: Clutter.ActorAlign.END,
  335. y_align: Clutter.ActorAlign.CENTER,
  336. });
  337. catSelectBox.add(catSelectLabel, { y_align: St.Align.MIDDLE });
  338. catSelectBox.add(catSelectIcon, { y_align: St.Align.END });
  339. this.actor = new St.Button ({
  340. x_align: Clutter.ActorAlign.CENTER,
  341. y_align: Clutter.ActorAlign.CENTER,
  342. child: catSelectBox,
  343. style_class: 'button',
  344. style: 'padding: 5px 5px;',
  345. x_expand: true,
  346. y_expand: false,
  347. x_fill: true,
  348. y_fill: true,
  349. });
  350. this.actor.connect('button-press-event', this._onButtonPress.bind(this));
  351. this._menu = null;
  352. this._menuManager = new PopupMenu.PopupMenuManager(this);
  353. }
  354. popupMenu () {
  355. this.actor.fake_release();
  356. if (!this._menu) {
  357. this._menu = new SelectCategoryMenu(this, this._dialog);
  358. this._menu.super_menu.connect('open-state-changed', (menu, isPoppedUp) => {
  359. if (!isPoppedUp) {
  360. this.actor.sync_hover();
  361. this.emit('menu-state-changed', false);
  362. }
  363. });
  364. this._menuManager.addMenu(this._menu.super_menu);
  365. }
  366. this.emit('menu-state-changed', true);
  367. this.actor.set_hover(true);
  368. this._menu.popup();
  369. this._menuManager.ignoreRelease();
  370. return false;
  371. }
  372. _onButtonPress (actor, event) {
  373. this.popupMenu();
  374. return Clutter.EVENT_STOP;
  375. }
  376. destroy () {
  377. if (this._menu) {
  378. this._menu.destroy();
  379. }
  380. this.actor.destroy();
  381. }
  382. };
  383. Signals.addSignalMethods(SelectCategoryButton.prototype);
  384. //------------------------------------------------
  385. // Very complex way to have a menubutton for displaying a menu with standard
  386. // categories. Menu part.
  387. class SelectCategoryMenu {
  388. constructor (source, dialog) {
  389. this.super_menu = new PopupMenu.PopupMenu(source.actor, 0.5, St.Side.RIGHT);
  390. this._source = source;
  391. this._dialog = dialog;
  392. this.super_menu.actor.add_style_class_name('app-well-menu');
  393. this._source.actor.connect('destroy', this.super_menu.destroy.bind(this));
  394. // We want to keep the item hovered while the menu is up //XXX used ??
  395. this.super_menu.blockSourceEvents = true;
  396. Main.uiGroup.add_actor(this.super_menu.actor);
  397. // This is a really terrible hack to overwrite _redisplay without
  398. // actually inheriting from PopupMenu.PopupMenu
  399. this.super_menu._redisplay = this._redisplay;
  400. this.super_menu._dialog = this._dialog;
  401. }
  402. _redisplay () {
  403. this.removeAll();
  404. let mainCategories = ['AudioVideo', 'Audio', 'Video', 'Development',
  405. 'Education', 'Game', 'Graphics', 'Network', 'Office', 'Science',
  406. 'Settings', 'System', 'Utility'];
  407. for (var i=0; i<mainCategories.length; i++) {
  408. let labelItem = mainCategories[i] ;
  409. let item = new PopupMenu.PopupMenuItem( labelItem );
  410. item.connect('activate', () => {
  411. this._dialog._addCategory(null, labelItem);
  412. });
  413. this.addMenuItem(item);
  414. }
  415. }
  416. popup (activatingButton) {
  417. this.super_menu._redisplay();
  418. this.super_menu.open();
  419. }
  420. destroy () {
  421. this.super_menu.close(); //FIXME error in the logs but i don't care
  422. this.super_menu.destroy();
  423. }
  424. };
  425. Signals.addSignalMethods(SelectCategoryMenu.prototype);
  426. //----------------------------------------
  427. // This custom widget is a deletable row, displaying a category name.
  428. class AppCategoryBox {
  429. constructor (dialog, i) {
  430. this.super_box = new St.BoxLayout({
  431. vertical: false,
  432. style_class: 'appCategoryBox',
  433. });
  434. this._dialog = dialog;
  435. this.catName = this._dialog._categories[i];
  436. this.super_box.add_actor(new St.Label({
  437. text: this.catName,
  438. y_align: Clutter.ActorAlign.CENTER,
  439. x_align: Clutter.ActorAlign.CENTER,
  440. }));
  441. this.super_box.add_actor( new St.BoxLayout({ x_expand: true }) );
  442. this.deleteButton = new St.Button({
  443. x_expand: false,
  444. y_expand: true,
  445. style_class: 'appCategoryDeleteBtn',
  446. y_align: Clutter.ActorAlign.CENTER,
  447. x_align: Clutter.ActorAlign.CENTER,
  448. child: new St.Icon({
  449. icon_name: 'edit-delete-symbolic',
  450. icon_size: 16,
  451. style_class: 'system-status-icon',
  452. x_expand: false,
  453. y_expand: true,
  454. style: 'margin: 3px;',
  455. y_align: Clutter.ActorAlign.CENTER,
  456. x_align: Clutter.ActorAlign.CENTER,
  457. }),
  458. });
  459. this.super_box.add_actor(this.deleteButton);
  460. this.deleteButton.connect('clicked', this.removeFromList.bind(this));
  461. }
  462. removeFromList () {
  463. this._dialog._categories.splice(this._dialog._categories.indexOf(this.catName), 1);
  464. if (this._dialog._categories.length == 0) {
  465. this._dialog.noCatLabel.visible = true;
  466. }
  467. this.destroy();
  468. }
  469. destroy () {
  470. this.deleteButton.destroy();
  471. this.super_box.destroy();
  472. }
  473. };