diff --git a/data/theme/gnome-shell-sass/widgets/_app-grid.scss b/data/theme/gnome-shell-sass/widgets/_app-grid.scss index f5467bbd6..42aeadc19 100644 --- a/data/theme/gnome-shell-sass/widgets/_app-grid.scss +++ b/data/theme/gnome-shell-sass/widgets/_app-grid.scss @@ -90,6 +90,33 @@ $app_grid_fg_color: #fff; border-radius: 8px; spacing: 24px; background-color: transparentize(darken($osd_bg_color,10%), 0.05); + + & .folder-name-container { + padding: 12px 18px; + spacing: 12px; + + & .folder-name-label, + & .folder-name-entry { + font-size: 18pt; + font-weight: bold; + } + + & .folder-name-entry { width: 300px } + + /* FIXME: this is to keep the label in sync with the entry */ + & .folder-name-label { padding: 5px 7px } + + & .edit-folder-button { + @extend %button; + + padding: 0; + width: 36px; + height: 36px; + border-radius: 18px; + + & > StIcon { icon-size: 16px } + } + } } .app-folder-dialog-container { padding: 12px; diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 9f60eef20..88e34cc6e 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -1629,7 +1629,7 @@ var FolderIcon = GObject.registerClass({ if (this._dialog) return; if (!this._dialog) { - this._dialog = new AppFolderDialog(this); + this._dialog = new AppFolderDialog(this, this._folder); this._parentView.addFolderDialog(this._dialog); this._dialog.connect('open-state-changed', (popup, isOpen) => { if (!isOpen) @@ -1817,7 +1817,7 @@ var AppFolderDialog = GObject.registerClass({ 'open-state-changed': { param_types: [GObject.TYPE_BOOLEAN] }, }, }, class AppFolderDialog extends St.Widget { - _init(source) { + _init(source, folder) { super._init({ layout_manager: new Clutter.BinLayout(), style_class: 'app-folder-dialog-container', @@ -1833,6 +1833,7 @@ var AppFolderDialog = GObject.registerClass({ })); this._source = source; + this._folder = folder; this._view = source.view; this._isOpen = false; @@ -1844,8 +1845,11 @@ var AppFolderDialog = GObject.registerClass({ y_expand: true, x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.FILL, + vertical: true, }); this.add_child(this._viewBox); + + this._addFolderNameEntry(); this._viewBox.add_child(this._view); global.focus_manager.add_group(this); @@ -1860,6 +1864,134 @@ var AppFolderDialog = GObject.registerClass({ this._needsZoomAndFade = false; } + _addFolderNameEntry() { + this._entryBox = new St.BoxLayout({ + style_class: 'folder-name-container', + }); + this._viewBox.add_child(this._entryBox); + + // Empty actor to center the title + let ghostButton = new Clutter.Actor(); + this._entryBox.add_child(ghostButton); + + let stack = new Shell.Stack({ + x_expand: true, + x_align: Clutter.ActorAlign.CENTER, + }); + this._entryBox.add_child(stack); + + // Folder name label + this._folderNameLabel = new St.Label({ + style_class: 'folder-name-label', + x_expand: true, + y_expand: true, + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER, + }); + + stack.add_child(this._folderNameLabel); + + // Folder name entry + this._entry = new St.Entry({ + style_class: 'folder-name-entry', + opacity: 0, + reactive: false, + }); + this._entry.clutter_text.set({ + x_expand: true, + x_align: Clutter.ActorAlign.CENTER, + }); + + this._entry.clutter_text.connect('activate', () => { + this._showFolderLabel(); + }); + + stack.add_child(this._entry); + + // Edit button + this._editButton = new St.Button({ + style_class: 'edit-folder-button', + button_mask: St.ButtonMask.ONE, + toggle_mode: true, + reactive: true, + can_focus: true, + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.CENTER, + child: new St.Icon({ + icon_name: 'document-edit-symbolic', + icon_size: 16, + }), + }); + + this._editButton.connect('notify::checked', () => { + if (this._editButton.checked) + this._showFolderEntry(); + else + this._showFolderLabel(); + }); + + this._entryBox.add_child(this._editButton); + + ghostButton.add_constraint(new Clutter.BindConstraint({ + source: this._editButton, + coordinate: Clutter.BindCoordinate.SIZE, + })); + + this._folder.connect('changed::name', () => this._syncFolderName()); + this._syncFolderName(); + } + + _syncFolderName() { + let newName = _getFolderName(this._folder); + + this._folderNameLabel.text = newName; + this._entry.text = newName; + } + + _switchActor(from, to) { + to.reactive = true; + to.ease({ + opacity: 255, + duration: 300, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + from.ease({ + opacity: 0, + duration: 300, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + from.reactive = false; + }, + }); + } + + _showFolderLabel() { + if (this._editButton.checked) + this._editButton.checked = false; + + this._maybeUpdateFolderName(); + this._switchActor(this._entry, this._folderNameLabel); + } + + _showFolderEntry() { + this._switchActor(this._folderNameLabel, this._entry); + + this._entry.clutter_text.set_selection(0, -1); + this._entry.clutter_text.grab_key_focus(); + } + + _maybeUpdateFolderName() { + let folderName = _getFolderName(this._folder); + let newFolderName = this._entry.text.trim(); + + if (newFolderName.length === 0 || newFolderName === folderName) + return; + + this._folder.set_string('name', newFolderName); + this._folder.set_boolean('translate', false); + } + _zoomAndFadeIn() { let [sourceX, sourceY] = this._source.get_transformed_position(); @@ -1944,7 +2076,13 @@ var AppFolderDialog = GObject.registerClass({ vfunc_allocate(box, flags) { let contentBox = this.get_theme_node().get_content_box(box); - this._view.adaptToSize(contentBox.get_width(), contentBox.get_height()); + + let [, entryBoxHeight] = this._entryBox.get_size(); + let spacing = this._viewBox.layout_manager.spacing; + + this._view.adaptToSize( + contentBox.get_width(), + contentBox.get_height() - entryBoxHeight - spacing); super.vfunc_allocate(box, flags); @@ -2027,6 +2165,7 @@ var AppFolderDialog = GObject.registerClass({ return; this._zoomAndFadeOut(); + this._showFolderLabel(); this._grabHelper.ungrab({ actor: this }); this._isOpen = false;