diff --git a/examples/galleryapp/resources.qrc b/examples/galleryapp/resources.qrc --- a/examples/galleryapp/resources.qrc +++ b/examples/galleryapp/resources.qrc @@ -18,6 +18,7 @@ ../gallerydata/contents/ui/gallery/ColorsGallery.qml ../gallerydata/contents/ui/gallery/MetricsGallery.qml ../gallerydata/contents/ui/gallery/LayersGallery.qml + ../gallerydata/contents/ui/gallery/FormLayoutGallery.qml ../gallerydata/contents/ui/ExampleApp.qml ../gallerydata/contents/ui/gallery/ColorSetGallery.qml ../gallerydata/contents/ui/DesktopExampleApp.qml diff --git a/examples/gallerydata/contents/ui/MainPage.qml b/examples/gallerydata/contents/ui/MainPage.qml --- a/examples/gallerydata/contents/ui/MainPage.qml +++ b/examples/gallerydata/contents/ui/MainPage.qml @@ -98,6 +98,10 @@ text: "Text Field" component: "TextField" } + ListElement { + text: "Form Layout" + component: "FormLayout" + } ListElement { text: "Multiple Columns" component: "MultipleColumns" diff --git a/examples/gallerydata/contents/ui/gallery/ButtonGallery.qml b/examples/gallerydata/contents/ui/gallery/ButtonGallery.qml --- a/examples/gallerydata/contents/ui/gallery/ButtonGallery.qml +++ b/examples/gallerydata/contents/ui/gallery/ButtonGallery.qml @@ -145,7 +145,7 @@ spacing: Units.smallSpacing Controls.Button { - text: "Open Bottom drawer" + text: "Open &Bottom drawer" anchors.horizontalCenter: parent.horizontalCenter onClicked: bottomDrawer.open() } diff --git a/examples/gallerydata/contents/ui/gallery/FormLayoutGallery.qml b/examples/gallerydata/contents/ui/gallery/FormLayoutGallery.qml new file mode 100644 --- /dev/null +++ b/examples/gallerydata/contents/ui/gallery/FormLayoutGallery.qml @@ -0,0 +1,95 @@ + + +import QtQuick 2.6 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 +import org.kde.kirigami 2.3 as Kirigami + +Kirigami.ScrollablePage { + title: "Form Layout" + + Kirigami.FormLayout { + id: layout + width: 500 + height: 500 + TextField { + Kirigami.FormData.label: "Label:" + } + TextField { + } + TextField { + Kirigami.FormData.label:"Lo&nger label:" + } + Kirigami.Separator { + Kirigami.FormData.isSection: true + } + TextField { + Kirigami.FormData.label: "After separator:" + } + ComboBox { + Kirigami.FormData.label: "Combo:" + model: ["First", "Second", "Third"] + } + CheckBox { + checked: true + text: "Option" + } + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Section title" + } + TextField { + Kirigami.FormData.label: "Label:" + } + Item { + width:1 + height:1 + Kirigami.FormData.isSection: true + } + TextField { + Kirigami.FormData.label: "Section without line:" + } + TextField { + } + Item { + width:1 + height:1 + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Section with title without line" + } + TextField { + Kirigami.FormData.label: "Title:" + } + ColumnLayout { + Layout.rowSpan: 3 + Kirigami.FormData.label: "Label for radios:" + RadioButton { + checked: true + text: "One" + } + RadioButton { + text: "Two" + } + RadioButton { + text: "Three" + } + } + Button { + text: item ? "Remove Field" : "Add Field" + property TextField item + onClicked: { + if (item) { + item.destroy(); + } else { + item = dyncomponent.createObject(layout); + } + } + Component { + id: dyncomponent + TextField { + Kirigami.FormData.label: "Generated Title:" + } + } + } + } +} diff --git a/examples/gallerydata/contents/ui/gallery/TextFieldGallery.qml b/examples/gallerydata/contents/ui/gallery/TextFieldGallery.qml --- a/examples/gallerydata/contents/ui/gallery/TextFieldGallery.qml +++ b/examples/gallerydata/contents/ui/gallery/TextFieldGallery.qml @@ -20,49 +20,43 @@ import QtQuick 2.0 import QtQuick.Controls 2.0 as Controls import QtQuick.Layouts 1.2 -import org.kde.kirigami 2.2 +import org.kde.kirigami 2.3 as Kirigami -ScrollablePage { +Kirigami.ScrollablePage { id: page Layout.fillWidth: true implicitWidth: applicationWindow().width title: "Text fields" ColumnLayout { - objectName: "pollo" - width: page.width - spacing: Units.smallSpacing - - Controls.Label { - text: "Placeholder text:" - } - Controls.TextField { - placeholderText: "Search..." - Layout.alignment: Qt.AlignHCenter - } - Controls.Label { - text: "Disabled field:" - } - Controls.TextField { - text: "Disabled" - enabled: false - Layout.alignment: Qt.AlignHCenter - } - Controls.Label { - text: "Password:" - } - Controls.TextField { - echoMode: TextInput.Password - Layout.alignment: Qt.AlignHCenter - } - Controls.Label { - text: "Numbers:" - } - Controls.TextField { - inputMask: "99999999" - inputMethodHints: Qt.ImhDigitsOnly + Kirigami.FormLayout { Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + + width: page.width + spacing: Units.smallSpacing + + Controls.TextField { + placeholderText: "Search..." + Kirigami.FormData.label: "Placeholder text:" + } + Controls.TextField { + text: "Disabled" + enabled: false + Kirigami.FormData.label: "Disabled field:" + } + Controls.TextField { + echoMode: TextInput.Password + Kirigami.FormData.label: "Password:" + } + + Controls.TextField { + inputMask: "99999999" + inputMethodHints: Qt.ImhDigitsOnly + Kirigami.FormData.label: "Numbers:" + } } + Controls.Label { text: "Text area:" } @@ -81,8 +75,8 @@ id: field anchors.fill: parent text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent eu nisl ac nibh malesuada pretium ut sit amet libero. Nulla libero arcu, pharetra a dignissim nec, iaculis sit amet metus. Suspendisse quis justo efficitur, pharetra dui maximus, aliquam dolor. Vestibulum vel imperdiet turpis. Mauris ut leo mauris. Praesent ut libero sollicitudin, tincidunt nisi a, efficitur erat. Curabitur lacinia leo et tempor aliquam." - Layout.minimumWidth: Units.gridUnit * 12 - Layout.minimumHeight: Units.gridUnit * 12 + Layout.minimumWidth: Kirigami.Units.gridUnit * 12 + Layout.minimumHeight: Kirigami.Units.gridUnit * 12 wrapMode: Controls.TextArea.WordWrap } } diff --git a/kirigami.qrc b/kirigami.qrc --- a/kirigami.qrc +++ b/kirigami.qrc @@ -44,6 +44,7 @@ src/controls/Label.qml src/controls/BasicListItem.qml src/controls/AbstractApplicationHeader.qml + src/controls/FormLayout.qml src/styles/Plasma/Theme.qml src/styles/Plasma/Units.qml src/styles/Plasma/Icon.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,6 +16,8 @@ enums.cpp desktopicon.cpp settings.cpp + formlayoutattached.cpp + mnemonicattached.cpp ${kirigami_QM_LOADER} ${KIRIGAMI_STATIC_FILES} ) diff --git a/src/controls/FormLayout.qml b/src/controls/FormLayout.qml new file mode 100644 --- /dev/null +++ b/src/controls/FormLayout.qml @@ -0,0 +1,55 @@ +/* + * Copyright 2017 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.6 +import org.kde.kirigami 2.3 as Kirigami + +import "templates" as T + +/** + * This is the base class for Form layouts conforming to the + * Kirigami Human interface guidelines. The layout will + * be divided in two columns: on the right there will be a column + * of fields, on the left their labels specified in the FormData attached + * property. + * + * Example: + * @code + * import org.kde.kirigami 2.3 as Kirigami + * Kirigami.FormLayout { + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * Kirigami.Separator { + * Kirigami.FormData.label: "Section Title" + * Kirigami.FormData.isSection: true + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * TextField { + * } + * } + * @endcode + * @inherits T.FormLayout + * @since 2.3 + */ +T.FormLayout { + +} diff --git a/src/controls/GlobalDrawer.qml b/src/controls/GlobalDrawer.qml --- a/src/controls/GlobalDrawer.qml +++ b/src/controls/GlobalDrawer.qml @@ -17,11 +17,11 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import QtQuick 2.1 +import QtQuick 2.6 import QtQuick.Templates 2.0 as T2 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 -import org.kde.kirigami 2.2 +import org.kde.kirigami 2.3 import "private" import "templates/private" @@ -390,28 +390,48 @@ BasicListItem { + id: backItem visible: level > 0 supportsMouseEvents: true icon: (LayoutMirroring.enabled ? "go-previous-symbolic-rtl" : "go-previous-symbolic") - label: qsTr("Back") + + label: MnemonicData.richTextLabel + MnemonicData.enabled: backItem.enabled && backItem.visible + MnemonicData.controlType: MnemonicData.MenuItem + MnemonicData.label: qsTr("Back") + separatorVisible: false onClicked: stackView.pop() } + Shortcut { + sequence: backItem.MnemonicData.sequence + onActivated: backItem.clicked() + } Repeater { id: actionsRepeater model: actions delegate: BasicListItem { id: listItem supportsMouseEvents: true checked: modelData.checked - icon: modelData.icon - label: modelData.text + + icon: modelData.iconName + + label: MnemonicData.richTextLabel + MnemonicData.enabled: listItem.enabled && listItem.visible + MnemonicData.controlType: MnemonicData.MenuItem + MnemonicData.label: modelData.text + separatorVisible: false visible: model ? model.visible || model.visible===undefined : modelData.visible enabled: model ? model.enabled : modelData.enabled opacity: enabled ? 1.0 : 0.3 Icon { + Shortcut { + sequence: listItem.MnemonicData.sequence + onActivated: listItem.clicked() + } isMask: true anchors { verticalCenter: contentItem.verticalCenter diff --git a/src/controls/Heading.qml b/src/controls/Heading.qml --- a/src/controls/Heading.qml +++ b/src/controls/Heading.qml @@ -65,7 +65,7 @@ lineHeight: 1.2 font.pointSize: headerPointSize(level) - font.weight: Font.Light + font.weight: level <= 4 ? Font.Light : Font.Normal wrapMode: Text.WordWrap opacity: 0.8 diff --git a/src/controls/templates/FormLayout.qml b/src/controls/templates/FormLayout.qml new file mode 100644 --- /dev/null +++ b/src/controls/templates/FormLayout.qml @@ -0,0 +1,217 @@ +/* + * Copyright 2017 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.6 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 +import org.kde.kirigami 2.3 as Kirigami + +/** + * This is the base class for Form layouts conforming to the + * Kirigami Human interface guidelines. The layout will + * be divided in two columns: on the right there will be a column + * of fields, on the left their labels specified in the FormData attached + * property. + * + * Example: + * @code + * import org.kde.kirigami 2.3 as Kirigami + * Kirigami.FormLayout { + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * Kirigami.Separator { + * Kirigami.FormData.label: "Section Title" + * Kirigami.FormData.isSection: true + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * TextField { + * } + * } + * @endcode + * @inherits QtQuick.Controls.Control + * @since 2.3 + */ +Control { + id: root + + /** + * wideMode: bool + * If true the layout will be optimized for a wide screen, such as + * a desktop machine (the labels will be on a left column, + * the fields on a right column beside it), if false (such as on a phone) + * everything is laid out in a single column. + * by default this will be based on wether the application is + * wide enough for the layout of being in such mode. + * It can be overridden by reasigning the property + */ + property bool wideMode: width >= lay.wideImplicitWidth + + implicitWidth: lay.implicitWidth + implicitHeight: lay.implicitHeight + Layout.preferredHeight: lay.implicitHeight + + GridLayout { + id: lay + property int wideImplicitWidth + columns: root.wideMode ? 2 : 1 + rowSpacing: Kirigami.Units.smallSpacing + columnSpacing: Kirigami.Units.smallSpacing + property var knownItems: [] + anchors { + left: parent.left + top: parent.top + right: parent.right + } + + Timer { + id: hintCompression + onTriggered: { + if (root.wideMode) { + lay.wideImplicitWidth = lay.implicitWidth; + } + } + } + onImplicitWidthChanged: hintCompression.restart(); + Component.onCompleted: wideImplicitWidth = lay.implicitWidth; + } + + Item { + id: temp + } + + Timer { + id: relayoutTimer + interval: 0 + onTriggered: { + var __items = children; + //exclude the layout and temp + for (var i = 2; i < __items.length; ++i) { + var item = __items[i]; + + //skip items that are already there + if (lay.knownItems.indexOf(item) != -1 || + //exclude Repeaters + //NOTE: this is an heuristic but there are't better ways + (item.hasOwnProperty("model") && item.model !== undefined && item.children.length == 0)) { + continue; + } + lay.knownItems.push(item); + + var itemContainer = itemComponent.createObject(temp, {"item": item}) + + //if section, label goes after the separator + if (item.Kirigami.FormData.isSection) { + //put an extra spacer + var placeHolder = placeHolderComponent.createObject(lay, {"item": item}); + placeHolder.Layout.colSpan = 2; + itemContainer.parent = lay; + } + + var buddy = buddyComponent.createObject(lay, {"item": item}) + + itemContainer.parent = lay; + //if section, wee need another placeholder + if (item.Kirigami.FormData.isSection) { + var placeHolder = placeHolderComponent.createObject(lay, {"item": item}); + placeHolder.parent = lay; + } + } + } + } + + onChildrenChanged: relayoutTimer.restart(); + + Component.onCompleted: relayoutTimer.triggered(); + Component { + id: itemComponent + Item { + id: container + property var item + enabled: item.enabled + visible: item.visible + implicitWidth: item.implicitWidth + Layout.preferredWidth: item.Layout.preferredWidth > 0 ? item.Layout.preferredWidth : item.implicitWidth + Layout.preferredHeight: item.Layout.preferredHeight > 0 ? item.Layout.preferredHeight : item.implicitHeight + + Layout.alignment: (root.wideMode ? Qt.AlignLeft | Qt.AlignVCenter : Qt.AlignHCenter | Qt.AlignTop) + Layout.fillWidth: item.Kirigami.FormData.isSection + Layout.columnSpan: item.Kirigami.FormData.isSection ? lay.columns : 1 + onItemChanged: { + if (!item) { + container.destroy(); + } + } + onXChanged: item.x = x; + onYChanged: item.y = y; + onWidthChanged: item.width = width; + } + } + Component { + id: placeHolderComponent + Item { + property var item + enabled: item.enabled + visible: item.visible + width: Kirigami.Units.smallSpacing + height: Kirigami.Units.smallSpacing + onItemChanged: { + if (!item) { + labelItem.destroy(); + } + } + } + } + Component { + id: buddyComponent + Kirigami.Heading { + id: labelItem + + property var item + enabled: item.enabled + visible: item.visible + Kirigami.MnemonicData.enabled: item.Kirigami.FormData.buddyFor && item.Kirigami.FormData.buddyFor.activeFocusOnTab + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel + Kirigami.MnemonicData.label: item.Kirigami.FormData.label + text: Kirigami.MnemonicData.richTextLabel + + level: item.Kirigami.FormData.isSection ? 3 : 5 + + Layout.preferredHeight: item.Kirigami.FormData.label.length > 0 ? implicitHeight : Kirigami.Units.smallSpacing + + Layout.alignment: root.wideMode + ? (Qt.AlignRight | (item.Kirigami.FormData.buddyFor.height > height * 2 ? Qt.AlignTop : Qt.AlignVCenter)) + : (Qt.AlignLeft | Qt.AlignBottom) + verticalAlignment: root.wideMode ? Text.AlignVCenter : Text.AlignBottom + + Layout.topMargin: item.Kirigami.FormData.buddyFor.height > implicitHeight * 2 ? Kirigami.Units.smallSpacing/2 : 0 + onItemChanged: { + if (!item) { + labelItem.destroy(); + } + } + Shortcut { + sequence: labelItem.Kirigami.MnemonicData.sequence + onActivated: item.Kirigami.FormData.buddyFor.forceActiveFocus() + } + } + } +} diff --git a/src/formlayoutattached.h b/src/formlayoutattached.h new file mode 100644 --- /dev/null +++ b/src/formlayoutattached.h @@ -0,0 +1,137 @@ +/* +* Copyright (C) 2017 by 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. +*/ + +#ifndef FORMLAYOUTATTACHED_H +#define FORMLAYOUTATTACHED_H + +#include +#include + +class QQuickItem; + +/** + * This attached property contains the information for decorating a FormLayout: + * It contains the text labels of fields and information about sections. + * @code + * import org.kde.kirigami 2.3 as Kirigami + * Kirigami.FormLayout { + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * } + * @endcode + * @since 2.3 + */ +class FormLayoutAttached : public QObject +{ + Q_OBJECT + /** + * The label for a form layout field + */ + Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) + /** + * If true the FormLayout item is a section separator, a section separator + * may have different looks: + * * To make it just a space between two fields, just put an empty item with isSection: + * @code + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * Item { + * Kirigami.FormData.isSection: true + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * @endcode + * + * * To make it a space with a section title: + * @code + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * Item { + * Kirigami.FormData.label: "Section Title" + * Kirigami.FormData.isSection: true + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * @endcode + * + * * To make it a space with a section title and a separator line: + * @code + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * Kirigami.Separator { + * Kirigami.FormData.label: "Section Title" + * Kirigami.FormData.isSection: true + * } + * TextField { + * Kirigami.FormData.label: "Label:" + * } + * @endcode + */ + Q_PROPERTY(bool isSection READ isSection WRITE setIsSection NOTIFY isSectionChanged) + + /** + * The Item the label will be considered a "Buddy" for, + * which will be the parent item the attached property is in. + * A buddy item is useful for instance when the label has a keyboard accelerator, + * which on triggered will be given active keyboard focus to. + */ + Q_PROPERTY(QQuickItem *buddyFor READ buddyFor CONSTANT) + +public: + + explicit FormLayoutAttached(QObject *parent = nullptr); + ~FormLayoutAttached() override; + + void setLabel(const QString &text); + QString label() const; + + QString decoratedLabel() const; + + void setIsSection(bool section); + bool isSection() const; + + QQuickItem *buddyFor() const; + + //QML attached property + static FormLayoutAttached *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + void labelChanged(); + void isSectionChanged(); + +private: + QString m_label; + QString m_actualDecoratedLabel; + QString m_decoratedLabel; + QPointer m_buddyFor; + bool m_isSection = false; +}; + +QML_DECLARE_TYPEINFO(FormLayoutAttached, QML_HAS_ATTACHED_PROPERTIES) + +#endif // FORMLAYOUTATTACHED_H diff --git a/src/formlayoutattached.cpp b/src/formlayoutattached.cpp new file mode 100644 --- /dev/null +++ b/src/formlayoutattached.cpp @@ -0,0 +1,74 @@ +/* +* Copyright (C) 2017 by 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. +*/ + +#include "formlayoutattached.h" +#include +#include + +FormLayoutAttached::FormLayoutAttached(QObject *parent) + : QObject(parent) +{ + m_buddyFor = qobject_cast(parent); +} + +FormLayoutAttached::~FormLayoutAttached() +{ +} + +void FormLayoutAttached::setLabel(const QString &text) +{ + if (m_label == text) { + return; + } + + m_label = text; + emit labelChanged(); +} + +QString FormLayoutAttached::label() const +{ + return m_label; +} + +void FormLayoutAttached::setIsSection(bool section) +{ + if (m_isSection == section) { + return; + } + + m_isSection = section; + emit isSectionChanged(); +} + +bool FormLayoutAttached::isSection() const +{ + return m_isSection; +} + +QQuickItem *FormLayoutAttached::buddyFor() const +{ + return m_buddyFor; +} + +FormLayoutAttached *FormLayoutAttached::qmlAttachedProperties(QObject *object) +{ + return new FormLayoutAttached(object); +} + +#include "moc_formlayoutattached.cpp" diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp --- a/src/kirigamiplugin.cpp +++ b/src/kirigamiplugin.cpp @@ -23,6 +23,8 @@ #include "enums.h" #include "desktopicon.h" #include "settings.h" +#include "formlayoutattached.h" +#include "mnemonicattached.h" #include #include @@ -88,9 +90,9 @@ qmlRegisterUncreatableType(uri, 2, 0, "ApplicationHeaderStyle", "Cannot create objects of type ApplicationHeaderStyle"); + //old legacy retrocompatible Theme qmlRegisterSingletonType(componentUrl(QStringLiteral("Theme.qml")), uri, 2, 0, "Theme"); - //Theme changed from a singleton to an attached property - qmlRegisterUncreatableType(uri, 2, 2, "Theme", "Cannot create objects of type Theme, use it as an attached poperty"); + qmlRegisterSingletonType(componentUrl(QStringLiteral("Units.qml")), uri, 2, 0, "Units"); qmlRegisterType(componentUrl(QStringLiteral("Action.qml")), uri, 2, 0, "Action"); @@ -135,6 +137,15 @@ qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationItem.qml")), uri, 2, 1, "AbstractApplicationItem"); qmlRegisterType(componentUrl(QStringLiteral("ApplicationItem.qml")), uri, 2, 1, "ApplicationItem"); + //2.2 + //Theme changed from a singleton to an attached property + qmlRegisterUncreatableType(uri, 2, 2, "Theme", "Cannot create objects of type Theme, use it as an attached poperty"); + + //2.3 + qmlRegisterType(componentUrl(QStringLiteral("FormLayout.qml")), uri, 2, 3, "FormLayout"); + qmlRegisterUncreatableType(uri, 2, 3, "FormData", "Cannot create objects of type FormData, use it as an attached poperty"); + qmlRegisterUncreatableType(uri, 2, 3, "MnemonicData", "Cannot create objects of type MnemonicData, use it as an attached poperty"); + qmlProtectModule(uri, 2); } diff --git a/src/mnemonicattached.h b/src/mnemonicattached.h new file mode 100644 --- /dev/null +++ b/src/mnemonicattached.h @@ -0,0 +1,169 @@ +/* +* Copyright (C) 2017 by 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. +*/ + +#ifndef MNEMONICATTACHED_H +#define MNEMONICATTACHED_H + +#include +#include +#include + +class QQuickItem; + +/** + * This Attached property is used to calculate automated keyboard sequences + * to trigger actions based upon their text: if an "&" mnemonic is + * used (ie "&Ok"), the system will attempt to assign the desired letter giving + * it priority, otherwise a letter among the ones in the label will be used if + * possible and not conflicting. + * Different kinds of controls will have different priorities in assigning the + * shortcut: for instance the "Ok/Cancel" buttons in a dialog will have priority + * over fields of a FormLayout. + * @see ControlType + * + * Usually the developer shouldn't use this directly as base components + * already use this, but only when implementing a custom graphical Control. + * @since 2.3 + */ +class MnemonicAttached : public QObject +{ + Q_OBJECT + /** + * The label of the control we want to compute a mnemonic for, instance + * "Label:" or "&Ok" + */ + Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) + + /** + * The user-visible final label, which will have the shortcut letter underlined, + * such as "<u>O</u>k" + */ + Q_PROPERTY(QString richTextLabel READ richTextLabel NOTIFY richTextLabelChanged) + + /** + * The label with an "&" mnemonic in the place which will have the shortcut + * assigned, regardless the & wasassigned by the user or automatically generated. + */ + Q_PROPERTY(QString mnemonicLabel READ mnemonicLabel NOTIFY mnemonicLabelChanged) + + /** + * Only if true this mnemonic will be considered for the global assignment + * default: true + */ + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + + /** + * the type of control this mnemonic is attached: different types of controls have different importance and priority for shortcut assignment. + * @see ControlType + */ + Q_PROPERTY(MnemonicAttached::ControlType controlType READ controlType WRITE setControlType NOTIFY controlTypeChanged) + + /** + * The final key sequence assigned, if any: it will be Alt+alphanumeric char + */ + Q_PROPERTY(QKeySequence sequence READ sequence NOTIFY sequenceChanged) + +public: + enum ControlType { + ActionElement, /** pushbuttons, checkboxes etc */ + DialogButton, /** buttons for dialogs */ + MenuItem, /** Menu items */ + FormLabel, /** Buddy label in a FormLayout*/ + SecondaryControl /** Other controls that are considered not much important and low priority for shortcuts */ + }; + Q_ENUM(ControlType) + + explicit MnemonicAttached(QObject *parent = 0); + ~MnemonicAttached(); + + void setLabel(const QString &text); + QString label() const; + + QString richTextLabel() const; + QString mnemonicLabel() const; + + void setEnabled(bool enabled); + bool enabled() const; + + void setControlType(MnemonicAttached::ControlType controlType); + ControlType controlType() const; + + QKeySequence sequence(); + + //QML attached property + static MnemonicAttached *qmlAttachedProperties(QObject *object); + +protected: + bool eventFilter(QObject *watched, QEvent *e); + void updateSequence(); + +Q_SIGNALS: + void labelChanged(); + void enabledChanged(); + void sequenceChanged(); + void richTextLabelChanged(); + void mnemonicLabelChanged(); + void controlTypeChanged(); + +private: + void calculateWeights(); + + //TODO: to have support for DIALOG_BUTTON_EXTRA_WEIGHT etc, a type enum should be exported + enum { + // Additional weight for first character in string + FIRST_CHARACTER_EXTRA_WEIGHT = 50, + // Additional weight for the beginning of a word + WORD_BEGINNING_EXTRA_WEIGHT = 50, + // Additional weight for a 'wanted' accelerator ie string with '&' + WANTED_ACCEL_EXTRA_WEIGHT = 150, + // Default weight for an 'action' widget (ie, pushbuttons) + ACTION_ELEMENT_WEIGHT = 50, + // Additional weight for the dialog buttons (large, we basically never want these reassigned) + DIALOG_BUTTON_EXTRA_WEIGHT = 300, + // Weight for FormLayout labels (low) + FORM_LABEL_WEIGHT = 20, + // Weight for Secondary controls which are considered less important (low) + SECONDARY_CONTROL_WEIGHT = 10, + // Default weight for menu items + MENU_ITEM_WEIGHT = 250 + }; + + //order word letters by weight + int m_weight = 0; + int m_baseWeight = 0; + ControlType m_controlType = SecondaryControl; + QMap m_weights; + + QString m_label; + QString m_actualRichTextLabel; + QString m_richTextLabel; + QString m_mnemonicLabel; + bool m_enabled = true; + + QPointer m_window; + + //global mapping of mnemonics + //TODO: map by QWindow + static QHash s_sequenceToObject; + static QHash s_objectToSequence; +}; + +QML_DECLARE_TYPEINFO(MnemonicAttached, QML_HAS_ATTACHED_PROPERTIES) + +#endif // MnemonicATTACHED_H diff --git a/src/mnemonicattached.cpp b/src/mnemonicattached.cpp new file mode 100644 --- /dev/null +++ b/src/mnemonicattached.cpp @@ -0,0 +1,328 @@ +/* +* Copyright (C) 2017 by 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. +*/ + +#include "mnemonicattached.h" +#include +#include +#include +#include + +QHash MnemonicAttached::s_sequenceToObject = QHash(); +QHash MnemonicAttached::s_objectToSequence = QHash(); + +MnemonicAttached::MnemonicAttached(QObject *parent) + : QObject(parent) +{ + QQuickItem *parentItem = qobject_cast(parent); + if (parentItem) { + if (parentItem->window()) { + m_window = parentItem->window(); + m_window->installEventFilter(this); + } + connect(parentItem, &QQuickItem::windowChanged, this, + [this](QQuickWindow *window) { + if (m_window) { + QWindow *renderWindow = QQuickRenderControl::renderWindowFor(m_window); + if (renderWindow) { + renderWindow->removeEventFilter(this); + } else { + m_window->removeEventFilter(this); + } + } + m_window = window; + if (m_window) { + QWindow *renderWindow = QQuickRenderControl::renderWindowFor(m_window); + //renderWindow means the widget is rendering somewhere else, like a QQuickWidget + if (renderWindow && renderWindow != m_window) { + renderWindow->installEventFilter(this); + } else { + m_window->installEventFilter(this); + } + } + }); + } +} + +MnemonicAttached::~MnemonicAttached() +{ + QHash::iterator i = s_objectToSequence.find(this); + if (i != s_objectToSequence.end()) { + s_sequenceToObject.remove(i.value()); + s_objectToSequence.erase(i); + } +} + +bool MnemonicAttached::eventFilter(QObject *watched, QEvent *e) +{ + Q_UNUSED(watched) + + if (m_richTextLabel.isEmpty()) { + return false; + } + + if (e->type() == QEvent::KeyPress) { + QKeyEvent *ke = static_cast(e); + if (ke->key() == Qt::Key_Alt) { + m_actualRichTextLabel = m_richTextLabel; + emit richTextLabelChanged(); + } + + } else if (e->type() == QEvent::KeyRelease) { + QKeyEvent *ke = static_cast(e); + if (ke->key() == Qt::Key_Alt) { + m_actualRichTextLabel = m_label; + m_actualRichTextLabel.replace(QRegularExpression("\\&[^\\&]"), QString()); + emit richTextLabelChanged(); + } + } + return false; +} + +//Algorythm adapted from KAccelString +void MnemonicAttached::calculateWeights() +{ + m_weights.clear(); + + int pos = 0; + bool start_character = true; + bool wanted_character = false; + + while (pos < m_label.length()) { + QChar c = m_label[pos]; + + // skip non typeable characters + if (!c.isLetterOrNumber()) { + start_character = true; + ++pos; + continue; + } + + int weight = 1; + + // add special weight to first character + if (pos == 0) { + weight += FIRST_CHARACTER_EXTRA_WEIGHT; + } + + // add weight to word beginnings + if (start_character) { + weight += WORD_BEGINNING_EXTRA_WEIGHT; + start_character = false; + } + + // add weight to word beginnings + if (wanted_character) { + weight += WANTED_ACCEL_EXTRA_WEIGHT; + wanted_character = false; + } + + // add decreasing weight to left characters + if (pos < 50) { + weight += (50 - pos); + } + + // try to preserve the wanted accelarators + if (c == "&" && (pos == m_label.length() - 1 || m_label[pos+1] != "&")) { + wanted_character = true; + ++pos; + continue; + } + + while (m_weights.contains(weight)) { + ++weight; + } + + m_weights[weight] = c; + + ++pos; + } + + //update our maximum weight + if (m_weights.isEmpty()) { + m_weight = m_baseWeight; + } else { + m_weight = m_baseWeight + m_weights.keys().last(); + } +} + +void MnemonicAttached::updateSequence() +{ + //forget about old association + QHash::iterator objIt = s_objectToSequence.find(this); + if (objIt != s_objectToSequence.end()) { + s_sequenceToObject.remove(objIt.value()); + s_objectToSequence.erase(objIt); + } + + calculateWeights(); + + const QString text = label(); + + if (!m_enabled) { + m_actualRichTextLabel = text; + m_actualRichTextLabel.replace(QRegularExpression("\\&[^\\&]"), QString()); + //was the label already completely plain text? try to limit signal emission + if (m_mnemonicLabel != m_actualRichTextLabel) { + m_mnemonicLabel = m_actualRichTextLabel; + emit mnemonicLabelChanged(); + emit richTextLabelChanged(); + } + return; + } + + if (m_weights.isEmpty()) { + return; + } + + QMap::const_iterator i = m_weights.constEnd(); + do { + --i; + QChar c = i.value(); + QKeySequence ks("Alt+"%c); + MnemonicAttached *otherMa = s_sequenceToObject.value(ks); + Q_ASSERT(otherMa != this); + if (!otherMa || otherMa->m_weight < m_weight) { + //the old shortcut is less valuable than the current: remove it + if (otherMa) { + s_sequenceToObject.remove(otherMa->sequence()); + s_objectToSequence.remove(otherMa); + } + + s_sequenceToObject[ks] = this; + s_objectToSequence[this] = ks; + m_richTextLabel = text; + m_richTextLabel.replace(QRegularExpression("\\&[^\\&]"), QString()); + m_actualRichTextLabel = m_richTextLabel; + m_mnemonicLabel = m_richTextLabel; + m_mnemonicLabel.replace(c, "&" % c); + m_richTextLabel.replace(QString(c), "" % c % ""); + + //remap the sequence of the previous shortcut + if (otherMa) { + otherMa->updateSequence(); + } + + break; + } + } while (i != m_weights.constBegin()); + + if (s_objectToSequence.contains(this)) { + emit sequenceChanged(); + } else { + m_actualRichTextLabel = text; + m_actualRichTextLabel.replace(QRegularExpression("\\&[^\\&]"), QString()); + m_mnemonicLabel = m_actualRichTextLabel; + } + + emit richTextLabelChanged(); + emit mnemonicLabelChanged(); +} + +void MnemonicAttached::setLabel(const QString &text) +{ + if (m_label == text) { + return; + } + + m_label = text; + updateSequence(); + emit labelChanged(); +} + +QString MnemonicAttached::richTextLabel() const +{ + return !m_actualRichTextLabel.isEmpty() ? m_actualRichTextLabel : m_label; +} + +QString MnemonicAttached::mnemonicLabel() const +{ + return m_mnemonicLabel; +} + +QString MnemonicAttached::label() const +{ + return m_label; +} + +void MnemonicAttached::setEnabled(bool enabled) +{ + if (m_enabled == enabled) { + return; + } + + m_enabled = enabled; + updateSequence(); + emit enabledChanged(); +} + +bool MnemonicAttached::enabled() const +{ + return m_enabled; +} + +void MnemonicAttached::setControlType(MnemonicAttached::ControlType controlType) +{ + if (m_controlType == controlType) { + return; + } + + m_controlType = controlType; + + switch (controlType) { + case ActionElement: + m_baseWeight = ACTION_ELEMENT_WEIGHT; + break; + case DialogButton: + m_baseWeight = DIALOG_BUTTON_EXTRA_WEIGHT; + break; + case MenuItem: + m_baseWeight = MENU_ITEM_WEIGHT; + break; + case FormLabel: + m_baseWeight = FORM_LABEL_WEIGHT; + break; + default: + m_baseWeight = SECONDARY_CONTROL_WEIGHT; + break; + } + //update our maximum weight + if (m_weights.isEmpty()) { + m_weight = m_baseWeight; + } else { + m_weight = m_baseWeight + (m_weights.constEnd() - 1).key(); + } + emit controlTypeChanged(); +} + +MnemonicAttached::ControlType MnemonicAttached::controlType() const +{ + return m_controlType; +} + +QKeySequence MnemonicAttached::sequence() +{ + return s_objectToSequence.value(this); +} + +MnemonicAttached *MnemonicAttached::qmlAttachedProperties(QObject *object) +{ + return new MnemonicAttached(object); +} + +#include "moc_mnemonicattached.cpp"