/* * SPDX-FileCopyrightText: 2020 Arjen Hiemstra * SPDX-FileCopyrightText: 2023 ivan tkachenko * * SPDX-License-Identifier: LGPL-2.0-or-later */ import QtQuick import QtQuick.Controls import QtTest import org.kde.kirigami as Kirigami // TODO: Find a nicer way to handle this import "../src/controls/private" as KirigamiPrivate TestCase { id: testCase name: "ActionToolBarTest" width: 800 height: 400 visible: true when: windowShown // These buttons are required for getting the right metrics. // Since ActionToolBar bases all sizing on button sizes, we need to be able // to verify that layouting does the right thing. property ToolButton iconButton: KirigamiPrivate.PrivateActionToolButton { display: Button.IconOnly action: Kirigami.Action { icon.name: "document-new"; text: "Test Action" } font.pointSize: 10 } property ToolButton textButton: KirigamiPrivate.PrivateActionToolButton { display: Button.TextOnly action: Kirigami.Action { icon.name: "document-new"; text: "Test Action" } font.pointSize: 10 } property ToolButton textIconButton: KirigamiPrivate.PrivateActionToolButton { action: Kirigami.Action { icon.name: "document-new"; text: "Test Action" } font.pointSize: 10 } property TextField textField: TextField { font.pointSize: 10 } Component { id: single Kirigami.ActionToolBar { font.pointSize: 10 actions: [ Kirigami.Action { icon.name: "document-new"; text: "Test Action" } ] } } Component { id: multiple Kirigami.ActionToolBar { font.pointSize: 10 actions: [ Kirigami.Action { icon.name: "document-new"; text: "Test Action" }, Kirigami.Action { icon.name: "document-new"; text: "Test Action" }, Kirigami.Action { icon.name: "document-new"; text: "Test Action" } ] } } Component { id: iconOnly Kirigami.ActionToolBar { display: Button.IconOnly font.pointSize: 10 actions: [ Kirigami.Action { icon.name: "document-new"; text: "Test Action" }, Kirigami.Action { icon.name: "document-new"; text: "Test Action" }, Kirigami.Action { icon.name: "document-new"; text: "Test Action" } ] } } Component { id: qtActions Kirigami.ActionToolBar { font.pointSize: 10 actions: [ Action { icon.name: "document-new"; text: "Test Action" }, Action { icon.name: "document-new"; text: "Test Action" }, Action { icon.name: "document-new"; text: "Test Action" } ] } } Component { id: mixed Kirigami.ActionToolBar { font.pointSize: 10 actions: [ Kirigami.Action { icon.name: "document-new"; text: "Test Action"; displayHint: Kirigami.DisplayHint.IconOnly }, Kirigami.Action { icon.name: "document-new"; text: "Test Action" }, Kirigami.Action { icon.name: "document-new"; text: "Test Action"; displayComponent: TextField { } }, Kirigami.Action { icon.name: "document-new"; text: "Test Action"; displayHint: Kirigami.DisplayHint.AlwaysHide }, Kirigami.Action { icon.name: "document-new"; text: "Test Action"; displayHint: Kirigami.DisplayHint.KeepVisible } ] } } function test_layout_data() { return [ // One action // Full window width, should just display a toolbutton { tag: "single_full", component: single, width: testCase.width, expected: testCase.textIconButton.width }, // Small width, should display the overflow button { tag: "single_min", component: single, width: 50, expected: testCase.iconButton.width }, // Half window width, should display a single toolbutton { tag: "single_half", component: single, width: testCase.width / 2, expected: testCase.textIconButton.width }, // Multiple actions // Full window width, should display as many buttons as there are actions { tag: "multi_full", component: multiple, width: testCase.width, expected: testCase.textIconButton.width * 3 + Kirigami.Units.smallSpacing * 2 }, // Small width, should display just the overflow button { tag: "multi_min", component: multiple, width: 50, expected: testCase.iconButton.width }, // Half window width, should display one action and overflow button { tag: "multi_small", component: multiple, width: testCase.textIconButton.width * 2 + testCase.iconButton.width + Kirigami.Units.smallSpacing * 3, expected: testCase.textIconButton.width * 2 + testCase.iconButton.width + Kirigami.Units.smallSpacing * 2 }, // Multiple actions, display set to icon only // Full window width, should display as many icon-only buttons as there are actions { tag: "icon_full", component: iconOnly, width: testCase.width, expected: testCase.iconButton.width * 3 + Kirigami.Units.smallSpacing * 2 }, // Small width, should display just the overflow button { tag: "icon_min", component: iconOnly, width: 50, expected: testCase.iconButton.width }, // Quarter window width, should display one icon-only button and the overflow button { tag: "icon_small", component: iconOnly, width: testCase.iconButton.width * 4, expected: testCase.iconButton.width * 3 + Kirigami.Units.smallSpacing * 2 }, // QtQuick Controls actions // Full window width, should display as many buttons as there are actions { tag: "qt_full", component: qtActions, width: testCase.width, expected: testCase.textIconButton.width * 3 + Kirigami.Units.smallSpacing * 2 }, // Small width, should display just the overflow button { tag: "qt_min", component: qtActions, width: 50, expected: testCase.iconButton.width }, // Half window width, should display one action and overflow button { tag: "qt_small", component: qtActions, width: testCase.textIconButton.width * 2 + testCase.iconButton.width + Kirigami.Units.smallSpacing * 3, expected: testCase.textIconButton.width * 2 + testCase.iconButton.width + Kirigami.Units.smallSpacing * 2 }, // Mix of different display hints, displayComponent and normal actions. // Full window width, should display everything, but one action is collapsed to icon { tag: "mixed", component: mixed, width: testCase.width, expected: testCase.textIconButton.width * 2 + testCase.iconButton.width * 2 + testCase.textField.width + Kirigami.Units.smallSpacing * 4 } ] } // Test layouting of ActionToolBar // // ActionToolBar has some pretty complex behaviour, which generally boils down to it trying // to fit as many visible actions as possible and placing the hidden ones in an overflow menu. // This test, along with the data above, verifies that that this behaviour is correct. function test_layout(data) { var toolbar = createTemporaryObject(data.component, testCase, {width: data.width}) verify(toolbar) verify(waitForRendering(toolbar)) while (toolbar.visibleWidth == 0) { // The toolbar creates its delegates asynchronously during "idle // time", this means we need to wait for a bit so the toolbar has // the time to do that. As long as it has not finished creation, the // toolbar will have a visibleWidth of 0, so we can use that to // determine when it is done. wait(50) } compare(toolbar.visibleWidth, data.expected) } Component { id: heightMode Kirigami.ActionToolBar { id: heightModeToolBar font.pointSize: 10 property real customHeight: 50 actions: [ Kirigami.Action { displayComponent: Button { objectName: "tall" implicitHeight: heightModeToolBar.customHeight implicitWidth: 50 } }, Kirigami.Action { displayComponent: Button { objectName: "short" implicitHeight: 25 implicitWidth: 50 } } ] } } function getChild(toolbar, objectName) { let c = toolbar.children[0].children for (let i in c) { if (c[i].objectName === objectName) { return c[i] } } return -1 } function test_height() { var toolbar = createTemporaryObject(heightMode, testCase, {width: testCase.width}) verify(toolbar) verify(waitForRendering(toolbar)) while (toolbar.visibleWidth == 0) { // Same as above wait(50) } compare(toolbar.implicitHeight, 50) compare(toolbar.height, 50) toolbar.customHeight = 100 // Changing the delegate height will trigger the internal layout to // relayout, which is done in polish. This is not signaled to the // parent toolbar, so we need to wait on the contentItem here. verify(isPolishScheduled(toolbar.contentItem)) verify(waitForItemPolished(toolbar.contentItem)) // Implicit height changes should propagate to the layout's height as // long as that doesn't have an explicit height set. compare(toolbar.implicitHeight, 100) compare(toolbar.height, 100) // This should be the default, but make sure to set it regardless toolbar.heightMode = Kirigami.ToolBarLayout.ConstrainIfLarger toolbar.height = 50 // Find the actual child items so we can check their properties let t = getChild(toolbar, "tall"); let s = getChild(toolbar, "short"); // waitForItemPolished doesn't wait long enough and waitForRendering // waits too long, so just wait an arbitrary amount of time... wait(50) // ConstrainIfLarger should reduce the height of the first, but not touch the second compare(t.height, 50) compare(t.y, 0) compare(s.height, 25) compare(s.y, 13) // Should be centered and rounded // AlwaysCenter should not touch any item's height, only make sure they are centered toolbar.heightMode = Kirigami.ToolBarLayout.AlwaysCenter wait(50) compare(t.height, 100) compare(t.y, -25) compare(s.height, 25) compare(s.y, 13) // AlwaysFill should make sure each item has the same height as the toolbar toolbar.heightMode = Kirigami.ToolBarLayout.AlwaysFill wait(50) compare(t.height, 50) compare(t.y, 0) compare(s.height, 50) compare(s.y, 0) // Unconstraining the toolbar should reset its height to the maximum // implicit height and set any children to that same value as heightMode // is still AlwaysFill. toolbar.height = undefined wait(50) compare(toolbar.height, 100) compare(t.height, 100) compare(s.height, 100) } Component { id: toolbarComponent Kirigami.ToolBarLayout { fullDelegate: Item {} iconDelegate: Item {} separatorDelegate: Item {} moreButton: Item {} } } Component { id: actionComponent Kirigami.Action {} } function test_dynamicActions() { const toolbar = createTemporaryObject(toolbarComponent, this); verify(toolbar); const actionA = createTemporaryObject(actionComponent, this, { text: "Action A" }); verify(actionA) toolbar.actions.push(actionA); waitForPolish(toolbar); actionA.destroy(); wait(1500); // Let it destroy, and let toolBarLayout's throttle timer expire const actionB = createTemporaryObject(actionComponent, this, { text: "Action B" }); verify(actionB) // shoud not crash toolbar.actions.push(actionB); waitForPolish(toolbar); } function test_duplicateDynamicAction() { const toolbar = createTemporaryObject(toolbarComponent, this); verify(toolbar); const actionA = createTemporaryObject(actionComponent, this, { text: "Action A" }); verify(actionA) toolbar.actions.push(actionA); toolbar.actions.push(actionA); waitForPolish(toolbar); actionA.destroy(); wait(1500); // Let it destroy, and let toolBarLayout's throttle timer expire const actionB = createTemporaryObject(actionComponent, this, { text: "Action B" }); verify(actionB) // shoud not crash toolbar.actions.push(actionB); waitForPolish(toolbar); } }