diff --git a/src/controls/GlobalDrawer.qml b/src/controls/GlobalDrawer.qml index bd184010..c9527dc8 100644 --- a/src/controls/GlobalDrawer.qml +++ b/src/controls/GlobalDrawer.qml @@ -1,446 +1,463 @@ /* * Copyright 2015 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.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" /** * A drawer specialization intended for the global actions of the application * valid regardless of the application state (think about the menubar * of a desktop application). * * Example usage: * @code * import org.kde.kirigami 2.2 as Kirigami * * Kirigami.ApplicationWindow { * [...] * globalDrawer: Kirigami.GlobalDrawer { * actions: [ * Kirigami.Action { * text: "View" * iconName: "view-list-icons" * Kirigami.Action { * text: "action 1" * } * Kirigami.Action { * text: "action 2" * } * Kirigami.Action { * text: "action 3" * } * }, * Kirigami.Action { * text: "Sync" * iconName: "folder-sync" * } * ] * } * [...] * } * @endcode * */ OverlayDrawer { id: root edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.RightEdge : Qt.LeftEdge /** * title: string * A title to be displayed on top of the drawer */ property alias title: heading.text /** * icon: var * An icon to be displayed alongside the title. * It can be a QIcon, a fdo-compatible icon name, or any url understood by Image */ property alias titleIcon: headingIcon.source /** * bannerImageSource: string * An image to be used as background for the title and icon for * a decorative purpose. * It accepts any url format supported by Image */ property alias bannerImageSource: bannerImage.source /** * actions: list * The list of actions can be nested having a tree structure. * A tree depth bigger than 2 is discouraged. * * Example usage: * @code * import org.kde.kirigami 2.2 as Kirigami * * Kirigami.ApplicationWindow { * [...] * globalDrawer: Kirigami.GlobalDrawer { * actions: [ * Kirigami.Action { * text: "View" * iconName: "view-list-icons" * Kirigami.Action { * text: "action 1" * } * Kirigami.Action { * text: "action 2" * } * Kirigami.Action { * text: "action 3" * } * }, * Kirigami.Action { * text: "Sync" * iconName: "folder-sync" * } * ] * } * [...] * } * @endcode */ property list actions /** * content: list default property * Any random Item can be instantiated inside the drawer and * will be displayed underneath the actions list. * * Example usage: * @code * import org.kde.kirigami 2.2 as Kirigami * * Kirigami.ApplicationWindow { * [...] * globalDrawer: Kirigami.GlobalDrawer { * actions: [...] * Button { * text: "Button" * onClicked: //do stuff * } * } * [...] * } * @endcode */ default property alias content: mainContent.data /** * topContent: list default property * Items that will be instantiated inside the drawer and * will be displayed on top of the actions list. * * Example usage: * @code * import org.kde.kirigami 2.2 as Kirigami * * Kirigami.ApplicationWindow { * [...] * globalDrawer: Kirigami.GlobalDrawer { * actions: [...] * topContent: [Button { * text: "Button" * onClicked: //do stuff * }] * } * [...] * } * @endcode */ property alias topContent: topContent.data /** * resetMenuOnTriggered: bool * * On the actions menu, whenever a leaf action is triggered, the menu * will reset to its parent. */ property bool resetMenuOnTriggered: true /** * currentSubMenu: Action * * Points to the action acting as a submenu */ readonly property Action currentSubMenu: stackView.currentItem ? stackView.currentItem.current: null /** * Notifies that the banner has been clicked */ signal bannerClicked() /** * Reverts the menu back to its initial state */ function resetMenu() { stackView.pop(stackView.get(0, T2.StackView.DontLoad)); if (root.modal) { root.drawerOpen = false; } } rightPadding: !Settings.isMobile && mainFlickable.contentHeight > mainFlickable.height ? Units.gridUnit : Units.smallSpacing contentItem: ScrollView { id: scrollView //ensure the attached property exists Theme.inherit: true anchors.fill: parent implicitWidth: Math.min (Units.gridUnit * 20, root.parent.width * 0.8) horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff Flickable { id: mainFlickable contentWidth: width contentHeight: mainColumn.Layout.minimumHeight ColumnLayout { id: mainColumn width: mainFlickable.width spacing: 0 height: Math.max(root.height, Layout.minimumHeight) Image { id: bannerImage Layout.fillWidth: true Layout.preferredWidth: title.implicitWidth Layout.preferredHeight: bannerImageSource != "" ? 10 * Units.gridUnit : Layout.minimumHeight Layout.minimumHeight: title.height > 0 ? title.height + Units.smallSpacing * 2 : 0 MouseArea { anchors.fill: parent onClicked: root.bannerClicked() } fillMode: Image.PreserveAspectCrop asynchronous: true anchors { left: parent.left right: parent.right top: parent.top } EdgeShadow { edge: Qt.BottomEdge visible: bannerImageSource != "" anchors { left: parent.left right: parent.right bottom: parent.top } } LinearGradient { anchors { left: parent.left right: parent.right top: parent.top } visible: bannerImageSource != "" && root.title != "" height: title.height * 1.3 start: Qt.point(0, 0) end: Qt.point(0, height) gradient: Gradient { GradientStop { position: 0.0 color: Qt.rgba(0, 0, 0, 0.8) } GradientStop { position: 1.0 color: "transparent" } } } RowLayout { id: title anchors { left: parent.left top: parent.top margins: Units.smallSpacing * 2 } Icon { id: headingIcon Layout.minimumWidth: Units.iconSizes.large Layout.minimumHeight: width visible: valid isMask: false //TODO: find a better way to control selective coloring on Android enabled: !Settings.isMobile } Heading { id: heading Layout.fillWidth: true Layout.rightMargin: heading.height visible: text.length > 0 level: 1 color: bannerImageSource != "" ? "white" : Theme.textColor elide: Text.ElideRight } } } ColumnLayout { id: topContent spacing: 0 Layout.alignment: Qt.AlignHCenter Layout.leftMargin: root.leftPadding Layout.rightMargin: root.rightPadding Layout.bottomMargin: Units.smallSpacing Layout.topMargin: root.topPadding Layout.fillWidth: true Layout.fillHeight: true //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 } T2.StackView { id: stackView Layout.fillWidth: true Layout.minimumHeight: currentItem ? currentItem.implicitHeight : 0 Layout.maximumHeight: Layout.minimumHeight initialItem: menuComponent //NOTE: it's important those are NumberAnimation and not XAnimators // as while the animation is running the drawer may close, and //the animator would stop when not drawing see BUG 381576 popEnter: Transition { NumberAnimation { property: "x"; from: (stackView.mirrored ? -1 : 1) * -stackView.width; to: 0; duration: 400; easing.type: Easing.OutCubic } } popExit: Transition { NumberAnimation { property: "x"; from: 0; to: (stackView.mirrored ? -1 : 1) * stackView.width; duration: 400; easing.type: Easing.OutCubic } } pushEnter: Transition { NumberAnimation { property: "x"; from: (stackView.mirrored ? -1 : 1) * stackView.width; to: 0; duration: 400; easing.type: Easing.OutCubic } } pushExit: Transition { NumberAnimation { property: "x"; from: 0; to: (stackView.mirrored ? -1 : 1) * -stackView.width; duration: 400; easing.type: Easing.OutCubic } } replaceEnter: Transition { NumberAnimation { property: "x"; from: (stackView.mirrored ? -1 : 1) * stackView.width; to: 0; duration: 400; easing.type: Easing.OutCubic } } replaceExit: Transition { NumberAnimation { property: "x"; from: 0; to: (stackView.mirrored ? -1 : 1) * -stackView.width; duration: 400; easing.type: Easing.OutCubic } } } Item { Layout.fillWidth: true Layout.fillHeight: root.actions.length>0 Layout.minimumHeight: Units.smallSpacing } ColumnLayout { id: mainContent Layout.alignment: Qt.AlignHCenter Layout.leftMargin: root.leftPadding Layout.rightMargin: root.rightPadding Layout.fillWidth: true Layout.fillHeight: true //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 } Item { Layout.minimumWidth: Units.smallSpacing Layout.minimumHeight: root.bottomPadding } Component { id: menuComponent ColumnLayout { spacing: 0 property alias model: actionsRepeater.model property Action current property int level: 0 Layout.maximumHeight: Layout.minimumHeight 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.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.iconName - label: modelData.text + + label: MnemonicData.richTextLabel + MnemonicData.enabled: listItem.enabled && listItem.visible + 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 right: contentItem.right rightMargin: !Settings.isMobile && mainFlickable.contentHeight > mainFlickable.height ? Units.gridUnit : 0 } height: Units.iconSizes.smallMedium selected: listItem.checked || listItem.pressed width: height source: (LayoutMirroring.enabled ? "go-next-symbolic-rtl" : "go-next-symbolic") visible: modelData.children!==undefined && modelData.children.length > 0 } onClicked: { modelData.trigger(); if (modelData.children!==undefined && modelData.children.length > 0) { stackView.push(menuComponent, {model: modelData.children, level: level + 1, current: modelData }); } else if (root.resetMenuOnTriggered) { root.resetMenu(); } checked = Qt.binding(function() { return modelData.checked }); } } } } } } } } } diff --git a/src/mnemonicattached.cpp b/src/mnemonicattached.cpp index ba1f24f0..30d0a81c 100644 --- a/src/mnemonicattached.cpp +++ b/src/mnemonicattached.cpp @@ -1,247 +1,251 @@ /* * 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 QHash MnemonicAttached::s_sequenceToObject = QHash(); QHash MnemonicAttached::s_objectToSequence = QHash(); MnemonicAttached::MnemonicAttached(QObject *parent) : QObject(parent) { qApp->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 = 0; } else { m_weight = 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; } QKeySequence MnemonicAttached::sequence() { return s_objectToSequence.value(this); } MnemonicAttached *MnemonicAttached::qmlAttachedProperties(QObject *object) { return new MnemonicAttached(object); } #include "moc_mnemonicattached.cpp"