/*
* Copyright 2013 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This class controls a ribbon button.
*
* - It can call a configured function when pressed.
* - It supports enabling/disabling upon the current selection (see {@link #cfg-selection-target-id}) and associated rights (see {@link #cfg-rights}).
* - It supports enabling/disabling upon a focused tool (see {@link #cfg-tool-id})
* - It can be a toggle button (see {@link #cfg-toggle-enabled}).
*
* Note that a property "controlId" is available on the created button. This string references this controller id, that can be retrieve with {@link Ametys.ribbon.RibbonManager#getUI}
*/
Ext.define(
"Ametys.ribbon.element.ui.ButtonController",
{
extend: "Ametys.ribbon.element.RibbonUIController",
mixins: { common: 'Ametys.ribbon.element.ui.CommonController' },
/**
* @cfg {String} action A function to call when the button is pressed.
* Called with the following parameters:
* @cfg {Ametys.ribbon.element.ui.ButtonController} action.controller This button controller.
* @cfg {Boolean} action.state When the button is a toggle button, the new press-state of the button, null otherwise.
*/
/**
* @readonly
* @property {Boolean} ButtonController True if the instance is a Ametys.ribbon.element.ui.ButtonController instance
*/
isButtonController: true,
/**
* @cfg {Boolean/String} [toggle-enabled=false] When 'true', the button will be a toggle button. {@link #cfg-action} is still called, and the state can be retrieved using #isPressed
*/
/**
* @property {Boolean} _toggleEnabled See ({@link #cfg-toggle-enabled})
* @private
*/
/**
* @cfg {Boolean/String} [toggle-state=false] When 'true', the button is created as pressed. Only available for toggle buttons (#cfg-toggle-enabled)=true.
*/
/**
* @property {Boolean} _pressed The current pressed state for a toggle button ({@link #cfg-toggle-enabled})
* @private
*/
/**
* @cfg {Boolean/String} [sort-menu-items=false] When 'true', the items of a menu will be sorted by alphabetical order.
*/
/**
* @property {Boolean} _sortMenuItems See ({@link #sort-menu-items})
* @private
*/
/**
* @cfg {Boolean/String} [sort-gallery-items=false] When 'true', the items of a gallery into a group will be sorted by alphabetical order.
*/
/**
* @property {Boolean} _sortGalleryItems See ({@link #sort-gallery-items})
* @private
*/
/**
* @property {Boolean/Object} [_refreshing=false] An object determining the refresh state or false if not refreshing
* @property {Boolean} _refreshing.disabled The _disabled member before the refreshing starts
*/
/**
* @property {String[]} _referencedControllerIds The ids of the other controllers referenced by this controller (such as the menu or gallery items)
* @private
*/
/**
* @cfg {Object} ui-config Additionnal configuration object to be passed to UI controls
*/
/**
* @private
* @property {Number} _refreshingCounter the counter on refreshing state.
*/
_refreshingCounter: 0,
/**
* @private
* @property {Number} _nbGalleryItemsPerLine In the gallery, the number of gallery items to display on each line
*/
/**
* @private
* @property {Number} __galleryItemWidth The width in pixels of a gallery item
*/
_galleryItemWidth: 80,
constructor: function(config)
{
this.callParent(arguments);
this._toggleEnabled = this.getInitialConfig("toggle-enabled") == true || this.getInitialConfig("toggle-enabled") == 'true';
this._pressed = this.getInitialConfig("toggle-state") == true || this.getInitialConfig("toggle-state") == 'true';
this._refreshing = false;
this._sortMenuItems = this.getInitialConfig("sort-menu-items") == true || this.getInitialConfig("sort-menu-items") == 'true';
this._sortGalleryItems = this.getInitialConfig("sort-gallery-items") == true || this.getInitialConfig("sort-gallery-items") == 'true';
this._referencedControllerIds = [];
/**
* @cfg {Number/String} nb-gallery-items-per-line The number of item to show by line. When not specified, will be computed automatically.
*/
this._nbGalleryItemsPerLine = this.getInitialConfig("nb-gallery-items-per-line") ? parseInt(this.getInitialConfig("nb-gallery-items-per-line")) : null;
/** @cfg {String/Boolean} display-filter Should the gallery display a filter? empty will kept automatic behavior */
/** @property {Boolean} _displayToolbar See #cfg-display-toolbar */
this._displayToolbar = this.getInitialConfig("display-toolbar") == null ? null : this.getInitialConfig("display-toolbar") == true || this.getInitialConfig("display-toolbar") == 'true';
this._initialize(config);
},
createUI: function(size, colspan)
{
if (this.getLogger().isDebugEnabled())
{
this.getLogger().debug("Creating new UI button for controller " + this.getId() + " in size " + size + " (with colspan " + colspan + ")");
}
var hasGalleryItems = this.getInitialConfig("gallery-item") && this.getInitialConfig("gallery-item")["gallery-groups"].length > 0;
var menuItemsCount = this._getMenuItemsCount();
// FIXME RUNTIME-1539
// if (!hasGalleryItems && menuItemsCount == 1)
// {
// // There is only one menu item => transform menu to one button
// var menuItemCfg = this.getInitialConfig("menu-items");
// for (var i=0; i < menuItemCfg.length; i++)
// {
// var elmt = Ametys.ribbon.RibbonManager.getUI(menuItemCfg[i]);
// if (elmt != null)
// {
// return elmt.createUI (size, colspan);
// }
// }
// }
// else
// {
var menu = this._getMenu();
// Is this a split button, where the action is the one from a 'primary-menu-item-id' ?
var primaryMenuItemId = this.getInitialConfig("primary-menu-item-id");
var menuItemHandler = primaryMenuItemId && Ametys.ribbon.RibbonManager.hasUI(primaryMenuItemId) ? Ametys.ribbon.RibbonManager.getUI(primaryMenuItemId) : this;
var isSplitButton = (this.getInitialConfig("action") != null || menuItemHandler.getInitialConfig("action") != null) && menu;
var element = Ext.create(isSplitButton ? "Ametys.ui.fluent.ribbon.controls.SplitButton" : "Ametys.ui.fluent.ribbon.controls.Button", Ext.apply({
text: this.getInitialConfig("label"),
scale: size,
colspan: colspan,
iconCls: this._getIconCls(),
icon: this._iconGlyph ? null : Ametys.CONTEXT_PATH + (size == 'large' ? this._iconMedium : this._iconSmall),
tooltip: this._getTooltip(),
handler: !this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(menuItemHandler.onPress, menuItemHandler)) : null,
toggleHandler: this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(menuItemHandler.onPress, menuItemHandler)) : null,
// Note that the toggleBackOnPress is called with the original button, while the onPress is called with the menuItemHandler
disabled: this._disabled,
controlId: this.getId(),
enableToggle: this._toggleEnabled,
pressed: this._pressed,
menu: menu
}, this.getInitialConfig('ui-config') || {}));
// }
return element;
},
createMenuItemUI: function ()
{
if (this.getLogger().isDebugEnabled())
{
this.getLogger().debug("Creating new UI menu item for controller " + this.getId());
}
var menu = this._getMenu();
var element = Ext.create(this._toggleEnabled ? "Ext.menu.CheckItem" : "Ext.menu.Item", Ext.apply({
text: this.getInitialConfig("label"),
iconCls: this._getIconCls(),
icon: !this._iconGlyph && this._iconSmall ? Ametys.CONTEXT_PATH + this._iconSmall : null,
tooltip: this._getTooltip(false),
showCheckbox: false,
handler: !this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(this.onPress, this)) : null,
checkHandler: this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(this.onPress, this)) : null,
// Note that the toggleBackOnPress is called with the original button, while the onPress is called with the menuItemHandler
disabled: this._disabled,
controlId: this.getId(),
checked: this._toggleEnabled ? this._pressed : null,
hideOnClick: this.getInitialConfig("hide-on-click") || !this._toggleEnabled,
menu: menu
}, this.getInitialConfig('ui-config') || {}));
return element;
},
createGalleryItemUI: function ()
{
if (this.getLogger().isDebugEnabled())
{
this.getLogger().debug("Creating new UI gallery item for controller " + this.getId());
}
var element = Ext.create("Ametys.ui.fluent.ribbon.controls.Button", Ext.apply({
text: this.getInitialConfig("label"),
tooltip: this._getTooltip(false),
iconCls: this._getIconCls(),
icon: !this._iconGlyph && this._iconMedium ? Ametys.CONTEXT_PATH + this._iconMedium : null,
scale: 'large',
handler: !this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(this.onPress, this)) : null,
toggleHandler: this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(this.onPress, this)) : null,
disabled: this._disabled,
controlId: this.getId(),
enableToggle: this._toggleEnabled,
pressed: this._pressed
}, this.getInitialConfig('ui-config') || {}));
return element;
},
/**
* Get the menu constructed from button configuration. Call #_getMenuPanel to build items from galleries configuration. Call #_getMenuItems to build from items configuration.
* @return {Object/Ext.menu.Menu} A menu configuration or the menu itself. Can be null if there is no menu.
* @protected
*/
_getMenu: function ()
{
var items = [];
var mp = this._getMenuPanel();
if (mp)
{
items.push(mp);
}
var menuItems = this._getMenuItems();
for (var i=0; i < menuItems.length; i++)
{
items.push(menuItems[i]);
}
if (items.length > 0)
{
var menu = new Ext.create("Ext.menu.Menu", {
cls: 'x-fluent-menu',
ui: "ribbon-menu",
items: items
});
menu.on('show', Ext.bind(this._onMenuShowUpdateFilter, this));
menu.on('hide', Ext.bind(this._onMenuHideClearFilter, this));
menu.on('show', Ext.bind(this._onMenuShowComputeNbGalleryItemsPerLineColumnSize, this));
return menu;
}
return null;
},
/**
* Get the menu panels from galleries configuration if exists
* @returns {Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel} The menu panel having menu panels
* @protected
*/
_getMenuPanel: function ()
{
var me = this;
var menuPanels = [];
if (this.getInitialConfig("gallery-item"))
{
var galleryGroupsCfg = this.getInitialConfig("gallery-item")["gallery-groups"];
for (var i=0; i < galleryGroupsCfg.length; i++)
{
var gpItems = [];
var items = galleryGroupsCfg[i].items;
for (var j=0; j < items.length; j++)
{
if (typeof (items[j]) == 'string')
{
var elmt = Ametys.ribbon.RibbonManager.getUI(items[j]);
if (elmt != null)
{
gpItems.push(elmt.addGalleryItemUI());
if (!Ext.Array.contains(this._referencedControllerIds, elmt.getId()))
{
this._referencedControllerIds.push(elmt.getId());
}
}
}
else if (items[j].className)
{
var elmt = Ext.create(items[j].className, Ext.applyIf(items[j].config, {id: this.getId() + '.group-.' + i + '-item' + j, pluginName: this.getPluginName()}));
Ametys.ribbon.RibbonManager.registerUI(elmt);
gpItems.push(elmt.addGalleryItemUI());
if (!Ext.Array.contains(this._referencedControllerIds, elmt.getId()))
{
this._referencedControllerIds.push(elmt.getId());
}
}
}
if (gpItems.length > 0)
{
if (this._sortGalleryItems)
{
gpItems.sort(this._compareGalleryItemText);
}
var menuPanelCfg = Ext.applyIf({
title: galleryGroupsCfg[i].label,
items: gpItems
}, this._getMenuSubPanelConfig());
var menuPanel = Ext.create("Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel", menuPanelCfg);
menuPanels.push(menuPanel);
}
}
}
if (menuPanels.length > 0)
{
return this._createGalleryWrapper(menuPanels);
}
else
{
return null;
}
},
/**
* @private
* Creates the gallery wrapper
* @param {Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel[]} menuPanels The non-null aray of galleries to wrap
* @return {Ext.Panel} The wrapper
*/
_createGalleryWrapper: function(menuPanels)
{
var me = this;
var dockedItems = [];
if (this._displayToolbar !== false)
{
var items = [{
xtype: 'textfield',
flex: 1,
maxWidth: 4 * this._galleryItemWidth,
emptyText: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_RIBBON_SEARCHGALLERY_PLACEHOLDER}}",
listeners: {change: Ext.Function.createBuffered(this._searchFilter, 200, this)}
}, {
// Clear filter
xtype: 'button',
itemId: 'clear',
tooltip: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_RIBBON_SEARCHGALLERY_CLEAR}}",
handler: Ext.bind (this._clearSearchFilter, this),
iconCls: 'a-btn-glyph ametysicon-eraser11',
cls: 'a-btn-light'
}, {
xtype: 'component',
flex: 0.5
}];
items.push({
xtype: 'button',
itemId: 'sizeswitch',
tooltip: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_RIBBON_SIZE_TOGGLE}}",
toggleHandler: Ext.bind (this._toggleSize, this),
enableToggle: true,
iconCls: 'a-btn-glyph ametysicon-list', // Beware, there is an icon switch in the toggleHandler
cls: 'a-btn-light'
});
if (this._sortGalleryItems)
{
// We can only toggle galleries if sort is activated
// indeed, when removing the categories we sort the buttons (if not the toggle is useless)
items.push({
// Toggle categories
xtype: 'button',
itemId: 'categoryswitch',
tooltip: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_RIBBON_CATEGORIES_TOGGLE}}",
toggleHandler: Ext.bind (this._toggleCategories, this),
enableToggle: true,
iconCls: 'a-btn-glyph ametysicon-squares36',
cls: 'a-btn-light'
});
// Categories, can be destroyer. Let's add the 'empty' category
var menuPanelCfg = Ext.applyIf({
title: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_RIBBON_CATEGORIES_ALL}}",
hidden: true
}, this._getMenuSubPanelConfig());
var menuPanel = Ext.create("Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel", menuPanelCfg);
Ext.Array.insert(menuPanels, 0, [menuPanel]);
}
dockedItems.push({
xtype: 'container',
layout: {
type: 'hbox',
align: 'stretch'
},
dock: 'top',
items: items
});
}
var p = Ext.create("Ext.Panel", {
layout: 'anchor',
defaults: {
ui: "ribbon-menu",
anchor: '100%'
},
dockedItems: dockedItems,
flex: 1,
isPanel: true,
isMenuItem: true,
onClick: Ext.emptyFn, // A menu item must have a click listener
width: this._getMenuPanelWidth(),
scrollable: "vertical",
items: menuPanels,
listeners: {
afterrender: function() {
this._wheelListener = this.el.on(
'mousewheel', Ext.bind(me._onGalleryMouseWheel, me, [this], true), me, { destroyable: true }
);
},
destroy: function() {
Ext.destroyMembers(this, '_wheelListener');
}
}
});
return p;
},
/**
* This listener is called on 'keyup' event on filter input field.
* Filters the tree by text input.
* @param {Ext.form.Field} field The field
* @private
*/
_searchFilter: function(field)
{
var value = Ext.data.SortTypes.asNonAccentedUCString(field.getValue());
var oneElement = field.up() // toolbar
.up() // gallery wrapper
.up() // menu
.up(); // menu owner (button/parent menu)
var oldY = oneElement.menu.getY();
Ext.suspendLayouts();
try
{
this._getGalleries(oneElement).each(function(gallery) {
var itemsVisible = 0;
gallery.items.filterBy(function(button) { return button._filterable; }).each(function (button) {
if (Ext.data.SortTypes.asNonAccentedUCString(button.getText()).indexOf(value) == -1)
{
button.hide();
}
else
{
itemsVisible++;
button.show();
}
});
if (itemsVisible > 0)
{
gallery.show();
}
else
{
gallery.hide();
}
});
}
finally
{
Ext.resumeLayouts(true);
// changing stuffs may move the item vertically
oneElement.menu.setY(oldY);
}
},
/**
* Clear the filter search
* @param {Ext.Button} btn The button
* @private
*/
_clearSearchFilter: function(btn)
{
var filter = btn.prev();
filter.reset();
},
/**
* @private
* Switch from big to small size
* @param {Ext.Button} button The pressed button
* @param {boolean} toggleState The new state
*/
_toggleSize: function(button, toggleState)
{
var me = this;
button.setIconCls(toggleState ? "a-btn-glyph ametysicon-grid" : "a-btn-glyph ametysicon-list")
var oneElement = button.up() // toolbar
.up() // gallery wrapper
.up() // menu
.up(); // menu owner (button/parent menu)
var oldY = oneElement.menu.getY();
try
{
Ext.suspendLayouts();
this._getGalleries(oneElement).each(function (category) {
category.items.each(function (button) {
var isVisible = button.isVisible();
if (!isVisible)
{
button.show(); // Changing width on a hidden button is not correctly applied
}
button.setScale(toggleState ? "small" : "large");
button.setWidth(toggleState ? "100%" : me._galleryItemWidth);
button.setTextAlign(toggleState ? "left" : "center")
if (!isVisible)
{
button.hide();
}
});
})
}
finally
{
Ext.resumeLayouts(true);
// changing stuffs may move the item vertically
oneElement.menu.setY(oldY);
}
},
/**
* @private
* Display or hide the galleries categories
* @param {Ext.Button} button The pressed button
* @param {boolean} toggleState The new state
*/
_toggleCategories: function(button, toggleState)
{
var oneElement = button.up() // toolbar
.up() // gallery wrapper
.up() // menu
.up(); // menu owner (button/parent menu)
var oldY = oneElement.menu.getY();
var categories = this._getGalleries(oneElement);
var allCategory = categories.first();
try
{
Ext.suspendLayouts();
if (toggleState)
{
// Remove categories
allCategory.show();
var items = [];
// Get buttons to move (and hide old parents)
categories.each(function (category) {
if (category != allCategory)
{
category.items.each(function (b) {
b._origin = category.getId();
items.push(b);
});
category.hide();
}
});
// Reorder
items.sort(this._compareGalleryItemText);
// Insert
for (var i = 0; i < items.length; i++)
{
allCategory.add(items[i])
}
}
else
{
// Restore categories
allCategory.hide();
allCategory.items.each(function (b) {
var category = Ext.getCmp(b._origin);
if (b.isVisible())
{
category.show();
}
category.add(b);
});
// No need to resort items
}
}
finally
{
Ext.resumeLayouts(true);
// changing stuffs may move the item vertically
oneElement.menu.setY(oldY);
}
},
/**
* When mouse is scrolled in gallery
* @param {Event} e The scroll event
* @param {HTMLElement} target The target of event
* @param {Object} eOpt The options of the event
* @param {Ext.Element} elt The source element
*/
_onGalleryMouseWheel: function(e, target, eOpt, elt)
{
var cmp = Ext.Component.from(e.target),
cmpScroller = cmp.getScrollable && cmp.getScrollable();
// Only stop the event if we are not scrolling a scrollable component
// inside this container.
if (!cmpScroller || (cmpScroller === this.layout.owner.getScrollable()))
{
e.stopEvent();
elt.scrollBy(0, e.getWheelDelta(), true);
}
},
/**
* Comparison function for two gallery items based on item' text.
* @param {Ext.menu.Item} gpItem1 The first item to compare
* @param {Ext.menu.Item} gpItem2 The second item to compare with
* @return {Number}
*/
_compareGalleryItemText: function (gpItem1, gpItem2)
{
var text1 = Ext.data.SortTypes.asNonAccentedUCString (gpItem1.getText() || gpItem1.text || '');
var text2 = Ext.data.SortTypes.asNonAccentedUCString (gpItem2.getText() || gpItem2.text || '');
if (text1 == text2)
{
return 0;
}
else
{
return text1 < text2 ? -1 : 1;
}
},
/**
* Get the configuration of each menu panel from initial configuration if it exists. Called by #_getMenuPanel.
* @return {Object} The menu panel configuration object
* @protected
*/
_getMenuSubPanelConfig: function()
{
return {
defaults: {
width: this._galleryItemWidth
}
};
},
/**
* @private
* Dertermines if the given element is a gallery wrapper
* @param {Ext.Component} element The element to test
* @return {Boolean} true is it a gallery wrapper
*/
_isGalleriesWrapper: function(element)
{
return element.isPanel;
},
/**
* @private
* Dertermines if the given element is a gallery
* @param {Ext.Component} element The element to test
* @return {Boolean} true is it a gallery
*/
_isGallery: function(element)
{
return element.isPanel;
},
/**
* @protected
* @param {Ametys.ui.fluent.ribbon.controls.SplitButton/Ametys.ui.fluent.ribbon.controls.Button/Ext.menu.CheckItem/Ext.menu.Item} [element] To limit to one ui button. When null will loop on every button.
* @returns {Ext.util.MixedCollection} Collection of Ext.Container
*/
_getGalleriesWrappers: function(element)
{
var me = this;
var elements;
if (element)
{
elements = new Ext.util.MixedCollection();
elements.add(element);
}
else
{
elements = this.getUIControls();
}
var galleriesWrappers = new Ext.util.MixedCollection();
elements.each(function(element) {
if (element.getMenu() && element.getMenu().items)
{
element.getMenu().items.filterBy(me._isGalleriesWrapper, me).each(function (galleryWrapper) {
galleriesWrappers.add(galleryWrapper);
});
}
});
return galleriesWrappers;
},
/**
* @protected
* Get the gallery panels
* @param {Ametys.ui.fluent.ribbon.controls.SplitButton/Ametys.ui.fluent.ribbon.controls.Button/Ext.menu.CheckItem/Ext.menu.Item} [element] To limit to one ui button. When null will loop on every button.
* @returns {Ext.util.MixedCollection} Collection of Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel
*/
_getGalleries: function(element)
{
var me = this;
var galleries = new Ext.util.MixedCollection();
this._getGalleriesWrappers(element).each(function(galleryWrapper) {
galleryWrapper.items.filterBy(me._isGallery, me).each(function (gallery) {
galleries.add(gallery);
});
});
return galleries;
},
/**
* @private
* Listener "on menu hide" to clear the filter
* @param {Ext.Component} menu The menu
*/
_onMenuHideClearFilter: function(menu)
{
var oneElement = menu.up();
if (this._displayToolbar === false)
{
return;
}
var galleriesWrappers = this._getGalleriesWrappers(oneElement);
if (galleriesWrappers.length == 0)
{
// No gallery
return;
}
var toolbar = galleriesWrappers.get(0) // only one galleryWrapper when providing an element
.getDockedItems()[0]; // get the top dockbar
if (toolbar.isVisible())
{
this._clearSearchFilter(toolbar.getComponent("clear"));
// Untoggle the button
toolbar.getComponent("sizeswitch").toggle(false);
if (this._sortGalleryItems)
{
// Untoggle the button
toolbar.getComponent("categoryswitch").toggle(false);
}
}
},
/**
* @private
* Listener "on menu show" to show and prepare the filter
* @param {Ext.Component} menu The menu
*/
_onMenuShowUpdateFilter: function(menu)
{
var oneElement = menu.up();
if (this._displayToolbar === false)
{
return;
}
var galleriesWrappers = this._getGalleriesWrappers(oneElement);
if (galleriesWrappers.length == 0)
{
// No gallery
return;
}
var toolbar = galleriesWrappers.get(0) // only one galleryWrapper when providing an element
.getDockedItems()[0]; // get the top dockbar
// For each gallery, how many visible buttons?
var galleriesCount = 0;
var galleriesItemsCount = 0;
var g = this._getGalleries(oneElement);
if (g.getCount() == 0)
{
// No gallery in the wrapper return
toolbar.hide();
return;
}
g.each(function(gallery) {
if (gallery.isVisible())
{
gallery.items.each(function(button) {
button._filterable = false;
if (button.isVisible())
{
button._filterable = true;
galleriesItemsCount ++;
}
});
galleriesCount ++;
}
});
if (this._displayToolbar !== true // Unless display filter is forced
&& galleriesCount <= 2 && galleriesItemsCount <= 10) // few items does not need a filter
{
toolbar.hide();
return;
}
toolbar.show();
var filterInput = toolbar.items.get(0); // The toolbar have the filter input and the clear button
filterInput.focus();
},
/**
* @private
* Listener "on menu show" to compute the gallery size
* @param {Ext.Component} menu The menu
*/
_onMenuShowComputeNbGalleryItemsPerLineColumnSize: function(menu)
{
var oneElement = menu.up();
var me = this;
if (this._nbGalleryItemsPerLine)
{
// The value was fixed. Do not compute
return;
}
if (!oneElement.menu.items)
{
// No menu. Nothing to compute
return;
}
// For each gallery, how many visible buttons?
var galleriesItems = [];
var g = this._getGalleries(oneElement);
if (g.getCount() == 0)
{
// No gallery return
return;
}
g.each(function(gallery) {
if (gallery.isVisible())
{
var nbVisibleItems = gallery.items.filterBy(function(b) { return b.isVisible(); }).getCount();
galleriesItems.push(nbVisibleItems);
}
});
// Min/Max item per lines
var minIconsPerLine = 5;
var maxIconsPerLine;
// Compute max zone in px
var toolZone = Ext.ComponentQuery.query('viewport')[0].items.get(1); // Item #2 of viewport is the tool zone
var shouldFitInWidth = toolZone.getWidth() - 318 /* tip on sides */ - 50 /* to be sure */;
var nbButtonsThatFitInWidth = Math.max(minIconsPerLine, Math.floor((shouldFitInWidth) / this._galleryItemWidth)); // buttons that fit in width... but there is a min
var shouldFitInHeight = toolZone.getHeight() - 10 /* to be sure*/;
// Seek for big sections
var containsBigSection = false;
if (galleriesItems.length == 0)
{
// there is no gallery ?!
maxIconsPerLine = minIconsPerLine;
}
else
{
// By default, to avoid visual gap, the smallest line should not have more than 5 empty spaces
maxIconsPerLine = Math.min(10, Ext.min(galleriesItems) + 5, nbButtonsThatFitInWidth); // Max is between minIconsPerLine and the minimal
if (Ext.max(galleriesItems) / maxIconsPerLine > 3)
{
containsBigSection = true;
// When there is a least a big section, we accept a bigger visual gap
maxIconsPerLine = Math.min(10, Ext.min(galleriesItems) + 7, nbButtonsThatFitInWidth); // Max is between minIconsPerLine and the minimal
}
}
// Compute the minimal height that fit
var minHeight;
for (var j = minIconsPerLine; j <= maxIconsPerLine; j++)
{
var height = computeGalleryHeight(j);
if (!minHeight || height < minHeight.height) // On same height, min area is better
{
minHeight = { height: height, nbItemPerLine: j };
}
if (height < shouldFitInHeight && !containsBigSection)
{
break;
}
}
// UI update
var oldY = oneElement.menu.getY();
var newWidth = this._getMenuPanelWidth(minHeight.nbItemPerLine);
this._getGalleriesWrappers(oneElement).each(function (menuItem) {
menuItem.setWidth(newWidth + 20);
});
// changing width may move the item vertically
oneElement.menu.updateLayout(); // sometimes the height is not correctly updated
oneElement.menu.setY(oldY);
function computeGalleryHeight(nbItemPerLine)
{
var height = 0;
for (var i = 0; i < galleriesItems.length; i++)
{
height += 22; // Header size
height += Math.ceil(galleriesItems[i] / nbItemPerLine) * 78; // Buttons
height += 3; // Footer
}
return height;
}
},
/**
* @private
* Get the width for the galleries
*/
_getMenuPanelWidth: function(nbGalleryItemsPerLine)
{
var num = nbGalleryItemsPerLine || this._nbGalleryItemsPerLine;
return num ? this._galleryItemWidth * num + 4 : null;
},
// TO remove
_getMenuItemsCount: function ()
{
var count = 0;
if (this.getInitialConfig("menu-items"))
{
var menuItemCfg = this.getInitialConfig("menu-items");
for (var i=0; i < menuItemCfg.length; i++)
{
var elmt = Ametys.ribbon.RibbonManager.getUI(menuItemCfg[i]);
if (elmt != null)
{
count++;
}
}
}
return count;
},
/**
* Get the menu items from menu items configuration if it exists
* @returns {Ext.menu.Item[]} The menu items
* @protected
*/
_getMenuItems: function ()
{
var menuItems = [];
if (this.getInitialConfig("menu-items"))
{
var menuItemCfg = this.getInitialConfig("menu-items");
for (var i=0; i < menuItemCfg.length; i++)
{
var elmt = Ametys.ribbon.RibbonManager.getUI(menuItemCfg[i]);
if (elmt != null)
{
menuItems.push(elmt.addMenuItemUI());
if (!Ext.Array.contains(this._referencedControllerIds, elmt.getId()))
{
this._referencedControllerIds.push(elmt.getId());
}
}
}
}
if (this._sortMenuItems)
{
// Sort items by alphabetical order
menuItems.sort(this._compareMenuItemText);
}
return menuItems;
},
/**
* Comparison function for two menu items based on item' text.
* @param {Ext.menu.Item} item1 The first item to compare
* @param {Ext.menu.Item} item2 The second item to compare with
* @return {Number}
*/
_compareMenuItemText: function (item1, item2)
{
var text1 = Ext.data.SortTypes.asNonAccentedUCString (item1.text || '');
var text2 = Ext.data.SortTypes.asNonAccentedUCString (item2.text || '');
if (text1 == text2)
{
return 0;
}
else
{
return text1 < text2 ? -1 : 1;
}
},
/**
* Update the ui controls for images and tooltip
* @protected
*/
_updateUI: function()
{
var me = this;
this.getUIControls().each(function (element) {
if (!me._iconGlyph && element instanceof Ext.button.Button)
{
if (element.scale == 'large')
{
element.setIcon(Ametys.CONTEXT_PATH + me._iconMedium);
}
else
{
element.setIcon(Ametys.CONTEXT_PATH + me._iconSmall);
}
}
else if (me._iconGlyph)
{
element.setIconCls(me._getIconCls())
}
var isNotInRibbon = element.ownerCt instanceof Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel
|| element instanceof Ext.menu.Item;
element.setTooltip(me._getTooltip(!isNotInRibbon));
});
},
/**
* Get the ids of others controllers referenced by this controller (such as the menu or gallery items)
* @return {String[]} the ids of others controllers
*/
getReferencedControllerIds: function ()
{
return this._referencedControllerIds;
},
/**
* @private
* Additionnal handler for toggle buttons only
* @param {Ametys.ui.fluent.ribbon.controls.Button} button The pressed button
* @param {Boolean} state When the button is a toggle button, the new press-state of the button
*/
toggleBackOnPress: function(button, state)
{
// ensure, all UIs are coherent
// At this time, we do not apply the new press-state, the action will have to it by it-self.
// Note, that we will call toggle event on non toggle buttons, because lot's of button forgot to be declared as togglable
this.toggle(this._pressed);
},
/**
* Handler for the button. The default behavior is to call the function defined in #cfg-action
* @param {Ametys.ui.fluent.ribbon.controls.Button} button The pressed button
* @param {Boolean} state When the button is a toggle button, the new press-state of the button
* @protected
*/
onPress: function(button, state)
{
if (this.getLogger().isInfoEnabled())
{
this.getLogger().info("Pressing button " + this.getId() + "");
}
var actionFn = this.getInitialConfig("action");
if (actionFn)
{
if (this.getLogger().isDebugEnabled())
{
this.getLogger().debug("Calling action for button " + this.getId() + ": " + actionFn);
}
Ametys.executeFunctionByName(actionFn, null, null, this, this._toggleEnabled ? state : null);
}
},
/**
* Get the current controller state (pressed or not) for a toggle button controller (#cfg-toggle-enabled is 'true').
* @returns {Boolean} The state
*/
isPressed: function()
{
return this._pressed;
},
/**
* When the button is a toggle button (#cfg-toggle-enabled is 'true') this allow to change all controlled UIs state.
* No event is thrown on the UIs
* @param {Boolean} [state] Change the state to the current value. When not specified the new state is the opposite of the current state.
*/
toggle: function(state)
{
var me = this;
if (state && !this._toggleEnabled && this.getLogger().isWarnEnabled())
{
this.getLogger().warn("Controller '" + this.getId() + "' is using #toggle but is not <toggle-enabled>true</toggle-enabled>")
}
if (Ext.isBoolean(state))
{
this._pressed = state;
}
else
{
this._pressed = !this._pressed;
}
this.getUIControls().each(function (elmt) {
if (elmt instanceof Ext.Button)
{
elmt.enableToggle = true; // Since ExtJS 6.5.1, we force the configuration that should have been declared
elmt.toggle(me._pressed, true);
}
else if (elmt instanceof Ext.menu.CheckItem)
{
elmt.setChecked(me._pressed, true);
}
});
},
/**
* Set all controlled UIs in a refreshing state. See #stopRefreshing
*/
refreshing: function ()
{
this._refreshingCounter++;
var currentDisableState = this._disabled;
this.disable();
this._refreshing = {
disabled: currentDisableState
};
this.getUIControls().each(function (elmt) {
if (elmt instanceof Ext.Button)
{
elmt.refreshing();
}
});
},
/**
* Stop the refreshing state for all controlled UIs. See #refreshing
* @param [force=false] When false, stopRefreshing will only stop if the counter of refreshing is 0 ; when true, put the counter to 0
*/
stopRefreshing: function (force)
{
if (this._refreshingCounter != 0)
{
this._refreshingCounter = force ? 0 : Math.max(0, this._refreshingCounter - 1);
// Do NOT stop refreshing until a refresh is still in progress
if (this._refreshingCounter == 0)
{
this.getUIControls().each(function (elmt) {
if (elmt instanceof Ext.Button)
{
elmt.stopRefreshing();
}
});
var oldDisableState = this._refreshing.disabled;
this._refreshing = false;
this.setDisabled(oldDisableState);
}
}
},
updateState: function(selectionChanged, toolStatusChanged, toolDirtyStateChanged)
{
this.stopRefreshing(true);
this.mixins.common.updateState.apply(this, arguments)
},
disable: function()
{
if (!this._refreshing)
{
this.mixins.common.disable.apply(this);
}
else
{
this._refreshing.disabled = true;
}
},
enable: function()
{
if (!this._refreshing)
{
this.mixins.common.enable.apply(this);
}
else
{
this._refreshing.disabled = false;
}
},
isDisabled: function()
{
if (!this._refreshing)
{
return this.mixins.common.isDisabled.apply(this);
}
else
{
return this._refreshing.disabled;
}
},
/**
* Called to prepare options for a #serverCall
* @param {Object} options See #serverCall for default options. This implementation additionnal have the following properties
* @param {Boolean} [options.refreshing=false] When 'true', the button will automatically call #refreshing and #stopRefreshing before and after the server request.
*/
beforeServerCall: function(options)
{
if (options.refreshing == true)
{
this.refreshing();
}
},
/**
* @inheritDoc
* @private
*/
afterServerCall: function(serverResponse, options)
{
if (options.refreshing == true)
{
this.stopRefreshing();
}
},
/**
* @inheritDoc
* @private
*/
afterCancelledServerCall: function (options)
{
if (options.refreshing == true)
{
this.stopRefreshing();
}
}
}
);