// appfolderDialog.js // GPLv3 const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; const St = imports.gi.St; const Main = imports.ui.main; const ModalDialog = imports.ui.modalDialog; const PopupMenu = imports.ui.popupMenu; const ShellEntry = imports.ui.shellEntry; const Signals = imports.signals; const Gtk = imports.gi.Gtk; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = Me.imports.convenience; const Extension = Me.imports.extension; const Gettext = imports.gettext.domain('appfolders-manager'); const _ = Gettext.gettext; let FOLDER_SCHEMA; let FOLDER_LIST; //-------------------------------------------------------------- // This is a modal dialog for creating a new folder, or renaming or modifying // categories of existing folders. var AppfolderDialog = class AppfolderDialog { // build a new dialog. If folder is null, the dialog will be for creating a new // folder, else app is null, and the dialog will be for editing an existing folder constructor (folder, app, id) { this._folder = folder; this._app = app; this._id = id; this.super_dialog = new ModalDialog.ModalDialog({ destroyOnClose: true }); FOLDER_SCHEMA = new Gio.Settings({ schema_id: 'org.gnome.desktop.app-folders' }); FOLDER_LIST = FOLDER_SCHEMA.get_strv('folder-children'); let nameSection = this._buildNameSection(); let categoriesSection = this._buildCategoriesSection(); this.super_dialog.contentLayout.style = 'spacing: 20px'; this.super_dialog.contentLayout.add(nameSection, { x_fill: false, x_align: St.Align.START, y_align: St.Align.START }); if ( Convenience.getSettings('org.gnome.shell.extensions.appfolders-manager').get_boolean('categories') ) { this.super_dialog.contentLayout.add(categoriesSection, { x_fill: false, x_align: St.Align.START, y_align: St.Align.START }); } if (this._folder == null) { this.super_dialog.setButtons([ { action: this.destroy.bind(this), label: _("Cancel"), key: Clutter.Escape }, { action: this._apply.bind(this), label: _("Create"), key: Clutter.Return } ]); } else { this.super_dialog.setButtons([ { action: this.destroy.bind(this), label: _("Cancel"), key: Clutter.Escape }, { action: this._deleteFolder.bind(this), label: _("Delete"), key: Clutter.Delete }, { action: this._apply.bind(this), label: _("Apply"), key: Clutter.Return } ]); } this._nameEntryText.connect('key-press-event', (o, e) => { let symbol = e.get_key_symbol(); if (symbol == Clutter.Return || symbol == Clutter.KP_Enter) { this.super_dialog.popModal(); this._apply(); } }); } // build the section of the UI handling the folder's name and returns it. _buildNameSection () { let nameSection = new St.BoxLayout({ style: 'spacing: 5px;', vertical: true, x_expand: true, natural_width_set: true, natural_width: 350, }); let nameLabel = new St.Label({ text: _("Folder's name:"), style: 'font-weight: bold;', }); nameSection.add(nameLabel, { y_align: St.Align.START }); this._nameEntry = new St.Entry({ x_expand: true, }); this._nameEntryText = null; ///??? this._nameEntryText = this._nameEntry.clutter_text; nameSection.add(this._nameEntry, { y_align: St.Align.START }); ShellEntry.addContextMenu(this._nameEntry); this.super_dialog.setInitialKeyFocus(this._nameEntryText); if (this._folder != null) { this._nameEntryText.set_text(this._folder.get_string('name')); } return nameSection; } // build the section of the UI handling the folder's categories and returns it. _buildCategoriesSection () { let categoriesSection = new St.BoxLayout({ style: 'spacing: 5px;', vertical: true, x_expand: true, natural_width_set: true, natural_width: 350, }); let categoriesLabel = new St.Label({ text: _("Categories:"), style: 'font-weight: bold;', }); categoriesSection.add(categoriesLabel, { x_fill: false, x_align: St.Align.START, y_align: St.Align.START, }); let categoriesBox = new St.BoxLayout({ style: 'spacing: 5px;', vertical: false, x_expand: true, }); // at the left, how to add categories let addCategoryBox = new St.BoxLayout({ style: 'spacing: 5px;', vertical: true, x_expand: true, }); this._categoryEntry = new St.Entry({ can_focus: true, x_expand: true, hint_text: _("Other category?"), secondary_icon: new St.Icon({ icon_name: 'list-add-symbolic', icon_size: 16, style_class: 'system-status-icon', y_align: Clutter.ActorAlign.CENTER, }), }); ShellEntry.addContextMenu(this._categoryEntry, null); this._categoryEntry.connect('secondary-icon-clicked', this._addCategory.bind(this)); this._categoryEntryText = null; ///??? this._categoryEntryText = this._categoryEntry.clutter_text; this._catSelectButton = new SelectCategoryButton(this); addCategoryBox.add(this._catSelectButton.actor, { y_align: St.Align.CENTER }); addCategoryBox.add(this._categoryEntry, { y_align: St.Align.START }); categoriesBox.add(addCategoryBox, { x_fill: true, x_align: St.Align.START, y_align: St.Align.START, }); // at the right, a list of categories this.listContainer = new St.BoxLayout({ vertical: true, x_expand: true, }); this.noCatLabel = new St.Label({ text: _("No category") }); this.listContainer.add_actor(this.noCatLabel); categoriesBox.add(this.listContainer, { x_fill: true, x_align: St.Align.END, y_align: St.Align.START, }); categoriesSection.add(categoriesBox, { x_fill: true, x_align: St.Align.START, y_align: St.Align.START, }); // Load categories is necessary even if no this._folder, // because it initializes the value of this._categories this._loadCategories(); return categoriesSection; } open () { this.super_dialog.open(); } // returns if a folder id already exists _alreadyExists (folderId) { for(var i = 0; i < FOLDER_LIST.length; i++) { if (FOLDER_LIST[i] == folderId) { // this._showError( _("This appfolder already exists.") ); return true; } } return false; } destroy () { if ( Convenience.getSettings('org.gnome.shell.extensions.appfolders-manager').get_boolean('debug') ) { log('[AppfolderDialog v2] destroying dialog'); } this._catSelectButton.destroy(); // TODO ? this.super_dialog.destroy(); //XXX crée des erreurs reloues ??? } // Generates a valid folder id, which as no space, no dot, no slash, and which // doesn't already exist. _folderId (newName) { let tmp0 = newName.split(" "); let folderId = ""; for(var i = 0; i < tmp0.length; i++) { folderId += tmp0[i]; } tmp0 = folderId.split("."); folderId = ""; for(var i = 0; i < tmp0.length; i++) { folderId += tmp0[i]; } tmp0 = folderId.split("/"); folderId = ""; for(var i = 0; i < tmp0.length; i++) { folderId += tmp0[i]; } if(this._alreadyExists(folderId)) { folderId = this._folderId(folderId+'_'); } return folderId; } // creates a folder from the data filled by the user (with no properties) _create () { let folderId = this._folderId(this._nameEntryText.get_text()); FOLDER_LIST.push(folderId); FOLDER_SCHEMA.set_strv('folder-children', FOLDER_LIST); this._folder = new Gio.Settings({ schema_id: 'org.gnome.desktop.app-folders.folder', path: '/org/gnome/desktop/app-folders/folders/' + folderId + '/' }); // this._folder.set_string('name', this._nameEntryText.get_text()); //superflu // est-il nécessaire d'initialiser la clé apps à [] ?? this._addToFolder(); } // sets the name to the folder _applyName () { let newName = this._nameEntryText.get_text(); this._folder.set_string('name', newName); // génère un bug ? return Clutter.EVENT_STOP; } // loads categories, as set in gsettings, to the UI _loadCategories () { if (this._folder == null) { this._categories = []; } else { this._categories = this._folder.get_strv('categories'); if ((this._categories == null) || (this._categories.length == 0)) { this._categories = []; } else { this.noCatLabel.visible = false; } } this._categoriesButtons = []; for (var i = 0; i < this._categories.length; i++) { this._addCategoryBox(i); } } _addCategoryBox (i) { let aCategory = new AppCategoryBox(this, i); this.listContainer.add_actor(aCategory.super_box); } // adds a category to the UI (will be added to gsettings when pressing "apply" only) _addCategory (entry, new_cat_name) { if (new_cat_name == null) { new_cat_name = this._categoryEntryText.get_text(); } if (this._categories.indexOf(new_cat_name) != -1) { return; } if (new_cat_name == '') { return; } this._categories.push(new_cat_name); this._categoryEntryText.set_text(''); this.noCatLabel.visible = false; this._addCategoryBox(this._categories.length-1); } // adds all categories to gsettings _applyCategories () { this._folder.set_strv('categories', this._categories); return Clutter.EVENT_STOP; } // Apply everything by calling methods above, and reload the view _apply () { if (this._app != null) { this._create(); // this._addToFolder(); } this._applyCategories(); this._applyName(); this.destroy(); //----------------------- Main.overview.viewSelector.appDisplay._views[1].view._redisplay(); if ( Convenience.getSettings('org.gnome.shell.extensions.appfolders-manager').get_boolean('debug') ) { log('[AppfolderDialog v2] reload the view'); } } // initializes the folder with its first app. This is not optional since empty // folders are not displayed. TODO use the equivalent method from extension.js _addToFolder () { let content = this._folder.get_strv('apps'); content.push(this._app); this._folder.set_strv('apps', content); } // Delete the folder, using the extension.js method _deleteFolder () { if (this._folder != null) { Extension.deleteFolder(this._id); } this.destroy(); } }; //------------------------------------------------ // Very complex way to have a menubutton for displaying a menu with standard // categories. Button part. class SelectCategoryButton { constructor (dialog) { this._dialog = dialog; let catSelectBox = new St.BoxLayout({ vertical: false, x_expand: true, }); let catSelectLabel = new St.Label({ text: _("Select a category…"), x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, x_expand: true, }); let catSelectIcon = new St.Icon({ icon_name: 'pan-down-symbolic', icon_size: 16, style_class: 'system-status-icon', x_expand: false, x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER, }); catSelectBox.add(catSelectLabel, { y_align: St.Align.MIDDLE }); catSelectBox.add(catSelectIcon, { y_align: St.Align.END }); this.actor = new St.Button ({ x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER, child: catSelectBox, style_class: 'button', style: 'padding: 5px 5px;', x_expand: true, y_expand: false, x_fill: true, y_fill: true, }); this.actor.connect('button-press-event', this._onButtonPress.bind(this)); this._menu = null; this._menuManager = new PopupMenu.PopupMenuManager(this); } popupMenu () { this.actor.fake_release(); if (!this._menu) { this._menu = new SelectCategoryMenu(this, this._dialog); this._menu.super_menu.connect('open-state-changed', (menu, isPoppedUp) => { if (!isPoppedUp) { this.actor.sync_hover(); this.emit('menu-state-changed', false); } }); this._menuManager.addMenu(this._menu.super_menu); } this.emit('menu-state-changed', true); this.actor.set_hover(true); this._menu.popup(); this._menuManager.ignoreRelease(); return false; } _onButtonPress (actor, event) { this.popupMenu(); return Clutter.EVENT_STOP; } destroy () { if (this._menu) { this._menu.destroy(); } this.actor.destroy(); } }; Signals.addSignalMethods(SelectCategoryButton.prototype); //------------------------------------------------ // Very complex way to have a menubutton for displaying a menu with standard // categories. Menu part. class SelectCategoryMenu { constructor (source, dialog) { this.super_menu = new PopupMenu.PopupMenu(source.actor, 0.5, St.Side.RIGHT); this._source = source; this._dialog = dialog; this.super_menu.actor.add_style_class_name('app-well-menu'); this._source.actor.connect('destroy', this.super_menu.destroy.bind(this)); // We want to keep the item hovered while the menu is up //XXX used ?? this.super_menu.blockSourceEvents = true; Main.uiGroup.add_actor(this.super_menu.actor); // This is a really terrible hack to overwrite _redisplay without // actually inheriting from PopupMenu.PopupMenu this.super_menu._redisplay = this._redisplay; this.super_menu._dialog = this._dialog; } _redisplay () { this.removeAll(); let mainCategories = ['AudioVideo', 'Audio', 'Video', 'Development', 'Education', 'Game', 'Graphics', 'Network', 'Office', 'Science', 'Settings', 'System', 'Utility']; for (var i=0; i { this._dialog._addCategory(null, labelItem); }); this.addMenuItem(item); } } popup (activatingButton) { this.super_menu._redisplay(); this.super_menu.open(); } destroy () { this.super_menu.close(); //FIXME error in the logs but i don't care this.super_menu.destroy(); } }; Signals.addSignalMethods(SelectCategoryMenu.prototype); //---------------------------------------- // This custom widget is a deletable row, displaying a category name. class AppCategoryBox { constructor (dialog, i) { this.super_box = new St.BoxLayout({ vertical: false, style_class: 'appCategoryBox', }); this._dialog = dialog; this.catName = this._dialog._categories[i]; this.super_box.add_actor(new St.Label({ text: this.catName, y_align: Clutter.ActorAlign.CENTER, x_align: Clutter.ActorAlign.CENTER, })); this.super_box.add_actor( new St.BoxLayout({ x_expand: true }) ); this.deleteButton = new St.Button({ x_expand: false, y_expand: true, style_class: 'appCategoryDeleteBtn', y_align: Clutter.ActorAlign.CENTER, x_align: Clutter.ActorAlign.CENTER, child: new St.Icon({ icon_name: 'edit-delete-symbolic', icon_size: 16, style_class: 'system-status-icon', x_expand: false, y_expand: true, style: 'margin: 3px;', y_align: Clutter.ActorAlign.CENTER, x_align: Clutter.ActorAlign.CENTER, }), }); this.super_box.add_actor(this.deleteButton); this.deleteButton.connect('clicked', this.removeFromList.bind(this)); } removeFromList () { this._dialog._categories.splice(this._dialog._categories.indexOf(this.catName), 1); if (this._dialog._categories.length == 0) { this._dialog.noCatLabel.visible = true; } this.destroy(); } destroy () { this.deleteButton.destroy(); this.super_box.destroy(); } };