diff --git a/examples/gallerydata/contents/ui/DesktopExampleApp.qml b/examples/gallerydata/contents/ui/DesktopExampleApp.qml --- a/examples/gallerydata/contents/ui/DesktopExampleApp.qml +++ b/examples/gallerydata/contents/ui/DesktopExampleApp.qml @@ -33,7 +33,7 @@ actions: [ Kirigami.Action { - text: "Top Bar Style" + text: "Top Bar Style..." iconName: "view-list-icons" Kirigami.Action { text: "Auto" @@ -68,7 +68,7 @@ } }, Kirigami.Action { - text: "Top Bar Sizing" + text: "Top Bar Sizing..." iconName: "folder-sync" visible: Kirigami.Settings.isMobile Kirigami.Action { @@ -97,15 +97,39 @@ } }, Kirigami.Action { - text: "Modal Drawer" + text: "Global Drawer Mode..." iconName: "go-next" - checkable: true - checked: globalDrawer.modal - onCheckedChanged: globalDrawer.modal = checked + visible: !Kirigami.Settings.isMobile + Kirigami.Action { + text: "Overlay Drawer" + checked: globalDrawer.modal && !globalDrawer.collapsible + onTriggered: { + globalDrawer.modal = true; + globalDrawer.collapsible = false; + globalDrawer.collapsed = false; + } + } + Kirigami.Action { + text: "Sidebar Drawer" + checked: !globalDrawer.modal && !globalDrawer.collapsible + onTriggered: { + globalDrawer.modal = false; + globalDrawer.collapsible = false; + globalDrawer.collapsed = false; + } + } + Kirigami.Action { + text: "Collapsible Sidebar Drawer" + checked: !globalDrawer.modal && globalDrawer.collapsible + onTriggered: { + globalDrawer.modal = false; + globalDrawer.collapsible = true; + globalDrawer.collapsed = true; + } + } }, Kirigami.Action { text: "Open A Page" - iconName: "view-list-details" checkable: true //Need to do this, otherwise it breaks the bindings property bool current: pageStack.currentItem ? pageStack.currentItem.objectName == "settingsPage" : false @@ -129,10 +153,19 @@ ] Controls.CheckBox { - checked: true - text: "Option 1" + text: "Slow Animations" + onCheckedChanged: { + if (checked) { + Kirigami.Units.longDuration = 2500 + Kirigami.Units.shortDuration = 1500 + } else { + Kirigami.Units.longDuration = 250 + Kirigami.Units.shortDuration = 150 + } + } } Controls.CheckBox { + checked: true text: "Option 2" } Controls.CheckBox { diff --git a/examples/gallerydata/contents/ui/ExampleApp.qml b/examples/gallerydata/contents/ui/ExampleApp.qml --- a/examples/gallerydata/contents/ui/ExampleApp.qml +++ b/examples/gallerydata/contents/ui/ExampleApp.qml @@ -107,7 +107,6 @@ }, Kirigami.Action { text: "Open A Page" - iconName: "view-list-details" checkable: true //Need to do this, otherwise it breaks the bindings property bool current: pageStack.currentItem ? pageStack.currentItem.objectName == "settingsPage" : false diff --git a/src/controls/GlobalDrawer.qml b/src/controls/GlobalDrawer.qml --- a/src/controls/GlobalDrawer.qml +++ b/src/controls/GlobalDrawer.qml @@ -19,6 +19,7 @@ import QtQuick 2.6 import QtQuick.Templates 2.0 as T2 +import QtQuick.Controls 2.0 as QQC2 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import org.kde.kirigami 2.4 @@ -175,6 +176,28 @@ */ property alias topContent: topContent.data + /** + * showContentWhenCollapsed: bool + * If true, when the drawer is collapsed as a sidebar, the content items + * at the bottom will be hidden (default true). + * If you want to keep some items visible and some invisible, set this to + * false and control the visibility/opacity of individual items, + * binded to the collapsed property + * @since 2.5 + */ + property bool hideContentWhenCollapsed: true + + /** + * showContentWhenCollapsed: bool + * If true, when the drawer is collapsed as a sidebar, the top content items + * at the top will be hidden (default true). + * If you want to keep some items visible and some invisible, set this to + * false and control the visibility/opacity of individual items, + * binded to the collapsed property + * @since 2.5 + */ + property bool hideTopContentWhenCollapsed: true + /** * resetMenuOnTriggered: bool * @@ -214,6 +237,7 @@ anchors.fill: parent implicitWidth: Math.min (Units.gridUnit * 20, root.parent.width * 0.8) horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + Flickable { id: mainFlickable contentWidth: width @@ -224,23 +248,76 @@ spacing: 0 height: Math.max(root.height, Layout.minimumHeight) - BannerImage { - id: bannerImage + //TODO: cable visible of bannerimage + Item { + implicitHeight: root.collapsible ? Math.max(collapseButton.height + Units.smallSpacing, bannerImage.Layout.preferredHeight) : bannerImage.Layout.preferredHeight Layout.fillWidth: true - fillMode: Image.PreserveAspectCrop - MouseArea { + BannerImage { + id: bannerImage anchors.fill: parent - onClicked: root.bannerClicked() + opacity: !root.collapsed + fillMode: Image.PreserveAspectCrop + + Behavior on opacity { + OpacityAnimator { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + leftPadding: root.collapsible ? collapseButton.width + Units.smallSpacing*2 : topPadding + MouseArea { + anchors.fill: parent + onClicked: root.bannerClicked() + } + EdgeShadow { + edge: Qt.BottomEdge + visible: bannerImageSource != "" + anchors { + left: parent.left + right: parent.right + bottom: parent.top + } + } } - EdgeShadow { - edge: Qt.BottomEdge - visible: bannerImageSource != "" + PrivateActionToolButton { + id: collapseButton anchors { + top: parent.top left: parent.left - right: parent.right - bottom: parent.top + topMargin: root.collapsed || (root.title.length == 0 && root.titleIcon.length == 0) ? 0 : Units.smallSpacing + Units.iconSizes.large/2 - height/2 + leftMargin: root.collapsed || (root.title.length == 0 && root.titleIcon.length == 0) ? 0 : Units.smallSpacing + Behavior on leftMargin { + NumberAnimation { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + Behavior on topMargin { + NumberAnimation { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + + width: Units.iconSizes.smallMedium + Units.largeSpacing * 2 + height: width + + Behavior on y { + YAnimator { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + + visible: root.collapsible + kirigamiAction: Action { + icon.name: "application-menu" + checkable: true + checked: !root.collapsed + onCheckedChanged: root.collapsed = !checked } } } @@ -255,17 +332,27 @@ Layout.topMargin: root.topPadding Layout.fillWidth: true Layout.fillHeight: true + Layout.preferredHeight: implicitHeight * opacity //NOTE: why this? just Layout.fillWidth: true doesn't seem sufficient //as items are added only after this column creation Layout.minimumWidth: parent.width - root.leftPadding - root.rightPadding - visible: children.length > 0 && childrenRect.height > 0 + visible: children.length > 0 && childrenRect.height > 0 && opacity > 0 + opacity: !root.collapsed || !hideTopContentWhenCollapsed + Behavior on opacity { + //not an animator as is binded + NumberAnimation { + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } } T2.StackView { id: stackView Layout.fillWidth: true Layout.minimumHeight: currentItem ? currentItem.implicitHeight : 0 Layout.maximumHeight: Layout.minimumHeight + property ActionsMenu openSubMenu initialItem: menuComponent //NOTE: it's important those are NumberAnimation and not XAnimators // as while the animation is running the drawer may close, and @@ -310,23 +397,38 @@ //NOTE: why this? just Layout.fillWidth: true doesn't seem sufficient //as items are added only after this column creation Layout.minimumWidth: parent.width - root.leftPadding - root.rightPadding - visible: children.length > 0 + visible: children.length > 0 && (opacity > 0 || mainContentAnimator.running) + opacity: !root.collapsed || !hideContentWhenCollapsed + Behavior on opacity { + OpacityAnimator { + id: mainContentAnimator + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } } Item { Layout.minimumWidth: Units.smallSpacing Layout.minimumHeight: root.bottomPadding } Component { id: menuComponent - ColumnLayout { + + Column { spacing: 0 property alias model: actionsRepeater.model property Action current property int level: 0 Layout.maximumHeight: Layout.minimumHeight + move: Transition { + YAnimator { + duration: Units.longDuration/2 + easing.type: Easing.InOutQuad + } + } BasicListItem { id: backItem @@ -350,20 +452,45 @@ Repeater { id: actionsRepeater model: actions - delegate: BasicListItem { + delegate: + BasicListItem { id: listItem supportsMouseEvents: true - checked: modelData.checked + readonly property bool wideMode: width > height * 2 + checked: modelData.checked || (actionsMenu && actionsMenu.visible) + width: parent.width icon: modelData.iconName - label: MnemonicData.richTextLabel + label: width > height * 2 ? MnemonicData.richTextLabel : "" MnemonicData.enabled: listItem.enabled && listItem.visible MnemonicData.controlType: MnemonicData.MenuItem MnemonicData.label: modelData.text + property ActionsMenu actionsMenu: ActionsMenu { + x: Qt.application.layoutDirection == Qt.RightToLeft ? -width : listItem.width + actions: modelData.children + submenuComponent: Component { + ActionsMenu {} + } + onVisibleChanged: { + if (visible) { + stackView.openSubMenu = listItem.actionsMenu; + } else if (stackView.openSubMenu == listItem.actionsMenu) { + stackView.openSubMenu = null; + } + } + } separatorVisible: false - visible: model ? model.visible || model.visible===undefined : modelData.visible + //TODO: animate the hide by collapse + visible: (model ? model.visible || model.visible===undefined : modelData.visible) && opacity > 0 + opacity: (!root.collapsed || icon.length > 0) + Behavior on opacity { + OpacityAnimator { + duration: Units.longDuration/2 + easing.type: Easing.InOutQuad + } + } enabled: (model && model.enabled != undefined) ? model.enabled : modelData.enabled opacity: enabled ? 1.0 : 0.3 Icon { @@ -374,21 +501,60 @@ isMask: true Layout.alignment: Qt.AlignVCenter Layout.rightMargin: !Settings.isMobile && mainFlickable.contentHeight > mainFlickable.height ? Units.gridUnit : 0 - height: Units.iconSizes.smallMedium + Layout.leftMargin: !root.collapsed ? 0 : parent.width - listItem.width + Layout.preferredHeight: !root.collapsed ? Units.iconSizes.smallMedium : Units.iconSizes.small/2 selected: listItem.checked || listItem.pressed - width: height + Layout.preferredWidth: Layout.preferredHeight source: (LayoutMirroring.enabled ? "go-next-symbolic-rtl" : "go-next-symbolic") visible: modelData.children!==undefined && modelData.children.length > 0 } + data: [ + QQC2.ToolTip { + visible: (modelData.tooltip.length || root.collapsed) && (!actionsMenu || !actionsMenu.visible) && listItem.hovered && text.length > 0 + text: modelData.tooltip.length ? modelData.tooltip : modelData.text + delay: 1000 + timeout: 5000 + y: listItem.height/2 - height/2 + x: Qt.application.layoutDirection == Qt.RightToLeft ? -width : listItem.width + } + ] + onHoveredChanged: { + if (!hovered) { + return; + } + if (stackView.openSubMenu) { + stackView.openSubMenu.visible = false; + + if (!listItem.actionsMenu.hasOwnProperty("count") || listItem.actionsMenu.count>0) { + if (listItem.actionsMenu.hasOwnProperty("popup")) { + listItem.actionsMenu.popup(listItem, listItem.width, 0) + } else { + listItem.actionsMenu.visible = true; + } + } + } + } onClicked: { modelData.trigger(); if (modelData.children!==undefined && modelData.children.length > 0) { - stackView.push(menuComponent, {model: modelData.children, level: level + 1, current: modelData }); + if (root.collapsed) { + //fallbacks needed for Qt 5.9 + if ((!listItem.actionsMenu.hasOwnProperty("count") || listItem.actionsMenu.count>0) && !listItem.actionsMenu.visible) { + stackView.openSubMenu = listItem.actionsMenu; + if (listItem.actionsMenu.hasOwnProperty("popup")) { + listItem.actionsMenu.popup(listItem, listItem.width, 0) + } else { + listItem.actionsMenu.visible = true; + } + } + } else { + stackView.push(menuComponent, {model: modelData.children, level: level + 1, current: modelData }); + } } else if (root.resetMenuOnTriggered) { root.resetMenu(); } - checked = Qt.binding(function() { return modelData.checked }); + checked = Qt.binding(function() { return modelData.checked || (actionsMenu && actionsMenu.visible) }); } } } diff --git a/src/controls/Units.qml b/src/controls/Units.qml --- a/src/controls/Units.qml +++ b/src/controls/Units.qml @@ -55,12 +55,12 @@ * * desktop */ property QtObject iconSizes: QtObject { - property int small: fontMetrics.roundedIconSize(16 * devicePixelRatio) * (Settings.isMobile ? 1.5 : 1) - property int smallMedium: fontMetrics.roundedIconSize(22 * devicePixelRatio) * (Settings.isMobile ? 1.5 : 1) - property int medium: fontMetrics.roundedIconSize(32 * devicePixelRatio) * (Settings.isMobile ? 1.5 : 1) - property int large: fontMetrics.roundedIconSize(48 * devicePixelRatio) * (Settings.isMobile ? 1.5 : 1) - property int huge: fontMetrics.roundedIconSize(64 * devicePixelRatio) * (Settings.isMobile ? 1.5 : 1) - property int enormous: 128 * devicePixelRatio * (Settings.isMobile ? 1.5 : 1) + property int small: fontMetrics.roundedIconSize(16 * devicePixelRatio) + property int smallMedium: fontMetrics.roundedIconSize(22 * devicePixelRatio) + property int medium: fontMetrics.roundedIconSize(32 * devicePixelRatio) + property int large: fontMetrics.roundedIconSize(48 * devicePixelRatio) + property int huge: fontMetrics.roundedIconSize(64 * devicePixelRatio) + property int enormous: 128 * devicePixelRatio } /** diff --git a/src/controls/private/BannerImage.qml b/src/controls/private/BannerImage.qml --- a/src/controls/private/BannerImage.qml +++ b/src/controls/private/BannerImage.qml @@ -43,6 +43,11 @@ */ property int titleAlignment: Qt.AlignTop | Qt.AlignLeft + property int leftPadding: headingIcon.valid ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.largeSpacing + property int topPadding: headingIcon.valid ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.largeSpacing + property int rightPadding: headingIcon.valid ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.largeSpacing + property int bottomPadding: headingIcon.valid ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.largeSpacing + Layout.fillWidth: true Layout.preferredWidth: title.implicitWidth @@ -85,7 +90,10 @@ horizontalCenter: root.titleAlignment & Qt.AlignHCenter ? parent.horizontalCenter : undefined verticalCenter: root.titleAlignment & Qt.AlignVCenter ? parent.verticalCenter : undefined - margins: headingIcon.valid ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.largeSpacing + leftMargin: root.leftPadding + topMargin: root.topPadding + rightMargin: root.rightPadding + bottomMargin: root.bottomPadding } width: Math.min(implicitWidth, parent.width) Kirigami.Icon { diff --git a/src/controls/private/PrivateActionToolButton.qml b/src/controls/private/PrivateActionToolButton.qml --- a/src/controls/private/PrivateActionToolButton.qml +++ b/src/controls/private/PrivateActionToolButton.qml @@ -73,7 +73,7 @@ contentItem: MouseArea { hoverEnabled: true onPressed: mouse.accepted = false - Theme.colorSet: checked && (!control.flat && control.kirigamiAction && control.kirigamiAction.icon.color.a) ? Theme.Selection : Theme.Button + Theme.colorSet: checked && (!control.flat && control.kirigamiAction && control.kirigamiAction.icon.color.a) ? Theme.Selection : control.Theme.colorSet Theme.inherit: control.kirigamiAction && Theme.colorSet != Theme.Selection && control.kirigamiAction.icon.color.a == 0 RowLayout { id: layout diff --git a/src/controls/templates/OverlayDrawer.qml b/src/controls/templates/OverlayDrawer.qml --- a/src/controls/templates/OverlayDrawer.qml +++ b/src/controls/templates/OverlayDrawer.qml @@ -62,6 +62,32 @@ */ readonly property bool animating : enterAnimation.animating || exitAnimation.animating || positionResetAnim.running + /** + * collapsible: Bool + * When true, the drawer can be collapsed to a very thin, usually icon only sidebar. + * Only modal drawers are collapsible. + * Collapsible is not supported in Mobile mode + * @since 2.5 + */ + property bool collapsible: false + + /** + * collapsed: bool + * When true, the drawer will be collapsed to a very thin sidebar, + * usually icon only. + * Only collapsible drawers can be collapsed + */ + property bool collapsed: false + + /** + * collapsedSize: int + * When collapsed, the drawer will be resized to this size + * (which may be width for vertical drawers or height for + * horizontal drawers). + * By default it's just enough to accomodate medium sized icons + */ + property int collapsedSize: Units.iconSizes.medium + Units.smallSpacing * 2 + /** * A grouped property describing an optional icon. * * source: The source of the icon, a freedesktop-compatible icon name is recommended. @@ -234,7 +260,7 @@ } } - Theme.colorSet: Theme.View + Theme.colorSet: modal ? Theme.View : Theme.Window Theme.onColorSetChanged: { contentItem.Theme.colorSet = Theme.colorSet background.Theme.colorSet = Theme.colorSet @@ -250,15 +276,11 @@ bottomPadding: Units.smallSpacing parent: modal ? T2.ApplicationWindow.overlay : T2.ApplicationWindow.contentItem - height: edge == Qt.LeftEdge || edge == Qt.RightEdge ? applicationWindow().height : Math.min(contentItem.implicitHeight, Math.round(applicationWindow().height*0.8)) - width: edge == Qt.TopEdge || edge == Qt.BottomEdge ? applicationWindow().width : Math.min(contentItem.implicitWidth, Math.round(applicationWindow().width*0.8)) + edge: Qt.LeftEdge modal: true dragMargin: enabled && (edge == Qt.LeftEdge || edge == Qt.RightEdge) ? Qt.styleHints.startDragDistance : 0 - - implicitWidth: Math.max(background ? background.implicitWidth : 0, contentWidth + leftPadding + rightPadding) - implicitHeight: Math.max(background ? background.implicitHeight : 0, contentHeight + topPadding + bottomPadding) contentWidth: contentItem.implicitWidth || (contentChildren.length === 1 ? contentChildren[0].implicitWidth : 0) contentHeight: contentItem.implicitHeight || (contentChildren.length === 1 ? contentChildren[0].implicitHeight : 0) @@ -308,6 +330,24 @@ //BEGIN signal handlers + onCollapsedChanged: { + if ((!collapsible || modal) && collapsed) { + collapsed = true; + } + } + onCollapsibleChanged: { + if (!collapsible) { + collapsed = false; + } else if (modal) { + collapsible = false; + } + } + onModalChanged: { + if (modal) { + collapsible = false; + } + } + onPositionChanged: { if (peeking) { visible = true @@ -369,5 +409,35 @@ property: "position" duration: (root.position)*Units.longDuration } + readonly property Item statesItem: Item { + states: [ + State { + when: root.collapsed + PropertyChanges { + target: root + implicitWidth: edge == Qt.TopEdge || edge == Qt.BottomEdge ? applicationWindow().width : Math.min(collapsedSize, Math.round(applicationWindow().width*0.8)) + + implicitHeight: edge == Qt.LeftEdge || edge == Qt.RightEdge ? applicationWindow().height : Math.min(collapsedSize, Math.round(applicationWindow().height*0.8)) + } + }, + State { + when: !root.collapsed + PropertyChanges { + target: root + implicitWidth: edge == Qt.TopEdge || edge == Qt.BottomEdge ? applicationWindow().width : Math.min(contentItem.implicitWidth, Math.round(applicationWindow().width*0.8)) + + implicitHeight: edge == Qt.LeftEdge || edge == Qt.RightEdge ? applicationWindow().height : Math.min(contentItem.implicitHeight, Math.round(applicationWindow().height*0.8)) + } + } + ] + transitions: Transition { + reversible: true + NumberAnimation { + properties: "implicitWidth,implicitHeight" + duration: Units.longDuration + easing.type: Easing.InOutQuad + } + } + } } }