diff --git a/src/controls/Action.qml b/src/controls/Action.qml index 44f9419b..66adebff 100644 --- a/src/controls/Action.qml +++ b/src/controls/Action.qml @@ -1,177 +1,185 @@ /* * Copyright 2016 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.7 import QtQuick.Controls 2.4 as Controls import "private" /** * An item that represents an abstract Action * * @inherit QtQuick.Controls.Action */ Controls.Action { id: root /** * Hints for implementations using Actions indicating preferences about how to display the action. */ enum DisplayHint { /** * Indicates there is no specific preference. */ NoPreference = 0, /** * Only display an icon for this Action. */ IconOnly = 1, /** * Try to keep the action visible even when space constrained. * Mutually exclusive with AlwaysHide, KeepVisible has priority. */ KeepVisible = 2, /** * If possible, hide the action in an overflow menu or similar location. * Mutually exclusive with KeepVisible, KeepVisible has priority. */ AlwaysHide = 4, /** * When this action has children, do not display any indicator (like a * menu arrow) for this action. */ HideChildIndicator = 8 } /** * visible: bool * True (default) when the graphic representation of the action * is supposed to be visible. * It's up to the action representation to honor this property. */ property bool visible: true /** * iconName: string * Sets the icon name for the action. This will pick the icon with the given name from the current theme. */ property alias iconName: root.icon.name /** * iconSource: string * Sets the icon file or resource url for the action. Defaults to the empty URL. Use this if you want a specific file rather than an icon from the theme */ property alias iconSource: root.icon.source /** * A tooltip text to be shown when hovering the control bound to this action. Not all controls support tooltips on all platforms */ property string tooltip /** * children: list * A list of children actions. * Useful for tree-like menus * @code * Action { * text: "Tools" * Action { * text: "Action1" * } * Action { * text: "Action2" * } * } * @endcode */ /** * separator: bool * Whether the action is is a separator action; defaults to false. */ property bool separator: false /** * expandible: bool * When true, actions in globalDrawers and contextDrawers will become titles displaying te child actions as sub items * @since 2.6 */ property bool expandible: false property Controls.Action parent /** * displayHint: int * * A combination of values from the Action.DisplayHint enum. These are provided to implementations to indicate * a preference for certain display styles. The default is DisplayHint.NoPreference. * * Note that these are only preferences, implementations may choose to disregard them. * * @since 2.12 */ property int displayHint: Action.DisplayHint.NoPreference /** * Helper function to check if a certain display hint has been set. * * This function is mostly convenience to enforce the mutual exclusivity of KeepVisible and AlwaysHide. * * @param hint The display hint to check if it is set. * * @return true if the hint was set for this action, false if not. * * @since 2.12 */ function displayHintSet(hint) { if (hint === Action.DisplayHint.AlwaysHide && (displayHint & Action.DisplayHint.KeepVisible)) { return false; } return (displayHint & hint) } + /** + * A Component that should be preferred when displaying this Action. + * + * @since 5.65 + * @since 2.12 + */ + property Component displayComponent: null + default property alias children: root.__children property list __children onChildrenChanged: { var child; for (var i in children) { child = children[i]; if (child.hasOwnProperty("parent")) { child.parent = root } } } /** * visibleChildren: list * All child actions that are visible */ readonly property var visibleChildren: { var visible = []; var child; for (var i in children) { child = children[i]; if (!child.hasOwnProperty("visible") || child.visible) { visible.push(child) } } return visible; } } diff --git a/src/controls/ActionToolBar.qml b/src/controls/ActionToolBar.qml index 841090f3..3e7a0d01 100644 --- a/src/controls/ActionToolBar.qml +++ b/src/controls/ActionToolBar.qml @@ -1,169 +1,213 @@ /* * Copyright 2018 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.7 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.4 as Controls import org.kde.kirigami 2.5 as Kirigami import "private" /** * This is a simple toolbar built out of a list of actions * each action is represented by a ToolButton, those that won't fit * the size will go in a menu under a button with the overflow ... icon * * @inherits Item * @since 2.5 */ Item { id: root /** * actions: list * if the card should provide clickable actions, put them in this property, * they will be put in the footer as a list of ToolButtons plus an optional * overflow menu, when not all of them will fit in the available Card width. */ property list actions /** * actions: hiddenActions * This list of actions is for those you always want in the menu, even if there * is enough space. * @since 2.6 */ property list hiddenActions /** * flat: bool * Wether we want our buttons to have a flat appearance. Default: true */ property bool flat: true /** * display: enum * This controls the label position regarding the icon, is the same value to control individual Button components, * permitted values are: * * Button.IconOnly * * Button.TextOnly * * Button.TextBesideIcon * * Button.TextUnderIcon */ property int display: Controls.Button.TextBesideIcon property int alignment: Qt.AlignLeft /** * position enum * This property holds the position of the toolbar. * if this ActionToolBar is the contentItem of a QQC2 Toolbar, the position is binded to the ToolBar's position * * permitted values are: * *ToolBar.Header: The toolbar is at the top, as a window or page header. * *ToolBar.Footer: The toolbar is at the bottom, as a window or page footer. */ property int position: parent && parent.hasOwnProperty("position") ? parent.position : Controls.ToolBar.Header /** * The maximum width of the contents of this ToolBar. If the toolbar's width is larger than this value, empty space will * be added on the sides, according to the Alignment property. * * The value of this property is derived from the ToolBar's actions and their properties. */ readonly property alias maximumContentWidth: details.maximumWidth implicitHeight: actionsLayout.implicitHeight implicitWidth: actionsLayout.implicitWidth Layout.minimumWidth: moreButton.visible ? moreButton.implicitWidth : 0 RowLayout { id: actionsLayout anchors.fill: parent spacing: 0 Item { Layout.fillWidth: root.alignment == Qt.AlignRight || root.alignment == Qt.AlignHCenter || root.alignment == Qt.AlignCenter; Layout.fillHeight: true } Repeater { model: root.actions - delegate: PrivateActionToolButton { - id: actionDelegate + delegate: Loader { + id: delegate Layout.alignment: Qt.AlignVCenter - Layout.minimumWidth: implicitWidth // Use leftMargin instead of spacing on the layout to prevent spacer items // from creating useless spacing, only for items that are actually next to // other items. Layout.leftMargin: index > 0 ? Kirigami.Units.smallSpacing : 0 + Layout.fillWidth: item ? item.Layout.fillWidth : false + Layout.minimumWidth: item ? item.Layout.minimumWidth : implicitWidth + Layout.preferredWidth: item ? item.Layout.preferredWidth : implicitWidth + Layout.maximumWidth: item ? item.Layout.maximumWidth : -1 + + property var kirigamiAction: modelData + + sourceComponent: { + if (modelData.displayComponent && !modelData.displayHintSet(Action.DisplayHint.IconOnly)) { + return modelData.displayComponent + } + return toolButtonDelegate + } visible: details.visibleActions.indexOf(modelData) != -1 && (modelData.visible === undefined || modelData.visible) - flat: root.flat && !modelData.icon.color.a - display: details.iconOnlyActions.indexOf(modelData) != -1 ? Controls.Button.IconOnly : root.display - kirigamiAction: modelData + onLoaded: { + if (sourceComponent == toolButtonDelegate) { + item.kirigamiAction = modelData + } + } } } Item { Layout.fillWidth: root.alignment == Qt.AlignLeft || root.alignment == Qt.AlignHCenter || root.alignment == Qt.AlignCenter; Layout.fillHeight: true } PrivateActionToolButton { id: moreButton Layout.alignment: Qt.AlignRight visible: root.hiddenActions.length > 0 || details.hiddenActions.length > 0 kirigamiAction: Kirigami.Action { icon.name: "overflow-menu" displayHint: Kirigami.Action.DisplayHint.IconOnly | Kirigami.Action.DisplayHint.HideChildIndicator children: Array.prototype.map.call(root.actions, function (i) { return i }).concat(Array.prototype.map.call(hiddenActions, function (i) { return i })) } menu.submenuComponent: ActionsMenu { Binding { target: parentItem property: "visible" value: details.visibleActions.indexOf(parentAction) == -1 && (parentAction.visible === undefined || parentAction.visible) } } menu.itemDelegate: ActionMenuItem { visible: details.visibleActions.indexOf(action) == -1 && (action.visible === undefined || action.visible) } + + menu.loaderDelegate: Loader { + property var kirigamiAction + height: visible ? implicitHeight : 0 + visible: details.visibleActions.indexOf(kirigamiAction) == -1 && + (kirigamiAction.visible === undefined || kirigamiAction.visible) + } } } ActionToolBarLayoutDetails { id: details anchors.fill: parent actions: root.actions rightPadding: moreButton.width + Kirigami.Units.smallSpacing flat: root.flat display: root.display } + + Component { + id: toolButtonDelegate + + PrivateActionToolButton { + id: button + flat: root.flat && !kirigamiAction.icon.color.a + display: details.iconOnlyActions.indexOf(kirigamiAction) != -1 ? Controls.Button.IconOnly : root.display + + menu.actions: { + if (kirigamiAction.displayComponent && kirigamiAction.displayHintSet(Kirigami.Action.DisplayHint.IconOnly)) { + kirigamiAction.displayHint |= Kirigami.Action.DisplayHint.HideChildIndicator + return [kirigamiAction] + } + + if (kirigamiAction.children) { + return kirigamiAction.children + } + + return [] + } + } + } } diff --git a/src/controls/private/ActionToolBarLayoutDetails.qml b/src/controls/private/ActionToolBarLayoutDetails.qml index 511eb567..655d03cd 100644 --- a/src/controls/private/ActionToolBarLayoutDetails.qml +++ b/src/controls/private/ActionToolBarLayoutDetails.qml @@ -1,149 +1,173 @@ /* * Copyright 2018 Marco Martin * Copyright 2019 Arjen Hiemstra * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.7 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.4 as Controls import org.kde.kirigami 2.5 as Kirigami /** * This fairly complex thing determines the layout of ActionToolBar, that is, * which actions should be displayed in full width toolbutton form, which should * be displayed in an icon-only reduced size and which should be placed in the * overflow menu. * * It makes use of two fairly static layouts, one contains all actions in expanded * full-size form, the other in reduced icon-only form. The items in these layouts * determine if they should be visible based on their relative position and size * and some properties of the action. The update function then goes through these * items, adding the actions to the relevant lists, so they can be used by the * ActionToolBar to determine which action should be visible in what state. * * The reason for using two separate layouts from ActionToolBar's main layout is * so that we can use the actual geometry of the buttons to calculate the * visibility, completely disregarding any other layouting quirks. We can then * use that information so only the relevant things in the ActionToolBar are * visible. This allows the main ActionToolBar to use normal layout features for * things like the positioning of the visible actions. */ Item { id: details property var actions property var visibleActions: [] property var hiddenActions: [] property var iconOnlyActions: [] property bool flat: false property int display: Controls.Button.TextBesideIcon property real spacing: Kirigami.Units.smallSpacing property real leftPadding: 0 property real rightPadding: 0 property real iconOnlyWidth: 0 readonly property real iconLayoutWidth: width - rightPadding readonly property real fullLayoutWidth: iconLayoutWidth - iconOnlyWidth readonly property real maximumWidth: fullSizePlaceholderLayout.width + leftPadding + rightPadding enabled: false opacity: 0 // Cannot use visible: false because then relayout doesn't happen correctly function update() { var visible = [] var hidden = [] var iconOnly = [] var iconsWidth = 0 - if (details.width >= fullSizePlaceholderLayout.width + details.rightPadding) { - visibleActions = Array.prototype.map.call(details.actions, function(i) { return i }) - hiddenActions = [] - iconOnlyActions = [] - iconOnlyWidth = 0 - return - } - for (var i = 0; i < root.actions.length; ++i) { var item = fullSizePlaceholderRepeater.itemAt(i) var iconOnlyItem = iconOnlyPlaceholderRepeater.itemAt(i) if (item.actionVisible) { visible.push(item.kirigamiAction) } else if (iconOnlyItem.actionVisible) { visible.push(item.kirigamiAction) iconOnly.push(item.kirigamiAction) iconsWidth += iconOnlyItem.width + details.spacing } else { hidden.push(item.kirigamiAction) } } visibleActions = visible hiddenActions = hidden iconOnlyActions = iconOnly iconOnlyWidth = iconsWidth } onWidthChanged: Qt.callLater(update) Component.onCompleted: Qt.callLater(update) RowLayout { id: fullSizePlaceholderLayout spacing: details.spacing // This binding is here to take care of things like visibility changes onWidthChanged: Qt.callLater(details.update) Repeater { id: fullSizePlaceholderRepeater model: details.actions - PrivateActionToolButton { - flat: details.flat && !modelData.icon.color.a - display: details.display + Loader { + property var kirigamiAction: modelData + + sourceComponent: { + if (modelData.displayComponent && !modelData.displayHintSet(Kirigami.Action.DisplayHint.IconOnly)) { + return modelData.displayComponent + } + return toolButtonDelegate + } + visible: (modelData.visible === undefined || modelData.visible) - && (modelData.displayHint !== undefined && !modelData.displayHintSet(Kirigami.Action.DisplayHint.AlwaysHide)) - kirigamiAction: modelData + && (modelData.displayHint !== undefined && !modelData.displayHintSet(Kirigami.Action.DisplayHint.AlwaysHide)) + property bool actionVisible: visible && (x + width < details.fullLayoutWidth) + + onLoaded: { + if (sourceComponent == toolButtonDelegate) { + item.kirigamiAction = modelData + } + } + } + } + } + + Component { + id: toolButtonDelegate + PrivateActionToolButton { + flat: details.flat && !kirigamiAction.icon.color.a + display: details.display + menu.actions: { + if (kirigamiAction.displayComponent && kirigamiAction.displayHintSet(Kirigami.Action.DisplayHint.IconOnly)) { + return [kirigamiAction] + } + + if (kirigamiAction.children) { + return kirigamiAction.children + } + + return [] } } } RowLayout { id: iconOnlyPlaceholderLayout spacing: details.spacing Repeater { id: iconOnlyPlaceholderRepeater model: details.actions PrivateActionToolButton { flat: details.flat && !modelData.icon.color.a display: Controls.Button.IconOnly visible: (modelData.visible === undefined || modelData.visible) && (modelData.displayHint !== undefined && modelData.displayHintSet(Kirigami.Action.DisplayHint.KeepVisible)) kirigamiAction: modelData property bool actionVisible: visible && (iconOnlyPlaceholderRepeater.count === 1 || (x + width < details.iconLayoutWidth)) } } } } diff --git a/src/controls/private/ActionsMenu.qml b/src/controls/private/ActionsMenu.qml index 6210f59e..670d26b5 100644 --- a/src/controls/private/ActionsMenu.qml +++ b/src/controls/private/ActionsMenu.qml @@ -1,77 +1,82 @@ /* * Copyright 2018 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.3 import QtQuick.Controls 2.4 as Controls import org.kde.kirigami 2.4 as Kirigami Controls.Menu { id: theMenu z: 999999999 property alias actions: actionsInstantiator.model property Component submenuComponent //renamed to work on both Qt 5.9 and 5.10 property Component itemDelegate: ActionMenuItem {} property Component separatorDelegate: Controls.MenuSeparator { } + property Component loaderDelegate: Loader { property var kirigamiAction } property Controls.Action parentAction property Controls.MenuItem parentItem Item { id: invisibleItems visible: false } Instantiator { id: actionsInstantiator active: theMenu.visible delegate: QtObject { readonly property Controls.Action action: modelData property QtObject item: null function create() { if (!action.hasOwnProperty("children") && !action.children || action.children.length === 0) { if (action.hasOwnProperty("separator") && action.separator) { item = theMenu.separatorDelegate.createObject(null, {}); } + else if (action.displayComponent) { + item = theMenu.loaderDelegate.createObject(null, + { kirigamiAction: action, sourceComponent: action.displayComponent }); + } else { item = theMenu.itemDelegate.createObject(null, { action: action }); } theMenu.addItem(item) } else if (theMenu.submenuComponent) { item = theMenu.submenuComponent.createObject(null, { parentAction: action, title: action.text, actions: action.children }); theMenu.insertMenu(theMenu.count, item) item.parentItem = theMenu.contentData[theMenu.contentData.length-1] item.parentItem.icon = action.icon } } function remove() { if (!action.hasOwnProperty("children") && !action.children || action.children.length === 0) { theMenu.removeItem(item) } else if (theMenu.submenuComponent) { theMenu.removeMenu(item) } } } onObjectAdded: object.create() onObjectRemoved: object.remove() } }