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 @@ -99,6 +99,10 @@ 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 var 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,27 @@ +/* + * 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 + +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,13 +390,23 @@ 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 @@ -406,12 +416,21 @@ supportsMouseEvents: true checked: modelData.checked icon: modelData.iconName - label: modelData.text + + 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/private/PrivateActionToolButton.qml b/src/controls/private/PrivateActionToolButton.qml --- a/src/controls/private/PrivateActionToolButton.qml +++ b/src/controls/private/PrivateActionToolButton.qml @@ -60,7 +60,7 @@ source: control.action ? control.action.iconName : "" visible: control.action && control.action.iconName != "" } - Label { + Controls.Label { text: action ? action.text : "" visible: control.showText } 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,177 @@ +/* + * 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 + +Control { + id: root + implicitWidth: lay.implicitWidth + implicitHeight: lay.implicitHeight + Layout.preferredHeight: lay.implicitHeight + property bool wideMode: width >= lay.wideImplicitWidth + + 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.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: childrenChanged() + Component { + id: itemComponent + Item { + id: container + property var item + enabled: item.enabled + visible: item.visible + implicitWidth: item.implicitWidth + Layout.preferredWidth: item.Layout.preferredWidth + Layout.preferredHeight: Math.max(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,69 @@ +/* +* 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; + +class FormLayoutAttached : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) + Q_PROPERTY(bool isSection READ isSection WRITE setIsSection NOTIFY isSectionChanged) + Q_PROPERTY(QQuickItem *buddyFor READ buddyFor CONSTANT) + +public: + + explicit FormLayoutAttached(QObject *parent = 0); + ~FormLayoutAttached(); + + 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(); + void mnemonicChanged(); + void decoratedLabelChanged(); + +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,75 @@ +/* +* 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); + qApp->installEventFilter(this); +} + +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,125 @@ +/* +* 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; + +class MnemonicAttached : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) + Q_PROPERTY(QString richTextLabel READ richTextLabel NOTIFY richTextLabelChanged) + Q_PROPERTY(QString mnemonicLabel READ mnemonicLabel NOTIFY mnemonicLabelChanged) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(MnemonicAttached::ControlType controlType READ controlType WRITE setControlType NOTIFY controlTypeChanged) + Q_PROPERTY(QKeySequence sequence READ sequence NOTIFY sequenceChanged) + Q_ENUMS(ControlType) +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 */ + }; + + 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,307 @@ +/* +* 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 + +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) { + m_window->removeEventFilter(this); + } + m_window = window; + if (m_window) { + m_window->installEventFilter(this); + } + }); + } +} + +MnemonicAttached::~MnemonicAttached() +{ + if (s_objectToSequence.contains(this)) { + s_sequenceToObject.remove(s_objectToSequence.value(this)); + s_objectToSequence.remove(this); + } +} + +bool MnemonicAttached::eventFilter(QObject *watched, QEvent *e) +{ + Q_UNUSED(watched) + + if (m_richTextLabel.length() == 0) { + 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("&", 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 == "&") { + 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 + if (s_objectToSequence.contains(this)) { + s_sequenceToObject.remove(s_objectToSequence.value(this)); + s_objectToSequence.remove(this); + } + + calculateWeights(); + + const QString text = label(); + + if (!m_enabled) { + m_actualRichTextLabel = text; + m_actualRichTextLabel.replace("&", QString()); + 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("&", 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(); + } + + 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.length() > 0 ? 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.keys().last(); + } + 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"