543 lines
15 KiB
JavaScript
543 lines
15 KiB
JavaScript
|
// 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<mainCategories.length; i++) {
|
||
|
let labelItem = mainCategories[i] ;
|
||
|
let item = new PopupMenu.PopupMenuItem( labelItem );
|
||
|
item.connect('activate', () => {
|
||
|
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();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|