diff --git a/CMakeLists.txt b/CMakeLists.txt index 63f95086..48075fa9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,10 +23,10 @@ include(ECMGenerateHeaders) include(GenerateExportHeader) include(FeatureSummary) -find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core Gui Widgets Qml Quick) +find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core Gui Widgets Qml Quick Test) -find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Plasma Service Declarative) -find_package(KF5 REQUIRED COMPONENTS PlasmaQuick I18n) +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Plasma Service Declarative I18n) +find_package(KF5 REQUIRED COMPONENTS PlasmaQuick DBusAddons Notifications) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) @@ -44,9 +44,13 @@ install(DIRECTORY compositor/ PATTERN Messages.sh EXCLUDE PATTERN dummydata EXCLUDE) +kpackage_install_package(phonebook org.kde.phone.phonebook genericqml) +install(FILES phonebook/metadata.desktop DESTINATION ${XDG_APPS_INSTALL_DIR} RENAME org.kde.phone.dialer.desktop) + add_subdirectory(bin) add_subdirectory(qmlcomponents) add_subdirectory(services) add_subdirectory(settingsmodules) add_subdirectory(applets) add_subdirectory(containments) +add_subdirectory(dialer) diff --git a/applets/CMakeLists.txt b/applets/CMakeLists.txt index a99f1209..ca6d9d10 100644 --- a/applets/CMakeLists.txt +++ b/applets/CMakeLists.txt @@ -1,3 +1,5 @@ plasma_install_package(clock org.kde.phone.clock) plasma_install_package(notifications org.kde.phone.notifications) + +add_subdirectory(quicksettings) diff --git a/applets/notifications/contents/ui/NotificationStripe.qml b/applets/notifications/contents/ui/NotificationStripe.qml index c9406ac8..f555b622 100644 --- a/applets/notifications/contents/ui/NotificationStripe.qml +++ b/applets/notifications/contents/ui/NotificationStripe.qml @@ -18,25 +18,24 @@ */ import QtQuick 2.0 +import QtQuick.Layouts 1.1 + import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents MouseArea { - id: root + id: notificationItem - height: units.gridUnit * 2 + height: units.gridUnit * (expanded ? (actionsLayout.visible ? 6 : 4) : 2) + background.margins.top + background.margins.bottom width: parent.width anchors.bottomMargin: 10 drag.axis: Drag.XAxis - drag.target: root + drag.target: notificationItem property bool expanded: false - property var textGradient: Gradient { - GradientStop { position: 1.0; color: "#FF00000C" } - GradientStop { position: 0.0; color: "#00000C00" } - } - property color textGradientOverlay: "#9900000C" + property string source: model.source + property var actions: model.actions Behavior on x { SpringAnimation { spring: 2; damping: 0.2 } @@ -46,112 +45,105 @@ MouseArea { SpringAnimation { spring: 5; damping: 0.3 } } - onExpandedChanged: { - if (expanded && body) { - height = units.gridUnit * 4; - } else { - height = units.gridUnit * 2; - } - } - onReleased: { if (drag.active) { if (x > width / 4 || x < width / -4) { + //if there is an action, execute the first when swiping left + if (x < 0 && actions) { + var action = actions.get(0) + root.executeAction(source, action.id) + } notificationsModel.remove(index); } else { x = 0; } - } else if (body) { + } else if (body || actions) { expanded = !expanded; } } + PlasmaCore.FrameSvgItem { + id: background + imagePath: "widgets/background" + anchors { + fill: parent + rightMargin: -notificationItem.width + leftMargin: units.gridUnit + } + colorGroup: PlasmaCore.ColorScope.colorGroup + } + + PlasmaComponents.ToolButton { + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + leftMargin: units.gridUnit / 2 + } + iconSource: "window-close" + flat: false + onClicked: { + notificationsModel.remove(index); + } + } + + PlasmaComponents.Label { + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + leftMargin: units.gridUnit * 3 + } + color: PlasmaCore.ColorScope.textColor + text: model.appName + } + + PlasmaComponents.Label { + id: summaryText + anchors { + right: icon.left + verticalCenter: parent.verticalCenter + rightMargin: units.smallSpacing + } + horizontalAlignment: Qt.AlignRight + verticalAlignment: Qt.AlignVCenter + color: PlasmaCore.ColorScope.textColor + text: summary + (notificationItem.expanded ? (body ? "\n" + body : '') : + (body ? '...' : '')) + } + PlasmaCore.IconItem { id: icon + anchors { + right: notificationItem.right + verticalCenter: parent.verticalCenter + } width: units.iconSizes.medium height: width - visible: !root.expanded - anchors.verticalCenter: parent.verticalCenter - x: units.largeSpacing - y: 0 source: appIcon && appIcon.length > 0 ? appIcon : "im-user" } - - Item { - id: rounded - clip: true - height: parent.height - width: height / 2 - visible: !root.expanded + RowLayout { + id: actionsLayout anchors { - left: icon.right - leftMargin: units.largeSpacing + right: summaryText.right + top: summaryText.bottom + topMargin: units.smallSpacing } - - Rectangle { - id: roundedRect - height: parent.height - width: parent.width * 2 - radius: height //Math.max(height, units.gridUnit) - anchors { - left: parent.left - top: parent.top + opacity: notificationItem.expanded && notificationItem.actions && notificationItem.actions.count > 0 ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: units.shortDuration + easing.type: Easing.InOutQuad } - - gradient: root.textGradient - - Rectangle { - anchors.fill: parent - radius: height / 2 - color: textGradientOverlay + } + Repeater { + model: notificationItem.actions + delegate: PlasmaComponents.Button { + text: model.text + onClicked: { + root.executeAction(notificationItem.source, model.id) + notificationsModel.remove(index); + } } } } - - Rectangle { - id: summaryArea - width: parent.width - icon.width - rounded.width - (units.largeSpacing * 2) - height: parent.height - anchors { - left: root.expanded ? root.left : rounded.right - right: root.right - top: parent.top - } - - gradient: root.textGradient - Rectangle { - anchors.fill: parent - color: textGradientOverlay - } - - PlasmaComponents.Label { - id: summaryText - anchors.fill: parent - clip: true - horizontalAlignment: root.expanded ? Qt.AlignHCenter : Qt.AlignLeft - verticalAlignment: Qt.AlignVCenter - color: PlasmaCore.ColorScope.textColor - text: summary + (root.expanded ? (body ? "\n" + body : '') : - (body ? '...' : '')) - } - - } - - Rectangle { - id: extraArea - width: parent.width - height: parent.width - anchors { - left: summaryArea.right - top: parent.top - bottom: parent.bottom - } - - gradient: root.textGradient - Rectangle { - anchors.fill: parent - color: textGradientOverlay - } - } -} \ No newline at end of file +} diff --git a/applets/notifications/contents/ui/main.qml b/applets/notifications/contents/ui/main.qml index c38ab798..27199ed5 100644 --- a/applets/notifications/contents/ui/main.qml +++ b/applets/notifications/contents/ui/main.qml @@ -25,9 +25,59 @@ import org.kde.plasma.components 2.0 as PlasmaComponents Item { id: root + property int notificationId: 0 Layout.minimumHeight: notificationView.contentsHeight + function addNotification(source, data, actions) { + // Do not show duplicated notifications + // Remove notifications that are sent again (odd, but true) + for (var i = 0; i < notificationsModel.count; ++i) { + var tmp = notificationsModel.get(i); + var matches = (tmp.appName == data.appName && + tmp.summary == data.summary && + tmp.body == data.body); + var sameSource = tmp.source == source; + + if (sameSource && matches) { + return; + } + + if (sameSource || matches) { + notificationsModel.remove(i) + break; + } + } + + data["id"] = ++notificationId; + data["source"] = source; + if (data["summary"].length < 1) { + data["summary"] = data["body"]; + data["body"] = ''; + } + data["actions"] = actions; + + notificationsModel.insert(0, data); + if (!data["isPersistent"]) { + pendingRemovals.push(notificationId); + pendingTimer.start(); + } + } + + function executeAction(source, id) { + //try to use the service + if (source.indexOf("notification") !== -1) { + var service = notificationsSource.serviceForSource(source) + var op = service.operationDescription("invokeAction") + op["actionId"] = id + + service.startOperationCall(op) + //try to open the id as url + } else if (source.indexOf("Job") !== -1) { + Qt.openUrlExternally(id) + } + } + PlasmaCore.DataSource { id: notificationsSource @@ -58,7 +108,7 @@ Item { } } - homescreen.addNotification( + root.addNotification( sourceName, data, actions); @@ -70,17 +120,26 @@ Item { id: notificationsModel ListElement { + source: "call1Source" appIcon: "call-start" summary: "Missed call from Joe" + appName: "Phone" body: "Called at 8:42 from +41 56 373 37 31" + actions: [] } ListElement { + source: "im1Source" appIcon: "im-google" + appName: "Message" summary: "July: Hey! Are you around?" + actions: [] } ListElement { + source: "im2Source" appIcon: "im-google" + appName: "Message" summary: "July: Hello?" + actions: [] } } diff --git a/applets/quicksettings/CMakeLists.txt b/applets/quicksettings/CMakeLists.txt new file mode 100644 index 00000000..595a1f47 --- /dev/null +++ b/applets/quicksettings/CMakeLists.txt @@ -0,0 +1,19 @@ +set(quicksettings_SRCS + quicksettings.cpp +) + +add_library(plasma_applet_quicksettings MODULE ${quicksettings_SRCS}) + +kcoreaddons_desktop_to_json(plasma_applet_quicksettings package/metadata.desktop) + +target_link_libraries(plasma_applet_quicksettings + Qt5::Gui + KF5::Plasma + KF5::I18n) + + +install(TARGETS plasma_applet_quicksettings DESTINATION ${PLUGIN_INSTALL_DIR}) +#install(FILES plasma-quicksettings-default.desktop DESTINATION ${SERVICES_INSTALL_DIR}) + +plasma_install_package(package org.kde.phone.quicksettings) + diff --git a/applets/quicksettings/Messages.sh b/applets/quicksettings/Messages.sh new file mode 100755 index 00000000..83683e21 --- /dev/null +++ b/applets/quicksettings/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.rc -o -name \*.ui -o -name \*.kcfg` >> rc.cpp +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.phone.quicksettings.pot +rm -f rc.cpp diff --git a/applets/quicksettings/package/contents/ui/BrightnessDelegate.qml b/applets/quicksettings/package/contents/ui/BrightnessDelegate.qml new file mode 100644 index 00000000..0e592700 --- /dev/null +++ b/applets/quicksettings/package/contents/ui/BrightnessDelegate.qml @@ -0,0 +1,75 @@ +/* + * 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.4 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +Item { + id: delegateRoot + implicitWidth: delegate.implicitWidth + implicitHeight: delegate.implicitHeight + (slider.opacity > 0 ? slider.height : 0) + + property int screenBrightness + readonly property int maximumScreenBrightness: pmSource.data["PowerDevil"] ? pmSource.data["PowerDevil"]["Maximum Screen Brightness"] || 0 : 0 + + PlasmaCore.DataSource { + id: pmSource + engine: "powermanagement" + connectedSources: ["PowerDevil"] + + onDataChanged: { + delegateRoot.screenBrightness = source.data["PowerDevil"]["Screen Brightness"]; + } + } + + Delegate { + id: delegate + toggled: slider.opacity > 0 + function toggle() { + slider.opacity = slider.opacity > 0 ? 0 : 1; + } + } + PlasmaComponents.Slider { + id: slider + anchors.top: delegate.bottom + width: flow.width + opacity: 0 + x: -delegateRoot.parent.x + value: screenBrightness + minimumValue: maximumValue > 100 ? 1 : 0 + maximumValue: delegateRoot.maximumScreenBrightness + Behavior on opacity { + OpacityAnimator { + duration: units.shortDuration + easing.type: Easing.InOutQuad + } + } + } + + onScreenBrightnessChanged: { + var service = pmSource.serviceForSource("PowerDevil"); + var operation = service.operationDescription("setBrightness"); + operation.brightness = slider.value; + operation.silent = true + service.startOperationCall(operation); + } +} + diff --git a/applets/quicksettings/package/contents/ui/Delegate.qml b/applets/quicksettings/package/contents/ui/Delegate.qml new file mode 100644 index 00000000..5752de39 --- /dev/null +++ b/applets/quicksettings/package/contents/ui/Delegate.qml @@ -0,0 +1,85 @@ +/* + * 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.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +RowLayout { + id: delegateRoot + property bool toggled: model.enabled + spacing: units.smallSpacing + implicitWidth: flow.width / 2 - units.largeSpacing / 2 + Rectangle { + Layout.minimumWidth: units.iconSizes.large + Layout.minimumHeight: width + color: toggled ? + Qt.rgba(PlasmaCore.ColorScope.highlightColor.r, PlasmaCore.ColorScope.highlightColor.g, PlasmaCore.ColorScope.highlightColor.b, iconMouseArea.pressed ? 0.5 : 0.3) : + Qt.rgba(PlasmaCore.ColorScope.textColor.r, PlasmaCore.ColorScope.textColor.g, PlasmaCore.ColorScope.textColor.b, iconMouseArea.pressed ? 0.5 : 0.2) + + PlasmaCore.IconItem { + colorGroup: PlasmaCore.ColorScope.colorGroup + anchors { + fill: parent + margins: units.smallSpacing + } + source: model.icon + MouseArea { + id: iconMouseArea + anchors.fill: parent + onClicked: { + if (delegateRoot.toggle) { + delegateRoot.toggle(); + } else if (model.toggleFunction) { + root[model.toggleFunction](); + } else if (model.settingsCommand) { + plasmoid.nativeInterface.executeCommand(model.settingsCommand); + } + } + } + } + } + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Qt.rgba(PlasmaCore.ColorScope.textColor.r, PlasmaCore.ColorScope.textColor.g, PlasmaCore.ColorScope.textColor.b, labelMouseArea.pressed ? 0.5 : 0.2) + PlasmaComponents.Label { + anchors { + fill: parent + margins: units.smallSpacing + } + text: model.text + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + MouseArea { + id: labelMouseArea + anchors.fill: parent + onClicked: { + if (model.settingsCommand) { + plasmoid.nativeInterface.executeCommand(model.settingsCommand); + } else if (model.toggleFunction) { + root[model.toggleFunction](); + } + } + } + } + } +} + diff --git a/applets/quicksettings/package/contents/ui/main.qml b/applets/quicksettings/package/contents/ui/main.qml new file mode 100644 index 00000000..85f20ec3 --- /dev/null +++ b/applets/quicksettings/package/contents/ui/main.qml @@ -0,0 +1,128 @@ +/* + * 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.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.plasmoid 2.0 + + +Item { + id: root + + function toggleAirplane() { + print("toggle airplane mode") + } + + Plasmoid.preferredRepresentation: plasmoid.fullRepresentation + + ListModel { + id: settingsModel + + ListElement { + text: "Settings" + icon: "configure" + enabled: false + settingsCommand: "active-settings" + toggleFunction: "" + delegate: "" + } + ListElement { + text: "Mobile network" + icon: "network-mobile-80" + enabled: true + settingsCommand: "" + } + ListElement { + text: "Airplane mode" + icon: "flightmode-on" + enabled: false + settingsCommand: "" + toggleFunction: "toggleAirplane" + } + ListElement { + text: "Bluetooth" + icon: "preferences-system-bluetooth" + enabled: false + settingsCommand: "" + } + ListElement { + text: "Wireless" + icon: "network-wireless-on" + enabled: true + settingsCommand: "active-settings -m org.kde.satellite.settings.wifi" + } + ListElement { + text: "Alarms" + icon: "korgac" + enabled: false + settingsCommand: "" + } + ListElement { + text: "Notifications" + icon: "preferences-desktop-notification" + enabled: true + settingsCommand: "" + } + ListElement { + text: "Brightness" + icon: "video-display-brightness" + enabled: false + settingsCommand: "active-settings -m org.kde.active.settings.powermanagement" + delegate: "BrightnessDelegate" + } + ListElement { + text: "Flashlight" + icon: "package_games_puzzle" + enabled: false + settingsCommand: "" + } + ListElement { + text: "Location" + icon: "plasmaapplet-location" + enabled: false + settingsCommand: "" + } + } + + Flow { + id: flow + anchors { + fill: parent + margins: units.largeSpacing + } + spacing: units.largeSpacing + Repeater { + model: settingsModel + delegate: Loader { + width: item ? item.implicitWidth : 0 + height: item ? item.implicitHeight : 0 + source: Qt.resolvedUrl((model.delegate ? model.delegate : "Delegate") + ".qml") + } + } + move: Transition { + NumberAnimation { + duration: units.shortDuration + easing.type: Easing.InOutQuad + properties: "x,y" + } + } + } +} diff --git a/applets/quicksettings/package/metadata.desktop b/applets/quicksettings/package/metadata.desktop new file mode 100644 index 00000000..fb11f66a --- /dev/null +++ b/applets/quicksettings/package/metadata.desktop @@ -0,0 +1,22 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=Quick Settings + +Comment=A panel to quickly access most important settings + +Icon=systemsettings +Type=Service +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Marco Martin +X-KDE-PluginInfo-Email=mart@kde.org +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=org.kde.phone.quicksettings +X-KDE-Library=plasma_applet_quicksettings +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-Website=plasma.kde.org +X-KDE-ServiceTypes=Plasma/Applet +X-Plasma-API=declarativeappletscript +X-KDE-PluginInfo-Category=Settings + +X-Plasma-MainScript=ui/main.qml +X-Plasma-RemoteLocation= diff --git a/applets/quicksettings/quicksettings.cpp b/applets/quicksettings/quicksettings.cpp new file mode 100644 index 00000000..f82e0a61 --- /dev/null +++ b/applets/quicksettings/quicksettings.cpp @@ -0,0 +1,43 @@ +/*************************************************************************** + * Copyright (C) 2015 Marco Martin * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 General Public License for more details. * + * * + * You should have received a copy of the GNU 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 "quicksettings.h" + +#include +#include + +QuickSettings::QuickSettings(QObject *parent, const QVariantList &args) + : Plasma::Applet(parent, args) +{ + setHasConfigurationInterface(true); +} + +QuickSettings::~QuickSettings() +{ +} + +void QuickSettings::executeCommand(const QString &command) +{ + qWarning()<<"Executing"< * + * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 General Public License for more details. * + * * + * You should have received a copy of the GNU 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 QUICKSETTINGS_H +#define QUICKSETTINGS_H + + +#include + + +class QuickSettings : public Plasma::Applet +{ + Q_OBJECT + +public: + QuickSettings( QObject *parent, const QVariantList &args ); + ~QuickSettings(); + +public Q_SLOTS: + void executeCommand(const QString &command); + +private: + +}; + +#endif diff --git a/containments/CMakeLists.txt b/containments/CMakeLists.txt index 7e680eeb..7f2eaffd 100644 --- a/containments/CMakeLists.txt +++ b/containments/CMakeLists.txt @@ -1,2 +1,3 @@ plasma_install_package(homescreen org.kde.phone.homescreen) +plasma_install_package(panel org.kde.phone.panel) diff --git a/containments/homescreen/contents/config/main.xml b/containments/homescreen/contents/config/main.xml index 61a525b9..92a9517b 100644 --- a/containments/homescreen/contents/config/main.xml +++ b/containments/homescreen/contents/config/main.xml @@ -9,6 +9,10 @@ + + + org.kde.phone.dialer.desktop + diff --git a/containments/homescreen/contents/ui/HomeLauncher.qml b/containments/homescreen/contents/ui/HomeLauncher.qml index a7b8ae93..ffd23976 100644 --- a/containments/homescreen/contents/ui/HomeLauncher.qml +++ b/containments/homescreen/contents/ui/HomeLauncher.qml @@ -8,150 +8,43 @@ Item { width: applicationsView.cellWidth height: width - property int idx: index - property int oldIdx: -1 + property var modelData: model + property bool isDropTarget: delegateRoot != dragDelegate && root.reorderingApps && applicationsView.dragData && applicationsView.dragData.ApplicationStorageIdRole == modelData.ApplicationStorageIdRole - Rectangle { - anchors.fill: parent - color: PlasmaCore.ColorScope.textColor - radius: units.gridUnit - opacity: (delegateItem.drag.target != null) ? 0.4 : 0 - Behavior on opacity { - NumberAnimation { + opacity: isDropTarget ? 0.3 : 1 + + PlasmaCore.IconItem { + id: icon + anchors.centerIn: parent + width: parent.height / 2 + height: width + source: modelData.ApplicationIconRole + scale: root.reorderingApps && applicationsView.dragData && applicationsView.dragData.ApplicationStorageIdRole != modelData.ApplicationStorageIdRole ? 0.6 : 1 + Behavior on scale { + NumberAnimation { duration: units.longDuration easing.type: Easing.InOutQuad } } } - //animate index change - onIdxChanged: { - if (delegateItem.drag.target != null) { - return; + + PlasmaComponents.Label { + id: label + visible: text.length > 0 + + anchors { + top: icon.bottom + left: icon.left + right: icon.right } - if (oldIdx < 0) { - oldIdx = idx; - return; - } + wrapMode: Text.WordWrap + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + maximumLineCount: 2 - delegateItem.x = ((oldIdx % 4) * GridView.view.cellWidth) - ((idx % 4) * GridView.view.cellWidth); - delegateItem.y = (Math.floor(oldIdx / 4) * GridView.view.cellHeight) - (Math.floor(idx / 4) * GridView.view.cellHeight); - - translAnim.running = true; - - oldIdx = idx; - } - - NumberAnimation { - id: translAnim - duration: units.longDuration - easing.type: Easing.InOutQuad - target: delegateItem - properties: "x,y" - to: 0 - } - MouseArea { - id: delegateItem - - width: applicationsView.cellWidth - height: width - - states: [ - State { - when: delegateItem.drag.target != null - ParentChange { - target: delegateItem - parent: delegateRoot.parent - } - PropertyChanges { - target: delegateItem - z: 9999 - } - } - ] - function updateRow() { - var pos = mapToItem(delegateRoot.parent, 0, 0); - - var newRow = (Math.round(delegateRoot.GridView.view.width / delegateRoot.GridView.view.cellWidth) * Math.round(pos.y / delegateRoot.GridView.view.cellHeight) + Math.round(pos.x / delegateRoot.GridView.view.cellWidth)); - - if (model.ApplicationOriginalRowRole != newRow) { - appListModel.moveItem(model.ApplicationOriginalRowRole, newRow); - } - - } - - onClicked: { - console.log("Clicked: " + model.ApplicationStorageIdRole) - appListModel.runApplication(model.ApplicationStorageIdRole) - oldX = x - oldY = y - } - onPressAndHold: { - delegateRoot.GridView.view.draggingItem = delegateItem; - delegateItem.drag.target = delegateItem; - root.reorderingApps = true; - } - onReleased: { - delegateRoot.GridView.view.draggingItem = delegateItem; - delegateItem.drag.target = null; - root.reorderingApps = false; - - translAnim.running = true; - autoScrollTimer.running = false; - } - onPositionChanged: { - if (!autoScrollTimer.running && delegateItem.drag.target) { - updateRow(); - - var screenPos = mapToItem(delegateRoot.GridView.view, 0, 0); - - if (applicationsView.contentY > 0 && screenPos.y < root.height / 4) { - autoScrollTimer.scrollDown = false; - autoScrollTimer.running = true; - } else if (!applicationsView.atYEnd && screenPos.y > 3 * (root.height / 4)) { - autoScrollTimer.scrollDown = true; - autoScrollTimer.running = true; - } else { - autoScrollTimer.running = false; - } - } else { - autoScrollTimer.running = false; - } - } - - PlasmaCore.IconItem { - id: icon - anchors.centerIn: parent - width: parent.height / 2 - height: width - source: model.ApplicationIconRole - scale: root.reorderingApps && !delegateItem.drag.target ? 0.6 : 1 - Behavior on scale { - NumberAnimation { - duration: units.longDuration - easing.type: Easing.InOutQuad - } - } - } - - PlasmaComponents.Label { - id: label - visible: text.length > 0 - - anchors { - top: icon.bottom - left: icon.left - right: icon.right - } - - wrapMode: Text.WordWrap - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - maximumLineCount: 2 - - text: model.ApplicationNameRole - font.pixelSize: theme.smallestFont.pixelSize - color: PlasmaCore.ColorScope.textColor - } + text: modelData.ApplicationNameRole + font.pixelSize: theme.smallestFont.pixelSize + color: PlasmaCore.ColorScope.textColor } } diff --git a/containments/homescreen/contents/ui/SatelliteStripe.qml b/containments/homescreen/contents/ui/SatelliteStripe.qml index 813576f6..74c1df11 100644 --- a/containments/homescreen/contents/ui/SatelliteStripe.qml +++ b/containments/homescreen/contents/ui/SatelliteStripe.qml @@ -1,7 +1,9 @@ import QtQuick 2.0 import org.kde.plasma.core 2.0 as PlasmaCore -Item { +PlasmaCore.ColorScope { + colorGroup: PlasmaCore.Theme.NormalColorGroup + PlasmaCore.FrameSvgItem { z: -1 imagePath: "widgets/background" @@ -11,10 +13,17 @@ Item { topMargin: -margins.top bottomMargin: -margins.bottom } + Rectangle { + anchors { + fill: parent + topMargin: parent.margins.top + bottomMargin: parent.margins.bottom + } + color: PlasmaCore.ColorScope.backgroundColor + } } - opacity: 0.6 - height: Math.max(100, units.gridUnit * 2.5) + height: applicationsView.cellWidth width: parent.width y: parent.height / 2 - height / 2 x: 0 diff --git a/containments/homescreen/contents/ui/main.qml b/containments/homescreen/contents/ui/main.qml index bc808af8..8a34a15a 100644 --- a/containments/homescreen/contents/ui/main.qml +++ b/containments/homescreen/contents/ui/main.qml @@ -16,18 +16,18 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. */ -import QtQuick 2.1 +import QtQuick 2.4 import QtQuick.Layouts 1.1 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents - +import org.kde.kquickcontrolsaddons 2.0 import org.kde.satellite.components 0.1 as SatelliteComponents import "plasmapackage:/code/LayoutManager.js" as LayoutManager -Item { +MouseEventListener { id: root width: 480 height: 640 @@ -42,6 +42,12 @@ Item { LayoutManager.save(); } + Plasmoid.onFocusChanged: { + if (!plasmoid.focus && applicationsView.contentY > -(applicationsView.headerItem.height - root.height/2)) { + applicationsView.contentY = -root.height; + } + } + function addApplet(applet, x, y) { var container = appletContainerComponent.createObject(appletsSpace.layout) container.visible = true @@ -65,7 +71,8 @@ Item { container.animationsEnabled = false; if (appletsSpace.lastSpacer.parent === appletsSpace.layout) { - before = appletsSpace.lastSpacer; + //Uncomment to make the spacer the last element again + // before = appletsSpace.lastSpacer; } if (before) { @@ -92,24 +99,28 @@ Item { LayoutManager.lastSpacer = appletsSpace.lastSpacer; LayoutManager.restore(); applicationsView.contentY = -root.height; + + appListModel.appOrder = plasmoid.configuration.AppOrder; + appListModel.loadApplications(); } SatelliteComponents.ApplicationListModel { id: appListModel + onAppOrderChanged: { + plasmoid.configuration.AppOrder = appListModel.appOrder; + } } Timer { id: autoScrollTimer property bool scrollDown: true repeat: true - interval: 10 + interval: 1500 onTriggered: { - applicationsView.contentY += scrollDown ? 8 : -8; - if (applicationsView.draggingItem) { - applicationsView.draggingItem.y += scrollDown ? 8 : -8; - - applicationsView.draggingItem.updateRow(); - } + scrollAnim.to = scrollDown ? + Math.min(applicationsView.contentItem.height - applicationsView.headerItem.height - root.height, applicationsView.contentY + root.height/2) : + Math.max(0, applicationsView.contentY - root.height/2); + scrollAnim.running = true; } } @@ -128,7 +139,7 @@ Item { } Layout.minimumWidth: root.width - Layout.minimumHeight: Math.max(applet.Layout.minimumHeight, root.height / 2) + Layout.minimumHeight: Math.max(applet.Layout.minimumHeight, (root.height-applicationsView.headerItem.margin) / 2) Layout.preferredWidth: root.width Layout.preferredHeight: Layout.minimumHeight @@ -138,15 +149,197 @@ Item { } } - Rectangle { - color: Qt.rgba(0, 0, 0, 0.9 * (Math.min(applicationsView.contentY + root.height, root.height) / root.height)) - anchors.fill: parent - } + onPressAndHold: { + var pos = mapToItem(applicationsView.headerItem.favoritesStrip, mouse.x, mouse.y); + //in favorites area? + var item; + if (applicationsView.headerItem.favoritesStrip.contains(pos)) { + item = applicationsView.headerItem.favoritesStrip.itemAt(pos.x, pos.y); + } else { + pos = mapToItem(applicationsView.contentItem, mouse.x, mouse.y); + item = applicationsView.itemAt(pos.x, pos.y) + } + if (!item) { + return; + } + applicationsView.dragData = new Object; + applicationsView.dragData.ApplicationNameRole = item.modelData.ApplicationNameRole; + applicationsView.dragData.ApplicationIconRole = item.modelData.ApplicationIconRole; + applicationsView.dragData.ApplicationStorageIdRole = item.modelData.ApplicationStorageIdRole; + applicationsView.dragData.ApplicationEntryPathRole = item.modelData.ApplicationEntryPathRole; + applicationsView.dragData.ApplicationOriginalRowRole = item.modelData.ApplicationOriginalRowRole; + + dragDelegate.modelData = applicationsView.dragData; + applicationsView.interactive = false; + root.reorderingApps = true; + dragDelegate.x = Math.floor(mouse.x / root.buttonHeight) * root.buttonHeight + dragDelegate.y = Math.floor(mouse.y / root.buttonHeight) * root.buttonHeight + dragDelegate.xTarget = mouse.x - dragDelegate.width/2; + dragDelegate.yTarget = mouse.y - dragDelegate.width/2; + dragDelegate.opacity = 1; + } + onPositionChanged: { + if (!applicationsView.dragData) { + return; + } + dragDelegate.x = mouse.x - dragDelegate.width/2; + dragDelegate.y = mouse.y - dragDelegate.height/2; + + var pos = mapToItem(applicationsView.contentItem, mouse.x, mouse.y); + + //in favorites area? + if (applicationsView.headerItem.favoritesStrip.contains(mapToItem(applicationsView.headerItem.favoritesStrip, mouse.x, mouse.y))) { + pos.y = 1; + } + + var newRow = (Math.round(applicationsView.width / applicationsView.cellWidth) * Math.floor(pos.y / applicationsView.cellHeight) + Math.floor(pos.x / applicationsView.cellWidth)); + + if (applicationsView.dragData.ApplicationOriginalRowRole != newRow) { + appListModel.moveItem(applicationsView.dragData.ApplicationOriginalRowRole, newRow); + applicationsView.dragData.ApplicationOriginalRowRole = newRow; + } + + var pos = mapToItem(applicationsView.headerItem.favoritesStrip, mouse.x, mouse.y); + //FAVORITES + if (applicationsView.headerItem.favoritesStrip.contains(pos)) { + autoScrollTimer.running = false; + scrollUpIndicator.opacity = 0; + scrollDownIndicator.opacity = 0; + //SCROLL UP + } else if (applicationsView.contentY > 0 && mouse.y < root.buttonHeight + root.height / 4) { + autoScrollTimer.scrollDown = false; + autoScrollTimer.running = true; + scrollUpIndicator.opacity = 1; + scrollDownIndicator.opacity = 0; + //SCROLL DOWN + } else if (!applicationsView.atYEnd && mouse.y > 3 * (root.height / 4)) { + autoScrollTimer.scrollDown = true; + autoScrollTimer.running = true; + scrollUpIndicator.opacity = 0; + scrollDownIndicator.opacity = 1; + //DON't SCROLL + } else { + autoScrollTimer.running = false; + scrollUpIndicator.opacity = 0; + scrollDownIndicator.opacity = 0; + } + + } + onReleased: { + applicationsView.interactive = true; + dragDelegate.xTarget = Math.floor(mouse.x / root.buttonHeight) * root.buttonHeight; + dragDelegate.yTarget = Math.floor(mouse.y / root.buttonHeight) * root.buttonHeight; + dragDelegate.opacity = 0; + dragDelegate.modelData.ApplicationIconRole = ""; + dragDelegate.modelDataChanged(); + applicationsView.dragData = null; + root.reorderingApps = false; + applicationsView.forceLayout(); + autoScrollTimer.running = false; + scrollUpIndicator.opacity = 0; + scrollDownIndicator.opacity = 0; + } + onClicked: { + var pos = mapToItem(applicationsView.headerItem.favoritesStrip, mouse.x, mouse.y); + //in favorites area? + var item; + if (applicationsView.headerItem.favoritesStrip.contains(pos)) { + item = applicationsView.headerItem.favoritesStrip.itemAt(pos.x, pos.y); + } else { + pos = mapToItem(applicationsView.contentItem, mouse.x, mouse.y); + item = applicationsView.itemAt(pos.x, pos.y) + } + if (!item) { + return; + } + + appListModel.runApplication(item.modelData.ApplicationStorageIdRole) + } PlasmaCore.ColorScope { anchors.fill: parent colorGroup: PlasmaCore.Theme.ComplementaryColorGroup + Rectangle { + color: PlasmaCore.ColorScope.backgroundColor + opacity: 0.9 * (Math.min(applicationsView.contentY + root.height, root.height) / root.height) + anchors.fill: parent + } + + PlasmaCore.Svg { + id: arrowsSvg + imagePath: "widgets/arrows" + colorGroup: PlasmaCore.Theme.ComplementaryColorGroup + } + PlasmaCore.SvgItem { + id: scrollUpIndicator + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: 200 + } + z: 2 + opacity: 0 + svg: arrowsSvg + elementId: "up-arrow" + width: units.iconSizes.large + height: width + Behavior on opacity { + OpacityAnimator { + duration: 1000 + easing.type: Easing.InOutQuad + } + } + } + PlasmaCore.SvgItem { + id: scrollDownIndicator + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + } + z: 2 + opacity: 0 + svg: arrowsSvg + elementId: "down-arrow" + width: units.iconSizes.large + height: width + Behavior on opacity { + OpacityAnimator { + duration: 1000 + easing.type: Easing.InOutQuad + } + } + } + + HomeLauncher { + id: dragDelegate + z: 999 + property int xTarget + property int yTarget + + Behavior on opacity { + ParallelAnimation { + OpacityAnimator { + duration: units.longDuration + easing.type: Easing.InOutQuad + } + PropertyAnimation { + properties: "x" + to: dragDelegate.xTarget + target: dragDelegate + duration: units.longDuration + easing.type: Easing.InOutQuad + } + PropertyAnimation { + properties: "y" + to: dragDelegate.yTarget + target: dragDelegate + duration: units.longDuration + easing.type: Easing.InOutQuad + } + } + } + } GridView { id: applicationsView anchors { @@ -156,7 +349,7 @@ Item { right: parent.right } - property Item draggingItem + property var dragData cellWidth: root.buttonHeight cellHeight: cellWidth @@ -183,29 +376,50 @@ Item { duration: units.longDuration easing.type: Easing.InOutQuad } + move: Transition { + NumberAnimation { + duration: units.longDuration + easing.type: Easing.InOutQuad + properties: "x,y" + } + } + moveDisplaced: Transition { + NumberAnimation { + duration: units.longDuration + easing.type: Easing.InOutQuad + properties: "x,y" + } + } //clip: true - delegate: HomeLauncher {} + delegate: HomeLauncher { + visible: index > 3 + } header: MouseArea { z: 999 property Item layout: appletsLayout property Item lastSpacer: spacer + property Item favoritesStrip: favoritesView width: root.width - height: mainLayout.Layout.minimumHeight + height: mainLayout.Layout.minimumHeight + property int margin: stripe.height + units.gridUnit * 2 onPressAndHold: { - plasmoid.action("configure").trigger(); + print(favoritesView.contains(mapToItem(favoritesView, mouse.x, mouse.y))) + if (!favoritesView.contains(mapToItem(favoritesView, mouse.x, mouse.y))) { + plasmoid.action("configure").trigger(); + } } ColumnLayout { id: mainLayout anchors { fill: parent - bottomMargin: stripe.height + units.gridUnit * 2 } Item { Layout.fillWidth: true Layout.minimumHeight: root.height + Layout.maximumHeight: root.height Clock { anchors { horizontalCenter: parent.horizontalCenter @@ -226,13 +440,20 @@ Item { } ColumnLayout { id: appletsLayout - Layout.minimumHeight: Math.max(root.height, Math.round(Layout.preferredHeight / root.height) * root.height) Item { id: spacer Layout.fillWidth: true Layout.fillHeight: true + Layout.minimumHeight: plasmoid.applets.length % 2 == 0 ? 0 : (root.height - margin)/2 + Layout.maximumHeight: Layout.minimumHeight } } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: margin + Layout.maximumHeight: Layout.minimumHeight + } } SatelliteStripe { id: stripe @@ -240,48 +461,42 @@ Item { property int viewPos: applicationsView.contentItem.height * applicationsView.visibleArea.yPosition y: Math.max(viewPos, - Math.min(parent.height, viewPos + root.height) - height + Math.max(0, -(parent.height - height + applicationsView.contentY))) + Math.min(parent.height, viewPos + root.height - height) + Math.max(0, -(parent.height - height + applicationsView.contentY))) PlasmaCore.Svg { id: stripeIcons imagePath: Qt.resolvedUrl("../images/homescreenicons.svg") } - Row { + GridView { + id: favoritesView + //FIXME: QQuickItem has a contains, but seems to not work + function contains(point) { + return point.x > 0 && point.x < width && point.y > 0 && point.y < height; + } anchors.fill: parent property int columns: 4 - property alias buttonHeight: stripe.height + interactive: false + flow: GridView.FlowTopToBottom + cellWidth: root.buttonHeight + cellHeight: cellWidth - HomeLauncherSvg { - id: phoneIcon - svg: stripeIcons - elementId: "phone" - callback: function() { - console.log("Start phone") + model: appListModel + delegate: HomeLauncher {} + + move: Transition { + NumberAnimation { + duration: units.longDuration + easing.type: Easing.InOutQuad + properties: "x,y" } } - - HomeLauncherSvg { - id: messagingIcon - svg: stripeIcons - elementId: "messaging" - callback: function() { console.log("Start messaging") } - } - - - HomeLauncherSvg { - id: emailIcon - svg: stripeIcons - elementId: "email" - callback: function() { console.log("Start email") } - } - - - HomeLauncherSvg { - id: webIcon - svg: stripeIcons - elementId: "web" - callback: function() { console.log("Start web") } + moveDisplaced: Transition { + NumberAnimation { + duration: units.longDuration + easing.type: Easing.InOutQuad + properties: "x,y" + } } } } @@ -310,7 +525,7 @@ Item { radius: width anchors.right: parent.right y: applicationsView.height * applicationsView.visibleArea.yPosition - opacity: scrollbarMouse.pressed || applicationsView.flicking ? 0.8 : 0 + opacity: scrollbarMouse.pressed || applicationsView.flicking || scrollDownIndicator.opacity > 0 || scrollUpIndicator.opacity > 0 ? 0.8 : 0 Behavior on opacity { NumberAnimation { duration: units.longDuration diff --git a/containments/panel/contents/code/LayoutManager.js b/containments/panel/contents/code/LayoutManager.js new file mode 100644 index 00000000..c3dc37ee --- /dev/null +++ b/containments/panel/contents/code/LayoutManager.js @@ -0,0 +1,189 @@ +/* + * Copyright 2013 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 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. + */ + +.pragma library + + +var layout; +var root; +var plasmoid; +var lastSpacer; + + +function restore() { + var configString = String(plasmoid.configuration.AppletOrder) + + //array, a cell for encoded item order + var itemsArray = configString.split(";"); + + //map applet id->order in panel + var idsOrder = new Object(); + //map order in panel -> applet pointer + var appletsOrder = new Object(); + + for (var i = 0; i < itemsArray.length; i++) { + //property name: applet id + //property value: order + idsOrder[itemsArray[i]] = i; + } + + for (var i = 0; i < plasmoid.applets.length; ++i) { + if (idsOrder[plasmoid.applets[i].id] !== undefined) { + appletsOrder[idsOrder[plasmoid.applets[i].id]] = plasmoid.applets[i]; + //ones that weren't saved in AppletOrder go to the end + } else { + appletsOrder["unordered"+i] = plasmoid.applets[i]; + } + } + + //finally, restore the applets in the correct order + for (var i in appletsOrder) { + root.addApplet(appletsOrder[i], -1, -1) + } + //rewrite, so if in the orders there were now invalid ids or if some were missing creates a correct list instead + save(); +} + +function save() { + var ids = new Array(); + for (var i = 0; i < layout.children.length; ++i) { + var child = layout.children[i]; + + if (child.applet) { + ids.push(child.applet.id); + } + } + plasmoid.configuration.AppletOrder = ids.join(';'); +} + +function removeApplet (applet) { + for (var i = layout.children.length - 1; i >= 0; --i) { + var child = layout.children[i]; + if (child.applet === applet) { + child.destroy(); + } + } +} + +//insert item2 before item1 +function insertBefore(item1, item2) { + if (item1 === item2) { + return; + } + var removed = new Array(); + + var child; + + var i; + for (i = layout.children.length - 1; i >= 0; --i) { + child = layout.children[i]; + removed.push(child); + child.parent = root; + + if (child === item1) { + break; + } + } + + item2.parent = layout; + + for (var j = removed.length - 1; j >= 0; --j) { + removed[j].parent = layout; + } + return i; +} + +//insert item2 after item1 +function insertAfter(item1, item2) { + if (item1 === item2) { + return; + } + var removed = new Array(); + + var child; + + var i; + for (i = layout.children.length - 1; i >= 0; --i) { + child = layout.children[i]; + //never ever insert after lastSpacer + if (child === lastSpacer && item1 === lastSpacer) { + removed.push(child); + child.parent = root; + break; + } else if (child === item1) { + break; + } + + removed.push(child); + child.parent = root; + } + + item2.parent = layout; + + for (var j = removed.length - 1; j >= 0; --j) { + removed[j].parent = layout; + } + return i; +} + +function insertAtIndex(item, position) { + if (position < 0 || position >= layout.children.length) { + return; + } + + //never ever insert after lastSpacer + if (layout.children[position] === lastSpacer) { + --position; + } + + var removedItems = new Array(); + + for (var i = position; i < layout.children.length; ++i) { + var child = layout.children[position]; + child.parent = root; + removedItems.push(child); + } + + item.parent = layout; + for (var i in removedItems) { + removedItems[i].parent = layout; + } +} + +function insertAtCoordinates(item, x, y) { + if (root.isHorizontal) { + y = layout.height / 2; + } else { + x = layout.width / 2; + } + var child = layout.childAt(x, y); + + if (!child || child === item) { + child = layout.children[0]; + } + item.parent = root; + + //PlasmaCore.Types.Vertical = 3 + if ((plasmoid.formFactor === 3 && y < child.y + child.height/2) || + (plasmoid.formFactor !== 3 && x < child.x + child.width/2)) { + return insertBefore(child, item); + } else { + return insertAfter(child, item); + } +} diff --git a/containments/panel/contents/config/main.xml b/containments/panel/contents/config/main.xml new file mode 100644 index 00000000..61a525b9 --- /dev/null +++ b/containments/panel/contents/config/main.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/shell/contents/views/SlidingPanel.qml b/containments/panel/contents/ui/SlidingPanel.qml similarity index 64% rename from shell/contents/views/SlidingPanel.qml rename to containments/panel/contents/ui/SlidingPanel.qml index 4ca70124..8629edd1 100644 --- a/shell/contents/views/SlidingPanel.qml +++ b/containments/panel/contents/ui/SlidingPanel.qml @@ -18,7 +18,7 @@ */ import QtQuick 2.0 -import QtQuick.Window 2.0 +import QtQuick.Window 2.2 import org.kde.plasma.core 2.0 as PlasmaCore Window { @@ -26,8 +26,10 @@ Window { flags: Qt.WindowDoesNotAcceptFocus property int offset: 0 + property int overShoot: units.gridUnit * 2 color: "transparent" + property alias contents: contentArea.data function updateState() { var delta = offset - mouseArea.startOffset; @@ -50,15 +52,17 @@ Window { MouseArea { id: mouseArea - y: units.iconSizes.small + y: 0 width: window.width height: window.height - y clip: true state: "closed" + drag.filterChildren: true property int oldMouseY: 0 property int startOffset: units.iconSizes.large; property string startState: "closed" + onPressed: { startState = state; startOffset = window.offset; @@ -67,26 +71,63 @@ Window { window.offset = startOffset; } onPositionChanged: { - window.offset = window.offset + (mouse.y - oldMouseY); + var factor = (mouse.y - oldMouseY > 0) ? (1 - Math.max(0, (slidingArea.y + overShoot) / overShoot)) : 1 + + window.offset = window.offset + (mouse.y - oldMouseY) * factor; oldMouseY = mouse.y; } - onReleased: window.updateState() + onReleased: { + if (Math.abs(window.offset - mouseArea.startOffset) < units.gridUnit && + slidingArea.y + slidingArea.height < mouse.y) { + mouseArea.state = "closed"; + } else { + window.updateState(); + } + } Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.6-Math.abs(slidingArea.y/slidingArea.height)) + } + PlasmaCore.ColorScope { id: slidingArea width: parent.width - height: parent.height + height: parent.height/1.5 y: Math.min(0, -height + window.offset) - - color: Qt.rgba(0, 0, 0, 0.7) + colorGroup: PlasmaCore.Theme.ComplementaryColorGroup Rectangle { - width: parent.width / 4 - height: units.gridUnit/2 - color: "yellow" - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - bottomMargin: units.gridUnit/2 + anchors.fill: parent + + Item { + id: contentArea + anchors { + fill: parent + topMargin: overShoot + } + } + color: PlasmaCore.ColorScope.backgroundColor + + Rectangle { + height: units.gridUnit + anchors { + left: parent.left + right: parent.right + top: parent.bottom + } + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 0.6) + } + GradientStop { + position: 0.5 + color: Qt.rgba(0, 0, 0, 0.2) + } + GradientStop { + position: 1.0 + color: "transparent" + } + } } } } @@ -103,7 +144,7 @@ Window { name: "open" PropertyChanges { target: window - offset: slidingArea.height + offset: slidingArea.height - overShoot } }, State { @@ -127,7 +168,7 @@ Window { PropertyAnimation { target: window duration: units.longDuration - easing: Easing.InOutQuad + easing.type: Easing.InOutQuad properties: "offset" } ScriptAction { diff --git a/containments/panel/contents/ui/main.qml b/containments/panel/contents/ui/main.qml new file mode 100644 index 00000000..4c33cc46 --- /dev/null +++ b/containments/panel/contents/ui/main.qml @@ -0,0 +1,309 @@ +/* + * Copyright 2015 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 + +import MeeGo.QOfono 0.2 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +import org.kde.plasma.workspace.components 2.0 as PlasmaWorkspace + +import "plasmapackage:/code/LayoutManager.js" as LayoutManager + +PlasmaCore.ColorScope { + id: root + width: 480 + height: 640 + colorGroup: PlasmaCore.Theme.ComplementaryColorGroup + + property Item toolBox + property int buttonHeight: width/4 + property bool reorderingApps: false + property QtObject expandedApplet + + Containment.onAppletAdded: { + addApplet(applet, x, y); + LayoutManager.save(); + } + + function addApplet(applet, x, y) { + var container = appletContainerComponent.createObject(layout) + container.visible = true + print("Applet added: " + applet) + + var appletWidth = applet.width; + var appletHeight = applet.height; + applet.parent = container; + container.applet = applet; + //applet.anchors.fill = container; + applet.anchors.left = container.left; + applet.anchors.right = container.right; + applet.height = units.iconSizes.medium + applet.visible = true; + container.visible = true; + + // If the provided position is valid, use it. + if (x >= 0 && y >= 0) { + var index = LayoutManager.insertAtCoordinates(container, x , y); + + // Fall through to determining an appropriate insert position. + } else { + var before = null; + + if (lastSpacer.parent === layout) { + before = lastSpacer; + } + + if (before) { + LayoutManager.insertBefore(before, container); + + // Fall through to adding at the end. + } else { + container.parent = layout; + } + + //event compress the enable of animations + //startupTimer.restart(); + } + + if (applet.Layout.fillWidth) { + lastSpacer.parent = root; + } else { + lastSpacer.parent = layout; + } + } + + Component.onCompleted: { + LayoutManager.plasmoid = plasmoid; + LayoutManager.root = root; + LayoutManager.layout = layout; + LayoutManager.restore(); + } + + RowLayout { + id: appletsLayout + Layout.minimumHeight: Math.max(root.height, Math.round(Layout.preferredHeight / root.height) * root.height) + } + + Component { + id: appletContainerComponent + Item { + property Item applet + Layout.fillWidth: true + clip: true + anchors { + left: parent.left + right: parent.right + } + height: applet && applet.expanded ? Math.max(applet.fullRepresentationItem.Layout.minimumHeight, units.iconSizes.medium) : units.iconSizes.medium + Behavior on height { + NumberAnimation { + duration: units.shortDuration + easing.type: Easing.InOutQuad + } + } + Connections { + target: applet + onExpandedChanged: { + if (root.expandedApplet) { + root.expandedApplet.expanded = false; + } + root.expandedApplet = applet; + } + } + } + } + + Component { + id: tabComponent + PlasmaComponents.TabButton { + width: parent.width / parent.children.length + height: units.iconSizes.huge + } + } + + + PlasmaCore.DataSource { + id: timeSource + engine: "time" + connectedSources: ["Local"] + interval: 60 * 1000 + } + + OfonoManager { + id: ofonoManager + onAvailableChanged: { + console.log("Ofono is " + available) + } + onModemAdded: { + console.log("modem added " + modem) + } + onModemRemoved: console.log("modem removed") + } + + OfonoNetworkRegistration { + id: netreg + Component.onCompleted: { + netreg.scan() + updateStrengthIcon() + } + + onNetworkOperatorsChanged : { + console.log("operators :"+netreg.currentOperator["Name"].toString()) + } + modemPath: ofonoManager.modems.length ? ofonoManager.modems[0] : "" + function updateStrengthIcon() { + if (netreg.strength >= 100) { + strengthIcon.source = "network-mobile-100"; + } else if (netreg.strength >= 80) { + strengthIcon.source = "network-mobile-80"; + } else if (netreg.strength >= 60) { + strengthIcon.source = "network-mobile-60"; + } else if (netreg.strength >= 40) { + strengthIcon.source = "network-mobile-40"; + } else if (netreg.strength >= 20) { + strengthIcon.source = "network-mobile-20"; + } else { + strengthIcon.source = "network-mobile-0"; + } + } + + onStrengthChanged: { + console.log("Strength changed to " + netreg.strength) + updateStrengthIcon() + } + } + + Rectangle { + parent: slidingPanel.visible ? panelContents : root + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: root.height + color: PlasmaCore.ColorScope.backgroundColor + + PlasmaCore.IconItem { + id: strengthIcon + colorGroup: PlasmaCore.ColorScope.colorGroup + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + width: height + height: parent.height + } + PlasmaComponents.Label { + anchors { + left: strengthIcon.right + verticalCenter: parent.verticalCenter + } + text: netreg.strength + "% " + netreg.name + color: PlasmaCore.ColorScope.textColor + font.pixelSize: parent.height / 2 + } + PlasmaComponents.Label { + id: clock + anchors.fill: parent + text: Qt.formatTime(timeSource.data.Local.DateTime, "hh:mm") + color: PlasmaCore.ColorScope.textColor + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + font.pixelSize: height / 2 + } + + + PlasmaWorkspace.BatteryIcon { + id: batteryIcon + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + } + width: height + height: parent.height + hasBattery: pmSource.data["Battery"]["Has Battery"] + // batteryType: "Phone" + percent: pmSource.data["Battery0"] ? pmSource.data["Battery0"]["Percent"] : 0 + + PlasmaCore.DataSource { + id: pmSource + engine: "powermanagement" + connectedSources: sources + onSourceAdded: { + disconnectSource(source); + connectSource(source); + } + onSourceRemoved: { + disconnectSource(source); + } + } + } + Rectangle { + height: units.smallSpacing/2 + color: PlasmaCore.ColorScope.highlightColor + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + } + MouseArea { + property int oldMouseY: 0 + + anchors.fill: parent + onPressed: { + oldMouseY = mouse.y; + slidingPanel.visibility = Qt.WindowFullScreen; + } + onPositionChanged: { + //var factor = (mouse.y - oldMouseY > 0) ? (1 - Math.max(0, (slidingArea.y + slidingPanel.overShoot) / slidingPanel.overShoot)) : 1 + var factor = 1; + slidingPanel.offset = slidingPanel.offset + (mouse.y - oldMouseY) * factor; + oldMouseY = mouse.y; + } + onReleased: slidingPanel.updateState(); + } + + SlidingPanel { + id: slidingPanel + width: plasmoid.availableScreenRect.width + height: plasmoid.availableScreenRect.height + contents: Item { + id: panelContents + anchors.fill: parent + clip: true + + Item { + id: lastSpacer + Layout.fillWidth: true + Layout.fillHeight: true + } + Column { + id: layout + anchors.fill: parent + spacing: units.smallSpacing + } + } + } +} diff --git a/containments/panel/metadata.desktop b/containments/panel/metadata.desktop new file mode 100644 index 00000000..34195117 --- /dev/null +++ b/containments/panel/metadata.desktop @@ -0,0 +1,18 @@ +[Desktop Entry] +Encoding=UTF-8 +Keywords= +Name=Phone Panel +Type=Service + +X-KDE-ServiceTypes=Plasma/Applet,Plasma/Containment +X-Plasma-API=declarativeappletscript +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Marco Martin +X-KDE-PluginInfo-Category= +X-KDE-PluginInfo-Email=mart@kde.org +X-KDE-PluginInfo-License=GPLv2+ +X-KDE-PluginInfo-Name=org.kde.phone.panel +X-KDE-PluginInfo-Version= +X-KDE-PluginInfo-Website= +X-Plasma-MainScript=ui/main.qml +X-Plasma-ContainmentType=Panel diff --git a/dialer/CMakeLists.txt b/dialer/CMakeLists.txt new file mode 100644 index 00000000..9927f95e --- /dev/null +++ b/dialer/CMakeLists.txt @@ -0,0 +1,5 @@ + +kpackage_install_package(package org.kde.phone.dialer genericqml) +install(FILES package/metadata.desktop DESTINATION ${XDG_APPS_INSTALL_DIR} RENAME org.kde.phone.dialer.desktop) + +add_subdirectory(src) diff --git a/dialer/package/contents/ui/Call/AnswerSwipe.qml b/dialer/package/contents/ui/Call/AnswerSwipe.qml new file mode 100644 index 00000000..278c3824 --- /dev/null +++ b/dialer/package/contents/ui/Call/AnswerSwipe.qml @@ -0,0 +1,99 @@ +/* + * 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.4 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.nemomobile.voicecall 1.0 + +MouseArea { + id: root + + signal accepted + signal rejected + + Layout.minimumHeight: units.gridUnit * 5 + Layout.fillWidth: true + property int handlePosition: (answerHandle.x + answerHandle.width/2) + drag { + target: answerHandle + axis: Drag.XAxis + minimumX: 0 + maximumX: width - answerHandle.width + } + Rectangle { + anchors.fill: parent + radius: height + color: Qt.rgba((handlePosition > root.width/2 ? 0.6 : 0)+0.2, (handlePosition < root.width/2 ? 0.6 : 0)+0.2, 0.2, Math.abs(handlePosition - (root.width/2)) / answerHandle.width/2); + + PlasmaCore.IconItem { + source: "call-start" + width: icon.width + height: width + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: units.largeSpacing + } + } + PlasmaCore.IconItem { + source: "call-stop" + width: icon.width + height: width + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + leftMargin: units.largeSpacing + } + } + + Rectangle { + id: answerHandle + x: parent.width/2 - width/2 + height: parent.height + width: height + radius: width + color: Qt.rgba(0.2, 0.8, 0.2, 1) + PlasmaCore.IconItem { + id: icon + source: "call-start" + width: parent.width * 0.7 + height: width + anchors.centerIn: parent + } + Behavior on x { + enabled: root.pressed + XAnimator { + duration: units.longDuration + easing.type: Easing.InOutQuad + } + } + } + } + onReleased: { + if (answerHandle.x <= answerHandle.width) { + root.accepted(); + } else if (answerHandle.x + answerHandle.width >= root.width - answerHandle.width) { + root.rejected(); + } + + answerHandle.x = width/2 - answerHandle.width/2 + } +} diff --git a/dialer/package/contents/ui/Call/Avatar.qml b/dialer/package/contents/ui/Call/Avatar.qml new file mode 100644 index 00000000..1c138601 --- /dev/null +++ b/dialer/package/contents/ui/Call/Avatar.qml @@ -0,0 +1,44 @@ +/* + * 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.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + + +Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: parent.height/2 + Rectangle { + height: Math.min(parent.width, parent.height) + width: height + radius: 5 + anchors.centerIn: parent + PlasmaCore.IconItem { + anchors { + fill: parent + centerIn: parent + margins: 20 + } + source: "im-user" + } + } +} diff --git a/dialer/package/contents/ui/Call/CallPage.qml b/dialer/package/contents/ui/Call/CallPage.qml new file mode 100644 index 00000000..e6032306 --- /dev/null +++ b/dialer/package/contents/ui/Call/CallPage.qml @@ -0,0 +1,201 @@ +/* + * Copyright 2014 Aaron Seigo + * 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.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.nemomobile.voicecall 1.0 + +import "../Dialpad" + +Item { + id: callPage + + state: voiceCallmanager.activeVoiceCall ? voiceCallmanager.activeVoiceCall.statusText : "disconnected" + property int status: voiceCallmanager.activeVoiceCall ? voiceCallmanager.activeVoiceCall.status : 0 + + property string providerId: voiceCallmanager.providers.id(0) + + function secondsToTimeString(seconds) { + seconds = Math.floor(seconds/1000) + var h = Math.floor(seconds / 3600); + var m = Math.floor((seconds - (h * 3600)) / 60); + var s = seconds - h * 3600 - m * 60; + if(h < 10) h = '0' + h; + if(m < 10) m = '0' + m; + if(s < 10) s = '0' + s; + return '' + h + ':' + m + ':' + s; + } + + onStatusChanged: { + if (status != 1) { + dialerButton.checked = false; + } + } + + ColumnLayout { + id: activeCallUi + spacing: 10 + + anchors { + fill: parent + margins: 20 + } + + Flickable { + id: topFlickable + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: parent.height/2 + + contentWidth: topContents.width + contentHeight: topContents.height + interactive: status == 1; + Row { + id: topContents + Avatar { + width: topFlickable.width + height: topFlickable.height + } + Dialpad { + width: topFlickable.width + height: topFlickable.height + + callback: function (string) { + if (voiceCallmanager.activeVoiceCall) { + voiceCallmanager.activeVoiceCall.sendDtmf(string); + } + } + } + } + + onMovingChanged: { + var checked = contentX > topFlickable.width/2; + + if (checked) { + topSlideAnim.to = topFlickable.width; + } else { + topSlideAnim.to = 0; + } + dialerButton.checked = checked; + topSlideAnim.running = true; + } + PropertyAnimation { + id: topSlideAnim + target: topFlickable + properties: "contentX" + duration: units.longDuration + easing.type: Easing.InOutQuad + } + } + + PlasmaComponents.Label { + Layout.fillWidth: true + Layout.minimumHeight: implicitHeight + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + font.pointSize: theme.defaultFont.pointSize * 2 + text: voiceCallmanager.activeVoiceCall ? voiceCallmanager.activeVoiceCall.lineId : "" + } + PlasmaComponents.Label { + Layout.fillWidth: true + Layout.minimumHeight: implicitHeight + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + text: { + if (!voiceCallmanager.activeVoiceCall) { + return ''; + //STATUS_DIALING + } else if (voiceCallmanager.activeVoiceCall.status == 3) { + return i18n("Calling..."); + } else if (voiceCallmanager.activeVoiceCall.duration > 0) { + return secondsToTimeString(voiceCallmanager.activeVoiceCall.duration); + } else { + return ''; + } + } + } + PlasmaComponents.ButtonRow { + opacity: status == 1 ? 1 : 0 + exclusive: false + spacing: 0 + Layout.alignment: Qt.AlignHCenter + PlasmaComponents.ToolButton { + id: muteButton + flat: false + iconSource: voiceCallmanager.isMicrophoneMuted ? "audio-volume-muted" : "audio-volume-high" + onClicked: { + voiceCallmanager.isMicrophoneMuted = !voiceCallmanager.isMicrophoneMuted; + } + } + PlasmaComponents.ToolButton { + id: dialerButton + flat: false + iconSource: "input-keyboard" + checkable: true + onCheckedChanged: { + if (checked) { + topSlideAnim.to = topFlickable.width; + } else { + topSlideAnim.to = 0; + } + topSlideAnim.running = true; + } + } + } + + + Item { + Layout.minimumHeight: units.gridUnit * 5 + Layout.fillWidth: true + + AnswerSwipe { + anchors.fill: parent + //STATUS_INCOMING + visible: status == 5 + onAccepted: { + if (voiceCallmanager.activeVoiceCall) { + voiceCallmanager.activeVoiceCall.answer(); + } + } + onRejected: { + if (voiceCallmanager.activeVoiceCall) { + voiceCallmanager.activeVoiceCall.hangup(); + } + } + } + + PlasmaComponents.Button { + anchors.fill: parent + //STATUS_INCOMING + visible: status != 5 + iconSource: "call-stop" + Layout.fillWidth: true + text: i18n("End Call") + onClicked: { + if (voiceCallmanager.activeVoiceCall) { + voiceCallmanager.activeVoiceCall.hangup(); + } + } + } + } + } +} diff --git a/dialer/package/contents/ui/Dialer/Contacts.qml b/dialer/package/contents/ui/Dialer/Contacts.qml new file mode 100644 index 00000000..6351527b --- /dev/null +++ b/dialer/package/contents/ui/Dialer/Contacts.qml @@ -0,0 +1,128 @@ +/* + * 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.0 +import QtQuick.Layouts 1.1 +import org.kde.people 1.0 as KPeople +import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + +Item { + PlasmaComponents.Label { + anchors.centerIn: parent + text: i18n("No contacts") + visible: contactsModel.count == 0 + } + + ColumnLayout { + anchors.fill: parent + //visible: contactsModel.count > 0 + + PlasmaComponents.ToolBar { + Layout.fillWidth: true + tools: RowLayout { + id: toolBarLayout + PlasmaComponents.TextField { + id: searchField + clearButtonShown: true + Layout.fillWidth: true + Layout.fillHeight: true + placeholderText: i18n("Search...") + } + } + } + + PlasmaExtras.ScrollArea { + Layout.fillWidth: true + Layout.fillHeight: true + ListView { + id: view + model: PlasmaCore.SortFilterModel { + id: filterModel + sourceModel: KPeople.PersonsModel { + id: contactsModel + } + sortRole: "display" + filterRole: "display" + filterRegExp: ".*"+searchField.text+".*" + sortOrder: Qt.AscendingOrder + } + section { + property: "display" + criteria: ViewSection.FirstCharacter + delegate: PlasmaComponents.ListItem { + id: sectionItem + sectionDelegate: true + PlasmaComponents.Label { + text: section + } + } + } + delegate: PlasmaComponents.ListItem { + RowLayout { + id: delegateLayout + + KQuickControlsAddons.QPixmapItem { + id: avatarLabel + + Layout.minimumWidth: units.iconSizes.medium + Layout.maximumWidth: Layout.minimumWidth + Layout.minimumHeight: Layout.minimumWidth + Layout.maximumHeight: Layout.minimumWidth + + pixmap: model.decoration + fillMode: KQuickControlsAddons.QPixmapItem.PreserveAspectFit + smooth: true + } + + ColumnLayout { + Layout.fillHeight: true + Layout.fillWidth: true + + PlasmaComponents.Label { + id: nickLabel + + Layout.fillWidth: true + + text: model.display + elide: Text.ElideRight + } + + PlasmaComponents.Label { + id: dataLabel + + Layout.fillWidth: true + + text: "605-909-123" + elide: Text.ElideRight + } + + } + } + MouseArea { + anchors.fill: parent + onClicked: call(12345) + } + } + } + } + } +} diff --git a/dialer/package/contents/ui/Dialer/DialPage.qml b/dialer/package/contents/ui/Dialer/DialPage.qml new file mode 100644 index 00000000..3e0c252d --- /dev/null +++ b/dialer/package/contents/ui/Dialer/DialPage.qml @@ -0,0 +1,67 @@ +/* + * 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.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +Column { + spacing: 0 + PlasmaComponents.TabGroup { + anchors { + left: parent.left + right: parent.right + } + height: parent.height - tabbar.height + History { + id: history + } + Contacts { + id: contacts + } + Dialer { + id: dialer + } + } + PlasmaComponents.TabBar { + id: tabbar + height: units.gridUnit * 5 + anchors { + left: parent.left + right: parent.right + } + tabPosition: Qt.BottomEdge + PlasmaComponents.TabButton { + iconSource: "view-history" + text: i18n("History") + tab: history + } + PlasmaComponents.TabButton { + iconSource: "view-pim-contacts" + text: i18n("Contacts") + tab: contacts + } + PlasmaComponents.TabButton { + iconSource: "input-keyboard" + text: i18n("Dialpad") + tab: dialer + } + } +} \ No newline at end of file diff --git a/dialer/package/contents/ui/Dialer/Dialer.qml b/dialer/package/contents/ui/Dialer/Dialer.qml new file mode 100644 index 00000000..da2806b7 --- /dev/null +++ b/dialer/package/contents/ui/Dialer/Dialer.qml @@ -0,0 +1,105 @@ +/* + * Copyright 2014 Aaron Seigo + * Copyright 2014 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.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.nemomobile.voicecall 1.0 +import "../Dialpad" + +Item { + id: dialer + + property alias numberEntryText: status.text + + property string providerId: voiceCallmanager.providers.id(0) + + function addNumber(number) { + status.text = status.text + number + } + + ColumnLayout { + id: dialPadArea + + anchors { + fill: parent + margins: units.largeSpacing + } + PlasmaComponents.Label { + id: status + Layout.fillWidth: true + Layout.minimumHeight: parent.height / 6 + Layout.maximumHeight: Layout.minimumHeight + horizontalAlignment: Qt.AlignRight + verticalAlignment: Qt.AlignVCenter + font.pointSize: 1024 + fontSizeMode: Text.Fit + } + + Dialpad { + callback: function (string) { + addNumber(string); + } + pressedCallback: function (string) { + voiceCallmanager.startDtmfTone(string); + } + releasedCallback: function (string) { + voiceCallmanager.stopDtmfTone(); + } + } + + RowLayout { + Layout.fillWidth: true + Layout.minimumHeight: parent.height / 6 + Layout.maximumHeight: Layout.minimumHeight + DialerIconButton { + id: callButton + Layout.minimumWidth: dialPadArea.width/3 + Layout.fillWidth: true + Layout.fillHeight: true + enabled: status.text.length > 0 + opacity: enabled ? 1 : 0.5 + source: "call-start" + callback: function() { + call(status.text); + } + } + Item { + Layout.minimumWidth: dialPadArea.width/3 + Layout.fillWidth: true + Layout.fillHeight: true + } + DialerIconButton { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumWidth: dialPadArea.width/3 + enabled: status.text.length > 0 + opacity: enabled ? 1 : 0.5 + source: "edit-clear" + callback: function(text) { + if (status.text.length > 0) { + status.text = status.text.substr(0, status.text.length - 1); + } + } + } + } + } +} diff --git a/dialer/package/contents/ui/Dialer/History.qml b/dialer/package/contents/ui/Dialer/History.qml new file mode 100644 index 00000000..ea29fd4b --- /dev/null +++ b/dialer/package/contents/ui/Dialer/History.qml @@ -0,0 +1,108 @@ +/* + * 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.4 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + +Item { + + function secondsToTimeString(seconds) { + seconds = Math.floor(seconds/1000) + var h = Math.floor(seconds / 3600); + var m = Math.floor((seconds - (h * 3600)) / 60); + var s = seconds - h * 3600 - m * 60; + if(h < 10) h = '0' + h; + if(m < 10) m = '0' + m; + if(s < 10) s = '0' + s; + return '' + h + ':' + m + ':' + s; + } + + PlasmaComponents.Label { + anchors.centerIn: parent + text: i18n("No recent calls") + visible: historyModel.count == 0 + } + + ColumnLayout { + anchors.fill: parent + visible: historyModel.count > 0 + PlasmaComponents.ToolBar { + Layout.fillWidth: true + tools: RowLayout { + id: toolBarLayout + PlasmaComponents.TabBar { + tabPosition: Qt.TopEdge + PlasmaComponents.TabButton { + iconSource: "call-start" + text: i18n("All") + onCheckedChanged: { + if (checked) { + filterModel.filterString = ""; + } + } + } + PlasmaComponents.TabButton { + iconSource: "list-remove" + text: i18n("Missed") + onCheckedChanged: { + if (checked) { + filterModel.filterString = "0"; + } + } + } + } + Item { + Layout.fillWidth: true + } + PlasmaComponents.Button { + text: i18n("Clear") + onClicked: clearHistory(); + } + } + } + PlasmaExtras.ScrollArea { + Layout.fillWidth: true + Layout.fillHeight: true + ListView { + id: view + model: PlasmaCore.SortFilterModel { + id: filterModel + sourceModel: historyModel + filterRole: "callType" + sortRole: "time" + sortOrder: Qt.DescendingOrder + } + section { + property: "date" + delegate: PlasmaComponents.ListItem { + id: sectionItem + sectionDelegate: true + PlasmaComponents.Label { + text: Qt.formatDate(section, Qt.locale().dateFormat(Locale.LongFormat)); + } + } + } + delegate: HistoryDelegate {} + } + } + } +} \ No newline at end of file diff --git a/dialer/package/contents/ui/Dialer/HistoryDelegate.qml b/dialer/package/contents/ui/Dialer/HistoryDelegate.qml new file mode 100644 index 00000000..c14cd3e2 --- /dev/null +++ b/dialer/package/contents/ui/Dialer/HistoryDelegate.qml @@ -0,0 +1,122 @@ +/* + * 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.4 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + +Item { + id: delegateParent + width: view.width + height: childrenRect.height + + Behavior on height { + SpringAnimation { spring: 5; damping: 0.3 } + } + SequentialAnimation { + id: removeAnim + XAnimator { + target: delegate + from: delegate.x + to: delegate.x > 0 ? width : -width + duration: units.longDuration + easing.type: Easing.InOutQuad + } + PropertyAnimation { + target: delegateParent + properties: "height" + to: 0 + duration: units.longDuration + easing.type: Easing.InOutQuad + } + ScriptAction { + script: removeCallFromHistory(model.originalIndex); + } + } + + XAnimator { + id: resetAnim + target: delegate + from: delegate.x + to: 0 + duration: units.longDuration + easing.type: Easing.InOutQuad + } + + PlasmaComponents.ListItem { + id: delegate + + MouseArea { + width: parent.width + height: childrenRect.height + onClicked: call(model.number); + drag.axis: Drag.XAxis + drag.target: delegate + onReleased: { + if (drag.active) { + if (delegate.x > delegate.width / 3 || delegate.x < width / -3) { + removeAnim.running = true; + } else { + resetAnim.running = true; + } + } + } + + RowLayout { + width: parent.width + //FIXME: ad hoc icons + PlasmaCore.IconItem { + width: units.iconSizes.medium + height: width + source: { + switch (model.callType) { + case 0: + return "list-remove"; + case 1: + return "go-down"; + case 2: + return "go-up"; + } + } + } + ColumnLayout { + PlasmaComponents.Label { + text: "Name (todo)" + } + PlasmaComponents.Label { + text: i18n("Number: %1", model.number) + Layout.fillWidth: true + } + } + ColumnLayout { + PlasmaComponents.Label { + Layout.alignment: Qt.AlignRight + text: Qt.formatTime(model.time, Qt.locale().timeFormat(Locale.ShortFormat)); + } + PlasmaComponents.Label { + Layout.alignment: Qt.AlignRight + text: i18n("Duration: %1", secondsToTimeString(model.duration)); + } + } + } + } + } +} diff --git a/dialer/package/contents/ui/Dialpad/DialerButton.qml b/dialer/package/contents/ui/Dialpad/DialerButton.qml new file mode 100644 index 00000000..75931d93 --- /dev/null +++ b/dialer/package/contents/ui/Dialpad/DialerButton.qml @@ -0,0 +1,125 @@ +/* + * Copyright 2014 Aaron Seigo + * Copyright 2014 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.4 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +PlasmaComponents.Label { + Layout.fillWidth: true + Layout.fillHeight: true + + //This is 0 to override the Label default height that would cause a binding loop + height: 0 + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + font.pointSize: 1024 + fontSizeMode: Text.VerticalFit + + property alias sub: longHold.text + property var callback + property var pressedCallback + property var releasedCallback + + Rectangle { + anchors.fill: parent + z: -1 + color: PlasmaCore.ColorScope.highlightColor + radius: units.smallSpacing + opacity: mouse.pressed ? 0.4 : 0 + Behavior on opacity { + OpacityAnimator { + duration: units.longDuration + easing.type: Easing.InOutQuad + } + } + } + + MouseArea { + id: mouse + anchors.fill: parent + onPressed: { + if (pressedCallback) { + pressedCallback(parent.text); + } else if (pad.pressedCallback) { + pad.pressedCallback(parent.text); + } + } + onReleased: { + if (releasedCallback) { + releasedCallback(parent.text); + } else if (pad.releasedCallback) { + pad.releasedCallback(parent.text); + } + } + onCanceled: { + if (releasedCallback) { + releasedCallback(parent.text); + } else if (pad.releasedCallback) { + pad.releasedCallback(parent.text); + } + } + + onClicked: { + if (callback) { + callback(parent.text); + } else if (pad.callback) { + pad.callback(parent.text); + } + } + + onPressAndHold: { + var text; + if (longHold.visible) { + text = longHold.text; + } else { + text = parent.text; + } + + if (text.length > 1) { + return; + } + + if (callback) { + callback(text); + } else if (pad.callback) { + pad.callback(text); + } + } + } + + PlasmaComponents.Label { + id: longHold + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: units.largeSpacing + } + height: parent.height * 0.4 + width: parent.width / 4 + verticalAlignment: Qt.AlignVCenter + visible: text.length > 0 + opacity: 0.6 + + font.pointSize: 1024 + fontSizeMode: Text.Fit + } +} diff --git a/dialer/package/contents/ui/Dialpad/DialerIconButton.qml b/dialer/package/contents/ui/Dialpad/DialerIconButton.qml new file mode 100644 index 00000000..a88a5280 --- /dev/null +++ b/dialer/package/contents/ui/Dialpad/DialerIconButton.qml @@ -0,0 +1,96 @@ +/* + * Copyright 2014 Aaron Seigo + * Copyright 2014 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.4 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +Item { + id: buttonRoot + + Layout.fillWidth: true + Layout.fillHeight: true + + property var callback + property var pressedCallback + property var releasedCallback + property string sub + property alias source: icon.source + property alias text: label.text + + Rectangle { + anchors.fill: parent + z: -1 + color: PlasmaCore.ColorScope.highlightColor + radius: units.smallSpacing + opacity: mouse.pressed ? 0.4 : 0 + Behavior on opacity { + OpacityAnimator { + duration: units.longDuration + easing.type: Easing.InOutQuad + } + } + } + + Row { + anchors.centerIn: parent + PlasmaCore.IconItem { + id: icon + anchors.verticalCenter: parent.verticalCenter + width: height + height: buttonRoot.height * 0.6 + } + PlasmaComponents.Label { + id: label + height: buttonRoot.height + anchors.verticalCenter: parent.verticalCenter + font.pointSize: 1024 + fontSizeMode: Text.VerticalFit + } + } + + MouseArea { + id: mouse + anchors.fill: parent + onClicked: { + if (callback) { + callback(parent.text); + } else { + addNumber(parent.text); + } + } + + onPressAndHold: { + var text; + if (longHold.visible) { + text = longHold.text; + } else { + text = parent.text; + } + + if (callback) { + callback(text); + } else if (pad.callback) { + pad.callback(text); + } + } + } +} diff --git a/dialer/package/contents/ui/Dialpad/Dialpad.qml b/dialer/package/contents/ui/Dialpad/Dialpad.qml new file mode 100644 index 00000000..9fa5c49d --- /dev/null +++ b/dialer/package/contents/ui/Dialpad/Dialpad.qml @@ -0,0 +1,55 @@ +/* + * Copyright 2014 Aaron Seigo + * Copyright 2014 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.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +GridLayout { + id: pad + columns: 3 + + property var callback + property var pressedCallback + property var releasedCallback + + property int buttonHeight: parent.height / 6 + + Layout.fillWidth: true + Layout.fillHeight: true + + DialerButton { id: one; text: "1" } + DialerButton { text: "2"; sub: "ABC" } + DialerButton { text: "3"; sub: "DEF" } + + DialerButton { text: "4"; sub: "GHI" } + DialerButton { text: "5"; sub: "JKL" } + DialerButton { text: "6"; sub: "MNO" } + + DialerButton { text: "7"; sub: "PQRS" } + DialerButton { text: "8"; sub: "TUV" } + DialerButton { text: "9"; sub: "WXYZ" } + + DialerButton { text: "*"; } + DialerButton { text: "0"; sub: "+"; } + DialerButton { text: "#" } +} + diff --git a/dialer/package/contents/ui/main.qml b/dialer/package/contents/ui/main.qml new file mode 100644 index 00000000..01a35fa4 --- /dev/null +++ b/dialer/package/contents/ui/main.qml @@ -0,0 +1,296 @@ +/** + * Copyright 2014 Aaron Seigo + * Copyright 2014 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 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.3 +import QtQuick.Controls 1.3 +import QtQuick.Layouts 1.1 +import QtQuick.LocalStorage 2.0 +import org.nemomobile.voicecall 1.0 +import MeeGo.QOfono 0.2 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras + +ApplicationWindow { + id: root + +//BEGIN PROPERTIES + width: 600 + height: 800 + + property int status: voiceCallmanager.activeVoiceCall ? voiceCallmanager.activeVoiceCall.status : 0 + //keep track of the status we were in + property int previousStatus + //keep track if we were visible when ringing + property bool wasVisible + //support a single provider for now + property string providerId: voiceCallmanager.providers.id(0) + //was the last call an incoming one? + property bool isIncoming +//END PROPERTIES + +//BEGIN SIGNAL HANDLERS + onStatusChanged: { + //STATUS_ACTIVE + if (status == 1) { + root.isIncoming = voiceCallmanager.activeVoiceCall.isIncoming; + //STATUS_INCOMING + } else if (status == 5) { + wasVisible = root.visible; + root.visible = true; + dialerUtils.notifyRinging(); + //Was STATUS_INCOMING now is STATUS_DISCONNECTED: Missed call! + } else if (status == 7 && previousStatus == 5) { + var prettyDate = Qt.formatTime(voiceCallmanager.activeVoiceCall.startedAt, Qt.locale().timeFormat(Locale.ShortFormat)); + dialerUtils.notifyMissedCall(voiceCallmanager.activeVoiceCall.lineId, i18n("%1 called at %2", voiceCallmanager.activeVoiceCall.lineId, prettyDate)); + root.visible = wasVisible; + insertCallInHistory(voiceCallmanager.activeVoiceCall.lineId, 0, 0); + } else if (status == 7) { + insertCallInHistory(voiceCallmanager.activeVoiceCall.lineId, voiceCallmanager.activeVoiceCall.duration, isIncoming ? 1 : 2); + } + + if (status != 5) { + dialerUtils.stopRinging(); + } + + previousStatus = status; + } + + Connections { + target: dialerUtils + onMissedCallsActionTriggered: { + root.visible = true; + } + } + + onVisibleChanged: { + //reset missed calls if the status is not STATUS_INCOMING when got visible + if (visible && status != 5) { + dialerUtils.resetMissedCalls(); + } + } +//END SIGNAL HANDLERS + +//BEGIN FUNCTIONS + function call(number) { + if (!voiceCallmanager.activeVoiceCall) { + console.log("Calling: " + providerId + " " + number); + voiceCallmanager.dial(providerId, number); + + } else { + console.log("Hanging up: " + voiceCallmanager.activeVoiceCall.lineId); + status.text = ''; + var call = voiceCallmanager.activeVoiceCall; + if (call) { + call.hangup(); + } + } + } + + function insertCallInHistory(number, duration, callType) { + //DATABSE + var db = LocalStorage.openDatabaseSync("PlasmaPhoneDialer", "1.0", "Call history of the Plasma Phone dialer", 1000000); + + db.transaction( + function(tx) { + var rs = tx.executeSql("INSERT INTO History VALUES(NULL, ?, datetime('now'), ?, ? )", [number, duration, callType]); + + var rs = tx.executeSql('SELECT * FROM History where id=?', [rs.insertId]); + + for(var i = 0; i < rs.rows.length; i++) { + var row = rs.rows.item(i); + row.date = Qt.formatDate(row.time, "yyyy-MM-dd"); + row.originalIndex = historyModel.count; + historyModel.append(row); + } + } + ) + } + + //index is historyModel row number, not db id and not sortmodel row number + function removeCallFromHistory(index) { + var item = historyModel.get(index); + + if (!item) { + return; + } + + var db = LocalStorage.openDatabaseSync("PlasmaPhoneDialer", "1.0", "Call history of the Plasma Phone dialer", 1000000); + + db.transaction( + function(tx) { + tx.executeSql("DELETE from History WHERE id=?", [item.id]); + } + ) + + historyModel.remove(index); + } + + function clearHistory() { + var db = LocalStorage.openDatabaseSync("PlasmaPhoneDialer", "1.0", "Call history of the Plasma Phone dialer", 1000000); + + db.transaction( + function(tx) { + tx.executeSql("DELETE from History"); + } + ) + + historyModel.clear(); + } + +//END FUNCTIONS + +//BEGIN DATABASE + Component.onCompleted: { + //HACK: make sure activeVoiceCall is loaded if already existing + voiceCallmanager.voiceCalls.onVoiceCallsChanged(); + voiceCallmanager.onActiveVoiceCallChanged(); + + //DATABSE + var db = LocalStorage.openDatabaseSync("PlasmaPhoneDialer", "1.0", "Call history of the Plasma Phone dialer", 1000000); + + db.transaction( + function(tx) { + // Create the database if it doesn't already exist + //callType: wether is incoming, outgoing, unanswered + tx.executeSql('CREATE TABLE IF NOT EXISTS History(id INTEGER PRIMARY KEY AUTOINCREMENT, number TEXT, time DATETIME, duration INTEGER, callType INTEGER)'); + + var rs = tx.executeSql('SELECT * FROM History'); + + for(var i = 0; i < rs.rows.length; i++) { + var row = rs.rows.item(i); + row.date = Qt.formatDate(row.time, "yyyy-MM-dd"); + row.originalIndex = historyModel.count; + historyModel.append(row); + } + } + ) + } +//END DATABASE + +//BEGIN MODELS + ListModel { + id: historyModel + } + + OfonoManager { + id: ofonoManager + onAvailableChanged: { + console.log("Ofono is " + available) + } + onModemAdded: { + console.log("modem added " + modem) + } + onModemRemoved: console.log("modem removed") + } + + OfonoConnMan { + id: ofono1 + Component.onCompleted: { + console.log(ofonoManager.modems) + } + modemPath: ofonoManager.modems.length > 0 ? ofonoManager.modems[0] : "" + } + + OfonoModem { + id: modem1 + modemPath: ofonoManager.modems.length > 0 ? ofonoManager.modems[0] : "" + + } + + OfonoContextConnection { + id: context1 + contextPath : ofono1.contexts.length > 0 ? ofono1.contexts[0] : "" + Component.onCompleted: { + print("Context Active: " + context1.active) + } + onActiveChanged: { + print("Context Active: " + context1.active) + } + } + + OfonoNetworkRegistration { + id: netreg + Component.onCompleted: { + netreg.scan() + } + + onNetworkOperatorsChanged : { + console.log("operators :"+netreg.currentOperator["Name"].toString()) + } + modemPath: ofonoManager.modems.length ? ofonoManager.modems[0] : "" + } + + OfonoNetworkOperator { + id: netop + } + + VoiceCallManager { + id: voiceCallmanager + + onActiveVoiceCallChanged: { + if (activeVoiceCall) { + //main.activeVoiceCallPerson = people.personByPhoneNumber(activeVoiceCall.lineId); + // dialerOverlay.item.numberEntryText = activeVoiceCall.lineId; + + } else { + // dialerOverlay.item.numberEntryText = ''; + + //main.activeVoiceCallPerson = null; + } + } + + onError: { + console.log('*** QML *** VCM ERROR: ' + message); + } + } + +//END MODELS + +//BEGIN UI + PlasmaExtras.ConditionalLoader { + anchors.fill: parent + when: root.visible && root.status == 0 + source: Qt.resolvedUrl("Dialer/DialPage.qml") + z: root.status == 0 ? 2 : 0 + opacity: root.status == 0 ? 1 : 0 + Behavior on opacity { + OpacityAnimator { + duration: units.shortDuration + easing.type: Easing.InOutQuad + } + } + } + + PlasmaExtras.ConditionalLoader { + anchors.fill: parent + when: root.status > 0 + source: Qt.resolvedUrl("Call/CallPage.qml") + opacity: root.status > 0 ? 1 : 0 + z: root.status > 0 ? 2 : 0 + Behavior on opacity { + OpacityAnimator { + duration: units.shortDuration + easing.type: Easing.InOutQuad + } + } + } + +//END UI +} diff --git a/dialer/package/metadata.desktop b/dialer/package/metadata.desktop new file mode 100644 index 00000000..e263428d --- /dev/null +++ b/dialer/package/metadata.desktop @@ -0,0 +1,21 @@ +[Desktop Entry] +Comment=Plasma phone dialer +Encoding=UTF-8 +Keywords= +Name=Phone +Type=Application +Icon=call-start +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Marco Martin +X-KDE-PluginInfo-Category=Communications +X-KDE-PluginInfo-Email=mart@kde.org +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=org.kde.phone.dialer +X-KDE-PluginInfo-Version= +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=KPackage/Generic +Exec=plasmaphonedialer %u + +X-Plasma-MainScript=ui/main.qml +X-Plasma-RemoteLocation= +MimeType=x-scheme-handler/call diff --git a/dialer/src/CMakeLists.txt b/dialer/src/CMakeLists.txt new file mode 100644 index 00000000..c2980082 --- /dev/null +++ b/dialer/src/CMakeLists.txt @@ -0,0 +1,26 @@ + + +set(plasmaphonedialer_SRCS + main.cpp + dialerutils.cpp +) + +add_executable(plasmaphonedialer ${plasmaphonedialer_SRCS}) +target_compile_definitions(plasmaphonedialer PRIVATE -DPROJECT_VERSION="${PROJECT_VERSION}") + +#find_package(ActiveApp REQUIRED) + +target_link_libraries(plasmaphonedialer + Qt5::Gui + Qt5::Quick + Qt5::Widgets + KF5::Declarative + KF5::I18n + KF5::Package + KF5::QuickAddons + KF5::DBusAddons + KF5::Notifications +) + +install(TARGETS plasmaphonedialer ${INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES plasma_dialer.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFY5RCDIR}) diff --git a/dialer/src/dialerutils.cpp b/dialer/src/dialerutils.cpp new file mode 100644 index 00000000..7c3c7eeb --- /dev/null +++ b/dialer/src/dialerutils.cpp @@ -0,0 +1,95 @@ +/* + * Copyright 2015 Marco Martin + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License version 2 as published by the Free Software Foundation. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "dialerutils.h" + +#include + +#include + +DialerUtils::DialerUtils(QObject *parent) +: QObject(parent), + m_missedCalls(0) +{ +} + +DialerUtils::~DialerUtils() +{ +} + +void DialerUtils::notifyMissedCall(const QString &caller, const QString &description) +{ + qWarning() << "Missed Call."; + + ++m_missedCalls; + if (!m_callsNotification) { + m_callsNotification = new KNotification("callMissed", KNotification::Persistent, 0); + } + m_callsNotification->setComponentName("plasma_dialer"); + m_callsNotification->setIconName("call-start"); + if (m_missedCalls == 1) { + m_callsNotification->setTitle(i18n("Missed call from %1", caller)); + m_callsNotification->setText(description); + } else { + m_callsNotification->setTitle(i18n("%1 calls missed", m_missedCalls)); + m_callsNotification->setText(i18n("Last call: %1", description)); + } + + QStringList actions; + actions.append(i18n("View")); + m_callsNotification->setActions(actions); + QObject::connect(m_callsNotification.data(), &KNotification::action1Activated, + [=]() { + qWarning()<<"View action activated"; + emit missedCallsActionTriggered(); + resetMissedCalls(); + }); + + if (m_missedCalls == 1) { + m_callsNotification->sendEvent(); + } else { + m_callsNotification->update(); + } +} + +void DialerUtils::resetMissedCalls() +{ + m_missedCalls = 0; + if (m_callsNotification) { + m_callsNotification->close(); + } + m_callsNotification.clear(); +} + +void DialerUtils::notifyRinging() +{ + if (!m_ringingNotification) { + m_ringingNotification = new KNotification("ringing", KNotification::Persistent|KNotification::LoopSound, 0); + m_ringingNotification->setComponentName("plasma_dialer"); + } + m_ringingNotification->sendEvent(); +} + +void DialerUtils::stopRinging() +{ + if (m_ringingNotification) { + m_ringingNotification->close(); + } +} + +#include "moc_dialerutils.cpp" diff --git a/dialer/src/dialerutils.h b/dialer/src/dialerutils.h new file mode 100644 index 00000000..41cc218f --- /dev/null +++ b/dialer/src/dialerutils.h @@ -0,0 +1,49 @@ +/* + * Copyright 2015 Marco Martin + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License version 2 as published by the Free Software Foundation. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef DIALERUTILS_H +#define DIALERUTILS_H + +#include +#include +#include + +class DialerUtils : public QObject +{ + Q_OBJECT +public: + + DialerUtils(QObject *parent = 0); + virtual ~DialerUtils(); + + Q_INVOKABLE void notifyMissedCall(const QString &caller, const QString &description); + Q_INVOKABLE void resetMissedCalls(); + Q_INVOKABLE void notifyRinging(); + Q_INVOKABLE void stopRinging(); + +Q_SIGNALS: + void missedCallsActionTriggered(); + +private: + QPointer m_callsNotification; + QPointer m_ringingNotification; + int m_missedCalls; +}; + + +#endif diff --git a/dialer/src/main.cpp b/dialer/src/main.cpp new file mode 100644 index 00000000..623c4c95 --- /dev/null +++ b/dialer/src/main.cpp @@ -0,0 +1,130 @@ +/* + * 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 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 + +#include "dialerutils.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + QCommandLineParser parser; + QApplication app(argc, argv); + + app.setQuitOnLastWindowClosed(false); + KDBusService service(KDBusService::Unique); + + const QString description = i18n("Plasma Phone Dialer"); + const char version[] = PROJECT_VERSION; + + app.setApplicationVersion(version); + parser.addVersionOption(); + parser.addHelpOption(); + parser.setApplicationDescription(description); + + QCommandLineOption daemonOption(QStringList() << QStringLiteral("d") << + QStringLiteral("daemon"), + i18n("Daemon mode. run without displaying anything.")); + + parser.addPositionalArgument("number", i18n("Call the given number")); + + parser.addOption(daemonOption); + + parser.process(app); + + const QString packagePath("org.kde.phone.dialer"); + + //usually we have an ApplicationWindow here, so we do not need to create a window by ourselves + KDeclarative::QmlObject *obj = new KDeclarative::QmlObject(); + obj->setTranslationDomain(packagePath); + obj->setInitializationDelayed(true); + obj->loadPackage(packagePath); + obj->engine()->rootContext()->setContextProperty("commandlineArguments", parser.positionalArguments()); + + DialerUtils *dialerUtils = new DialerUtils; + obj->engine()->rootContext()->setContextProperty("dialerUtils", QVariant::fromValue(dialerUtils)); + + obj->completeInitialization(); + + if (!obj->package().metadata().isValid()) { + return -1; + } + + KPluginMetaData data = obj->package().metadata(); + // About data + KAboutData aboutData(data.pluginId(), data.name(), data.version(), data.description(), KAboutLicense::byKeyword(data.license()).key()); + + for (auto author : data.authors()) { + aboutData.addAuthor(author.name(), author.task(), author.emailAddress(), author.webAddress(), author.ocsUsername()); + } + + //The root is not a window? + //have to use a normal QQuickWindow since the root item is already created + QWindow *window = qobject_cast(obj->rootObject()); + if (window) { + QObject::connect(&service, &KDBusService::activateRequested, [=](const QStringList &arguments, const QString &workingDirectory) { + Q_UNUSED(workingDirectory); + window->show(); + window->requestActivate(); + if (arguments.length() > 0) { + QString numberArg = arguments[1]; + if (numberArg.startsWith("call://")) { + numberArg = numberArg.mid(7); + } + obj->rootObject()->metaObject()->invokeMethod(obj->rootObject(), "call", Q_ARG(QVariant, numberArg)); + } + }); + if (!parser.isSet(daemonOption)) { + window->show(); + window->requestActivate(); + } + window->setTitle(obj->package().metadata().name()); + window->setIcon(QIcon::fromTheme(obj->package().metadata().iconName())); + + if (!parser.positionalArguments().isEmpty()) { + QString numberArg = parser.positionalArguments().first(); + if (numberArg.startsWith("call://")) { + numberArg = numberArg.mid(7); + } + qWarning() << "Calling" << numberArg; + obj->rootObject()->metaObject()->invokeMethod(obj->rootObject(), "call", Q_ARG(QVariant, numberArg)); + } + } else { + qWarning() << "Error loading the ApplicationWindow"; + } + + return app.exec(); +} + diff --git a/dialer/src/plasma_dialer.notifyrc b/dialer/src/plasma_dialer.notifyrc new file mode 100644 index 00000000..34ec63b5 --- /dev/null +++ b/dialer/src/plasma_dialer.notifyrc @@ -0,0 +1,15 @@ +[Global] +IconName=call-start +Comment=Phone + +[Event/callMissed] +Name=Missed Call +Comment=A call has been missed +Action=Popup + +[Event/ringing] +Name=Ringing +Comment=The phone is ringing +Action=Sound +Sound=KDE-Sys-Question.ogg + diff --git a/phonebook/contents/ui/ContactsList.qml b/phonebook/contents/ui/ContactsList.qml new file mode 100644 index 00000000..2844be5e --- /dev/null +++ b/phonebook/contents/ui/ContactsList.qml @@ -0,0 +1,187 @@ +/* + * Copyright 2015 Martin Klapetek + * + * 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 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.0 +import QtQuick.Controls 1.3 +import QtQuick.Layouts 1.1 +import org.kde.people 1.0 as KPeople +import org.kde.kquickcontrolsaddons 2.0 as ExtraComponents +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + + +PlasmaExtras.ScrollArea { + anchors.fill: parent + verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff + + contentItem: ListView { + id: contactsList + + property bool delegateSelected: false + + + section.property: "display" + section.criteria: ViewSection.FirstCharacter + clip: true + model: PlasmaCore.SortFilterModel { + sortRole: "display" + sourceModel: KPeople.PersonsModel { + id: contactsModel + } + } + + boundsBehavior: Flickable.StopAtBounds + highlight: PlasmaComponents.Highlight { + hover: contactsList.focus + } + highlightMoveDuration: 0 + + delegate: PlasmaComponents.ListItem { + height: units.gridUnit * 6 + enabled: true + clip: true + opacity: contactsList.delegateSelected && contactsList.currentIndex != index ? 0.4 : 1 + + onClicked: { + if (contactsList.delegateSelected) { + contactsList.currentIndex = -1; + contactsList.delegateSelected = false; + } else { + contactsList.currentIndex = index; + contactsList.delegateSelected = true; + } + + contactsList.toggleOverlayButtons(contactsList.delegateSelected); + } + + + RowLayout { + id: mainLayout + anchors.fill: parent + + ExtraComponents.QPixmapItem { + id: avatarLabel + + Layout.maximumWidth: parent.height + Layout.minimumWidth: parent.height + Layout.fillHeight: true + + pixmap: model.decoration + fillMode: ExtraComponents.QPixmapItem.PreserveAspectFit + smooth: true + } + + ColumnLayout { + Layout.fillHeight: true + Layout.fillWidth: true + + Label { + id: nickLabel + + Layout.fillWidth: true + + text: model.display + elide: Text.ElideRight + } + + Label { + id: dataLabel + + Layout.fillWidth: true + + text: "605-909-123" + elide: Text.ElideRight + } + + } + } + } + + function toggleOverlayButtons(show) { + if (show) { + settingsRect.parent = contactsList.currentItem + settingsRect.visible = true; + + callRect.parent = contactsList.currentItem + callRect.visible = true; + } else { + settingsRect.visible = false; + callRect.visible = false; + } + } + + Rectangle { + id: settingsRect + height: units.gridUnit * 6 + width: height + units.gridUnit * 2 + radius: 45 + z: 100 + visible: false + color: "lightblue" + + anchors { + left: parent.left + leftMargin: -width/2 + verticalCenter: parent.verticalCenter + } + + + PlasmaCore.IconItem { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: parent.height / 4 + source: "configure-shortcuts" + } + } + + Rectangle { + id: callRect + height: settingsRect.height + width: settingsRect.width + radius: height + z: 100 + visible: false + color: "lightgreen" + + anchors { + right: parent.right + rightMargin: -width/2 + verticalCenter: parent.verticalCenter + } + + PlasmaCore.IconItem { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: parent.height / 4 + source: "call-start" + } + MouseArea { + anchors.fill: parent + //TODO: needs the proper number + onClicked: Qt.openUrlExternally("call://" + "605909123") + } + } + + CustomSectionScroller { + listView: contactsList + } + + } +} diff --git a/phonebook/contents/ui/CustomSectionScroller.qml b/phonebook/contents/ui/CustomSectionScroller.qml new file mode 100644 index 00000000..287b3111 --- /dev/null +++ b/phonebook/contents/ui/CustomSectionScroller.qml @@ -0,0 +1,205 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the Qt Components project. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 + +import "private/SectionScroller.js" as Sections +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.plasma.components 2.0 as PlasmaComponents + +/** + * Similar to a ScrollBar or a ScrollDecorator. + * + * It's interactive and works on ListViews that have section.property set, so + * its contents are categorized. + * + * An indicator will say to what category the user scrolled to. Useful for + * things like address books or things sorted by date. Don't use with models + * too big (thousands of items) because it implies loading all the items to + * memory, as well loses precision. + * + * @inherit QtQuick.Item + */ +Item { + id: root + + /** + * The listview the sectionScroller will operate on. This component doesn't + * work with Flickable or GridView. + */ + property ListView listView + + onListViewChanged: { + if (listView && listView.model) + internal.initDirtyObserver(); + } + + Connections { + target: listView + onModelChanged: { + if (listView && listView.model) { + internal.initDirtyObserver() + } + } + } + + implicitWidth: scrollBar.implicitWidth + Behavior on opacity { + NumberAnimation { + duration: units.longDuration + } + } + + anchors { + right: listView.right + top: listView.top + bottom: listView.bottom + } + + + PlasmaComponents.RangeModel { + id: range + + minimumValue: 0 + maximumValue: Math.max(0, listView.contentHeight - listView.height) + stepSize: 0 + //inverted: true + positionAtMinimum: root.width*2 + positionAtMaximum: root.height - root.width*2 + value: listView.contentY + onPositionChanged: { + var section = Sections.closestSection(position/listView.height); + if (section) { + if (listView.section.criteria == ViewSection.FirstCharacter) { + sectionLabel.text = section[0]; + } else { + sectionLabel.text = section; + } + } + } + + } + + PlasmaComponents.ScrollBar { + id: scrollBar + flickableItem: listView + anchors.fill: parent + interactive: true + } + PlasmaCore.FrameSvgItem { + id: tooltip + imagePath: "widgets/background" + width: units.gridUnit * 5 + margins.left + margins.right + height: sectionLabel.height + subtitle.height + margins.top + margins.bottom + + ColumnLayout { + anchors.centerIn: parent + + PlasmaExtras.Title { + id: sectionLabel + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: Text.AlignHCenter + } + + PlasmaComponents.Label { + id: subtitle + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + visible: text.length > 0 + text: "Thursday, 7th" + } + + } + y: 0 + x: -listView.width/2 - width/2 + + opacity: sectionLabel.text && scrollBar.pressed ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: units.longDuration + } + } + } + + + Timer { + id: dirtyTimer + interval: 250 + onTriggered: { + Sections.initSectionData(listView); + internal.modelDirty = false; + tooltip.visible = Sections._sections.length > 1 + } + } + QtObject { + id: internal + + property bool modelDirty: false + function initDirtyObserver() { + Sections.initSectionData(listView); + tooltip.visible = Sections._sections.length > 1 + function dirtyObserver() { + if (!internal.modelDirty) { + internal.modelDirty = true; + dirtyTimer.running = true; + } + } + + if (listView.model.countChanged) + listView.model.countChanged.connect(dirtyObserver); + + if (listView.model.itemsChanged) + listView.model.itemsChanged.connect(dirtyObserver); + + if (listView.model.itemsInserted) + listView.model.itemsInserted.connect(dirtyObserver); + + if (listView.model.itemsMoved) + listView.model.itemsMoved.connect(dirtyObserver); + + if (listView.model.itemsRemoved) + listView.model.itemsRemoved.connect(dirtyObserver); + } + } + Accessible.role: Accessible.ScrollBar +} diff --git a/phonebook/contents/ui/main.qml b/phonebook/contents/ui/main.qml new file mode 100644 index 00000000..b0d3cd4b --- /dev/null +++ b/phonebook/contents/ui/main.qml @@ -0,0 +1,61 @@ +/* + * Copyright 2015 Martin Klapetek + * + * 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 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.0 +import QtQuick.Controls 1.3 +import QtQuick.Layouts 1.1 + +ApplicationWindow { + width: 720 + height: 1280 + visible: true + + toolBar: ToolBar { + RowLayout { + anchors.fill: parent + Layout.fillWidth: true + + ToolButton { + text: i18n("Settings") + iconName: "call-start" + } + + ToolButton { + text: i18n("Recent") + iconName: "appointment-new" + } + + ToolButton { + text: i18n("Alphabetical") + iconName: "im-user" + } + } + } + + ColumnLayout { + anchors.fill: parent + + ContactsList { + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + +} diff --git a/phonebook/contents/ui/private/SectionScroller.js b/phonebook/contents/ui/private/SectionScroller.js new file mode 100644 index 00000000..7b987fed --- /dev/null +++ b/phonebook/contents/ui/private/SectionScroller.js @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the Qt Components project. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** $QT_END_LICENSE$ +** +****************************************************************************/ + +var _sectionData = []; +var _sections = []; + +function initSectionData(list) { + if (!list || !list.model) return; + _sectionData = []; + _sections = []; + var current = ""; + var prop = list.section.property; + + for (var i = 0, count = list.model.count; i < count; i++) { + var item = list.model.get(i); + if (item[prop] !== current) { + current = item[prop]; + _sections.push(current); + _sectionData.push({ index: i, header: current }); + } + } +} + +function closestSection(pos) { + var tmp = (_sections.length) * pos; + var val = Math.ceil(tmp) // TODO: better algorithm + val = val < 2 ? 1 : val; + return _sections[val-1]; +} + +function indexOf(sectionName) { + var val = _sectionData[_sections.indexOf(sectionName)].index; + return val === 0 || val > 0 ? val : -1; +} diff --git a/phonebook/metadata.desktop b/phonebook/metadata.desktop new file mode 100644 index 00000000..48d1aa1e --- /dev/null +++ b/phonebook/metadata.desktop @@ -0,0 +1,20 @@ +[Desktop Entry] +Comment= +Encoding=UTF-8 +Keywords= +Name=Phonebook +Type=Application +Icon=package_toys +Exec=kpackagelauncherqml -a org.kde.phone.phonebook +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Martin Klapetek +X-KDE-PluginInfo-Category=Miscellaneous +X-KDE-PluginInfo-Email=mklapetek@kde.org +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=org.kde.phone.phonebook +X-KDE-PluginInfo-Version= +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=KPackage/GenericQML + +X-Plasma-MainScript=ui/main.qml +X-Plasma-RemoteLocation= diff --git a/prototypes/gridmultitasking/main.qml b/prototypes/gridmultitasking/main.qml new file mode 100644 index 00000000..27873997 --- /dev/null +++ b/prototypes/gridmultitasking/main.qml @@ -0,0 +1,253 @@ + +import QtQuick 2.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +Rectangle { + id: root + width: 300 + height: 500 + property Item currentApp + state: "switcher" + + Text { + anchors.centerIn: parent + text: "Homescreen" + } + + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.6 * Math.min(1, mainFlickable.contentY/(root.height*2)) + } + + Flickable { + id: mainFlickable + //Scale adjusted in the 0-1 range + property real zoomFactor: Math.max(mainFlickable.scale/0.5, 1) - 1 + width: root.width * 2 + 5 + height: root.height * 2 + 5 + + scale: 0.5 + contentWidth: width + contentHeight: mainContent.height + Behavior on scale { + NumberAnimation { + duration: units.longDuration + easing.type: Easing.InOutQuad + } + } + onMovingChanged: { + if (!moving && contentY < root.height * 2) { + root.state = "dragging"; + } + if (contentY < root.height) { + root.state = "homescreen" + } else { + root.state = "switcher" + } + } + + Item { + id: mainContent + width: parent.width + height: flow.y + flow.height + root.height + Flow { + id: flow + anchors { + left: parent.left + right: parent.right + } + y: root.height*2 + spacing: 5 + Repeater { + model: 5 + delegate: Rectangle { + id: appRect + color: "red" + width: root.width + height: root.height + MouseArea { + anchors.fill: parent + onClicked: { + root.state = "scrolling" + root.currentApp = appRect + root.state = "app" + } + Text { + anchors.centerIn: parent + text: "App " + modelData + } + } + } + } + } + } + } + Rectangle { + id: bottomBar + z: 99 + color: "blue" + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: 50 + MouseArea { + anchors.fill: parent + property int oldY + property real startY + onClicked: { + root.state = "switcher" + } + onPressed: { + if (root.state == "app") { + root.state = "zooming"; + } else { + root.state = "dragging"; + } + oldY = mouse.y; + startY = mouse.y; + } + onPositionChanged: { + if (root.state == "app" || root.state == "zooming") { + mainFlickable.scale = (1 - (startY - mouse.y) / root.height); + } else { + mainFlickable.contentY += oldY - mouse.y; + } + oldY = mouse.y; + } + onReleased: { + if (root.state == "app" || root.state == "zooming") { + if (mainFlickable.scale < 0.7) { + root.state = "switcher" + } else { + root.state = "app" + } + } else { + if (mainFlickable.contentY < root.height) { + root.state = "homescreen" + } else { + root.state = "switcher" + } + } + } + + Row { + PlasmaComponents.ToolButton { + height: bottomBar.height + width: height + iconSource: "applications-other" + onClicked: root.state = "switcher" + } + PlasmaComponents.ToolButton { + height: bottomBar.height + width: height + iconSource: "go-home" + onClicked: root.state = "homescreen" + } + } + } + } + states: [ + State { + name: "switcher" + PropertyChanges { + target: mainFlickable + scale: 0.5 + x: -root.width / 2 + y: -root.height / 2 + interactive: true + contentY: root.height*2 + (root.currentApp ? root.currentApp.y : 0) + visible: true + } + }, + State { + name: "dragging" + PropertyChanges { + target: mainFlickable + scale: 0.5 + x: -root.width / 2 + y: -root.height / 2 + interactive: true + contentY: contentY + visible: true + } + }, + State { + name: "zooming" + PropertyChanges { + target: mainFlickable + scale: scale + x: (-root.currentApp.x * mainFlickable.zoomFactor ) + (1 - mainFlickable.zoomFactor) * (-root.width / 2) + y: (-root.height / 2) * (1 - mainFlickable.zoomFactor) + interactive: true + contentY: (root.height*2 + (root.currentApp ? root.currentApp.y : 0)) * (1 - mainFlickable.zoomFactor) + (root.height*2 + root.currentApp.y) * mainFlickable.zoomFactor + visible: true + } + }, + State { + name: "app" + PropertyChanges { + target: mainFlickable + scale: 1 + x: -root.currentApp.x + y: 0 + interactive: false + contentY: root.height*2 + root.currentApp.y + visible: true + } + }, + State { + name: "homescreen" + PropertyChanges { + target: mainFlickable + scale: 0.5 + x: -root.width / 2 + y: -root.height / 2 + interactive: true + contentY: 0 + visible: true + } + } + ] + transitions: [ + Transition { + to: "dragging" + ScriptAction { + script: { + root.currentApp = null; + } + } + }, + Transition { + to: "zooming" + }, + Transition { + SequentialAnimation { + ScriptAction { + script: { + if (root.state != "homescreen") { + mainFlickable.visible = true; + } + } + } + PropertyAnimation { + target: mainFlickable + duration: units.longDuration + easing.type: Easing.InOutQuad + properties: "x,y,scale,contentY" + } + ScriptAction { + script: { + if (root.state == "homescreen") { + mainFlickable.visible = false; + root.currentApp = null; + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/qmlcomponents/CMakeLists.txt b/qmlcomponents/CMakeLists.txt index b25c4777..0532a9b6 100644 --- a/qmlcomponents/CMakeLists.txt +++ b/qmlcomponents/CMakeLists.txt @@ -20,4 +20,4 @@ install(TARGETS satellitecomponentsplugin DESTINATION ${QML_INSTALL_DIR}/org/kde install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kde/satellite/components) - +add_subdirectory(modeltest) diff --git a/qmlcomponents/applicationlistmodel.cpp b/qmlcomponents/applicationlistmodel.cpp index 9cd69bd7..09c104e9 100644 --- a/qmlcomponents/applicationlistmodel.cpp +++ b/qmlcomponents/applicationlistmodel.cpp @@ -30,13 +30,16 @@ #include #include #include +#include #include #include ApplicationListModel::ApplicationListModel(QObject *parent) : QAbstractListModel(parent) { - loadApplications(); + //can't use the new syntax as this signal is overloaded + connect(KSycoca::self(), SIGNAL(databaseChanged(const QStringList &)), + this, SLOT(sycocaDbChanged(const QStringList &))); } ApplicationListModel::~ApplicationListModel() @@ -55,15 +58,35 @@ QHash ApplicationListModel::roleNames() const return roleNames; } +void ApplicationListModel::sycocaDbChanged(const QStringList &changes) +{ + if (!changes.contains("apps") && !changes.contains("xdgdata-apps")) { + return; + } + + m_applicationList.clear(); + + loadApplications(); +} + +bool appNameLessThan(const ApplicationData &a1, const ApplicationData &a2) +{ + return a1.name.toLower() < a2.name.toLower(); +} void ApplicationListModel::loadApplications() { beginResetModel(); + m_applicationList.clear(); + KServiceGroup::Ptr group = KServiceGroup::root(); if (!group || !group->isValid()) return; KServiceGroup::List subGroupList = group->entries(true); + QMap orderedList; + QList unorderedList; + // Iterate over all entries in the group for(KServiceGroup::List::ConstIterator it = subGroupList.begin();it != subGroupList.end(); it++) { KSycocaEntry::Ptr groupEntry = (*it); @@ -85,7 +108,12 @@ void ApplicationListModel::loadApplications() data.icon = service->icon(); data.storageId = service->storageId(); data.entryPath = service->exec(); - m_applicationList << data; + + if (m_appPositions.contains(service->storageId())) { + orderedList[m_appPositions.value(service->storageId())] = data; + } else { + unorderedList << data; + } } } } @@ -93,6 +121,10 @@ void ApplicationListModel::loadApplications() } } + std::sort(unorderedList.begin(), unorderedList.end(), appNameLessThan); + m_applicationList << orderedList.values(); + m_applicationList << unorderedList; + endResetModel(); emit countChanged(); } @@ -121,6 +153,13 @@ QVariant ApplicationListModel::data(const QModelIndex &index, int role) const } } +Qt::ItemFlags ApplicationListModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return 0; + return Qt::ItemIsDragEnabled|QAbstractItemModel::flags(index); +} + int ApplicationListModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { @@ -130,6 +169,11 @@ int ApplicationListModel::rowCount(const QModelIndex &parent) const return m_applicationList.count(); } +void ApplicationListModel::moveRow(const QModelIndex &sourceParent, int sourceRow, const QModelIndex &destinationParent, int destinationChild) +{ + moveItem(sourceRow, destinationChild); +} + Q_INVOKABLE void ApplicationListModel::moveItem(int row, int destination) { if (row < 0 || destination < 0 || row >= m_applicationList.length() || @@ -141,8 +185,27 @@ Q_INVOKABLE void ApplicationListModel::moveItem(int row, int destination) } beginMoveRows(QModelIndex(), row, row, QModelIndex(), destination); - ApplicationData data = m_applicationList.takeAt(row); - m_applicationList.insert(destination, data); + if (destination > row) { + ApplicationData data = m_applicationList.at(row); + m_applicationList.insert(destination, data); + m_applicationList.takeAt(row); + } else { + ApplicationData data = m_applicationList.takeAt(row); + m_applicationList.insert(destination, data); + } + + + m_appOrder.clear(); + m_appPositions.clear(); + int i = 0; + for (auto app : m_applicationList) { + m_appOrder << app.storageId; + m_appPositions[app.storageId] = i; + ++i; + } + + + emit appOrderChanged(); endMoveRows(); } @@ -154,7 +217,29 @@ void ApplicationListModel::runApplication(const QString &storageId) KService::Ptr service = KService::serviceByStorageId(storageId); - QProcess::startDetached(service->exec()); + //ignore parameters like %u + QProcess::startDetached(service->exec().replace(QRegExp("%\\w"), "")); +} + +QStringList ApplicationListModel::appOrder() const +{ + return m_appOrder; +} + +void ApplicationListModel::setAppOrder(const QStringList &order) +{ + if (m_appOrder == order) { + return; + } + + m_appOrder = order; + m_appPositions.clear(); + int i = 0; + for (auto app : m_appOrder) { + m_appPositions[app] = i; + ++i; + } + emit appOrderChanged(); } #include "applicationlistmodel.moc" diff --git a/qmlcomponents/applicationlistmodel.h b/qmlcomponents/applicationlistmodel.h index 8e4f82ee..a5094566 100644 --- a/qmlcomponents/applicationlistmodel.h +++ b/qmlcomponents/applicationlistmodel.h @@ -38,6 +38,7 @@ class ApplicationListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(QStringList appOrder READ appOrder WRITE setAppOrder NOTIFY appOrderChanged) public: ApplicationListModel(QObject *parent = 0); @@ -45,10 +46,14 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + void moveRow(const QModelIndex &sourceParent, int sourceRow, const QModelIndex &destinationParent, int destinationChild); + int count() { return m_applicationList.count(); } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + Qt::ItemFlags flags(const QModelIndex &index) const; + QHash roleNames() const Q_DECL_OVERRIDE; enum Roles { @@ -59,16 +64,27 @@ public: ApplicationOriginalRowRole = Qt::UserRole + 6 }; + QStringList appOrder() const; + void setAppOrder(const QStringList &order); + Q_INVOKABLE void moveItem(int row, int order); Q_INVOKABLE void runApplication(const QString &storageId); + Q_INVOKABLE void loadApplications(); + +public Q_SLOTS: + void sycocaDbChanged(const QStringList &change); + Q_SIGNALS: void countChanged(); + void appOrderChanged(); private: QList m_applicationList; - void loadApplications(); + + QStringList m_appOrder; + QHash m_appPositions; }; #endif // APPLICATIONLISTMODEL_H diff --git a/qmlcomponents/modeltest/CMakeLists.txt b/qmlcomponents/modeltest/CMakeLists.txt new file mode 100644 index 00000000..246eeef7 --- /dev/null +++ b/qmlcomponents/modeltest/CMakeLists.txt @@ -0,0 +1,15 @@ + +set(applicationlistmodeltest_SRCS + main.cpp + modeltest.cpp + ../applicationlistmodel.cpp + ) + +add_executable(applicationlistmodeltest ${applicationlistmodeltest_SRCS}) +target_link_libraries(applicationlistmodeltest + Qt5::Core + Qt5::Test + Qt5::Widgets + KF5::Service + ) + diff --git a/qmlcomponents/modeltest/main.cpp b/qmlcomponents/modeltest/main.cpp new file mode 100644 index 00000000..cfa3ef52 --- /dev/null +++ b/qmlcomponents/modeltest/main.cpp @@ -0,0 +1,54 @@ +#include +#include +#include +#include +#include +#include +#include + + +#include "../applicationlistmodel.h" +#include "modeltest.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QWidget *widget = new QWidget; + QVBoxLayout *layout = new QVBoxLayout(widget); + + ApplicationListModel *applicationListModel = new ApplicationListModel(widget); + ModelTest *test = new ModelTest(applicationListModel, widget); + + QTreeView *view = new QTreeView(widget); + QPushButton *upButton = new QPushButton(widget); + upButton->setText("Move Up"); + QObject::connect(upButton, &QPushButton::clicked, [=](){ + QModelIndex idx = view->currentIndex(); + if (idx.row() > 0) { + applicationListModel->moveItem(idx.row(), idx.row()-1); + } + }); + QPushButton *downButton = new QPushButton(widget); + downButton->setText("Move Down"); + QObject::connect(downButton, &QPushButton::clicked, [=](){ + QModelIndex idx = view->currentIndex(); + if (idx.row() > 0) { + applicationListModel->moveItem(idx.row(), idx.row()+1); + } + }); + layout->addWidget(upButton); + layout->addWidget(downButton); + + view->setDragDropMode(QAbstractItemView::InternalMove); + view->setModel(applicationListModel); + applicationListModel->loadApplications(); + layout->addWidget(view); + + QAction *quit = new QAction(widget); + quit->setShortcut(Qt::CTRL + Qt::Key_Q); + QObject::connect(quit, SIGNAL(triggered()), &app, SLOT(quit())); + + widget->addAction(quit); + widget->show(); + return app.exec(); +} diff --git a/qmlcomponents/modeltest/modeltest.cpp b/qmlcomponents/modeltest/modeltest.cpp new file mode 100644 index 00000000..ec66e401 --- /dev/null +++ b/qmlcomponents/modeltest/modeltest.cpp @@ -0,0 +1,592 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "modeltest.h" + +#include +#include + +/*! + Connect to all of the models signals. Whenever anything happens recheck everything. +*/ +ModelTest::ModelTest ( QAbstractItemModel *_model, QObject *parent ) : QObject ( parent ), model ( _model ), fetchingMore ( false ) +{ + if (!model) + qFatal("%s: model must not be null", Q_FUNC_INFO); + + connect(model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(layoutChanged()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(modelReset()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), + this, SLOT(runAllTests()) ); + + // Special checks for changes + connect(model, SIGNAL(layoutAboutToBeChanged()), + this, SLOT(layoutAboutToBeChanged()) ); + connect(model, SIGNAL(layoutChanged()), + this, SLOT(layoutChanged()) ); + + connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(rowsInserted(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(rowsRemoved(QModelIndex,int,int)) ); + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(dataChanged(QModelIndex,QModelIndex)) ); + connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(headerDataChanged(Qt::Orientation,int,int)) ); + + runAllTests(); +} + +void ModelTest::runAllTests() +{ + if ( fetchingMore ) + return; + nonDestructiveBasicTest(); + rowCount(); + columnCount(); + hasIndex(); + index(); + parent(); + data(); +} + +/*! + nonDestructiveBasicTest tries to call a number of the basic functions (not all) + to make sure the model doesn't outright segfault, testing the functions that makes sense. +*/ +void ModelTest::nonDestructiveBasicTest() +{ + QVERIFY( model->buddy ( QModelIndex() ) == QModelIndex() ); + model->canFetchMore ( QModelIndex() ); + QVERIFY( model->columnCount ( QModelIndex() ) >= 0 ); + QVERIFY( model->data ( QModelIndex() ) == QVariant() ); + fetchingMore = true; + model->fetchMore ( QModelIndex() ); + fetchingMore = false; + Qt::ItemFlags flags = model->flags ( QModelIndex() ); + QVERIFY( flags == Qt::ItemIsDropEnabled || flags == 0 ); + model->hasChildren ( QModelIndex() ); + model->hasIndex ( 0, 0 ); + model->headerData ( 0, Qt::Horizontal ); + model->index ( 0, 0 ); + model->itemData ( QModelIndex() ); + QVariant cache; + model->match ( QModelIndex(), -1, cache ); + model->mimeTypes(); + QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() ); + QVERIFY( model->rowCount() >= 0 ); + QVariant variant; + model->setData ( QModelIndex(), variant, -1 ); + model->setHeaderData ( -1, Qt::Horizontal, QVariant() ); + model->setHeaderData ( 999999, Qt::Horizontal, QVariant() ); + QMap roles; + model->sibling ( 0, 0, QModelIndex() ); + model->span ( QModelIndex() ); + model->supportedDropActions(); +} + +/*! + Tests model's implementation of QAbstractItemModel::rowCount() and hasChildren() + + Models that are dynamically populated are not as fully tested here. + */ +void ModelTest::rowCount() +{ +// qDebug() << "rc"; + // check top row + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + int rows = model->rowCount ( topIndex ); + QVERIFY( rows >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( topIndex ) ); + + QModelIndex secondLevelIndex = model->index ( 0, 0, topIndex ); + if ( secondLevelIndex.isValid() ) { // not the top level + // check a row count where parent is valid + rows = model->rowCount ( secondLevelIndex ); + QVERIFY( rows >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( secondLevelIndex ) ); + } + + // The models rowCount() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::columnCount() and hasChildren() + */ +void ModelTest::columnCount() +{ + // check top row + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + QVERIFY( model->columnCount ( topIndex ) >= 0 ); + + // check a column count where parent is valid + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + if ( childIndex.isValid() ) + QVERIFY( model->columnCount ( childIndex ) >= 0 ); + + // columnCount() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::hasIndex() + */ +void ModelTest::hasIndex() +{ +// qDebug() << "hi"; + // Make sure that invalid values returns an invalid index + QVERIFY( !model->hasIndex ( -2, -2 ) ); + QVERIFY( !model->hasIndex ( -2, 0 ) ); + QVERIFY( !model->hasIndex ( 0, -2 ) ); + + int rows = model->rowCount(); + int columns = model->columnCount(); + + // check out of bounds + QVERIFY( !model->hasIndex ( rows, columns ) ); + QVERIFY( !model->hasIndex ( rows + 1, columns + 1 ) ); + + if ( rows > 0 ) + QVERIFY( model->hasIndex ( 0, 0 ) ); + + // hasIndex() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::index() + */ +void ModelTest::index() +{ +// qDebug() << "i"; + // Make sure that invalid values returns an invalid index + QVERIFY( model->index ( -2, -2 ) == QModelIndex() ); + QVERIFY( model->index ( -2, 0 ) == QModelIndex() ); + QVERIFY( model->index ( 0, -2 ) == QModelIndex() ); + + int rows = model->rowCount(); + int columns = model->columnCount(); + + if ( rows == 0 ) + return; + + // Catch off by one errors + QVERIFY( model->index ( rows, columns ) == QModelIndex() ); + QVERIFY( model->index ( 0, 0 ).isValid() ); + + // Make sure that the same index is *always* returned + QModelIndex a = model->index ( 0, 0 ); + QModelIndex b = model->index ( 0, 0 ); + QVERIFY( a == b ); + + // index() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::parent() + */ +void ModelTest::parent() +{ +// qDebug() << "p"; + // Make sure the model won't crash and will return an invalid QModelIndex + // when asked for the parent of an invalid index. + QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() ); + + if ( model->rowCount() == 0 ) + return; + + // Column 0 | Column 1 | + // QModelIndex() | | + // \- topIndex | topIndex1 | + // \- childIndex | childIndex1 | + + // Common error test #1, make sure that a top level index has a parent + // that is a invalid QModelIndex. + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + QVERIFY( model->parent ( topIndex ) == QModelIndex() ); + + // Common error test #2, make sure that a second level index has a parent + // that is the first level index. + if ( model->rowCount ( topIndex ) > 0 ) { + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + QVERIFY( model->parent ( childIndex ) == topIndex ); + } + + // Common error test #3, the second column should NOT have the same children + // as the first column in a row. + // Usually the second column shouldn't have children. + QModelIndex topIndex1 = model->index ( 0, 1, QModelIndex() ); + if ( model->rowCount ( topIndex1 ) > 0 ) { + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + QModelIndex childIndex1 = model->index ( 0, 0, topIndex1 ); + QVERIFY( childIndex != childIndex1 ); + } + + // Full test, walk n levels deep through the model making sure that all + // parent's children correctly specify their parent. + checkChildren ( QModelIndex() ); +} + +/*! + Called from the parent() test. + + A model that returns an index of parent X should also return X when asking + for the parent of the index. + + This recursive function does pretty extensive testing on the whole model in an + effort to catch edge cases. + + This function assumes that rowCount(), columnCount() and index() already work. + If they have a bug it will point it out, but the above tests should have already + found the basic bugs because it is easier to figure out the problem in + those tests then this one. + */ +void ModelTest::checkChildren ( const QModelIndex &parent, int currentDepth ) +{ + // First just try walking back up the tree. + QModelIndex p = parent; + while ( p.isValid() ) + p = p.parent(); + + // For models that are dynamically populated + if ( model->canFetchMore ( parent ) ) { + fetchingMore = true; + model->fetchMore ( parent ); + fetchingMore = false; + } + + int rows = model->rowCount ( parent ); + int columns = model->columnCount ( parent ); + + if ( rows > 0 ) + QVERIFY( model->hasChildren ( parent ) ); + + // Some further testing against rows(), columns(), and hasChildren() + QVERIFY( rows >= 0 ); + QVERIFY( columns >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( parent ) ); + + //qDebug() << "parent:" << model->data(parent).toString() << "rows:" << rows + // << "columns:" << columns << "parent column:" << parent.column(); + + const QModelIndex topLeftChild = model->index( 0, 0, parent ); + + QVERIFY( !model->hasIndex ( rows + 1, 0, parent ) ); + for ( int r = 0; r < rows; ++r ) { + if ( model->canFetchMore ( parent ) ) { + fetchingMore = true; + model->fetchMore ( parent ); + fetchingMore = false; + } + QVERIFY( !model->hasIndex ( r, columns + 1, parent ) ); + for ( int c = 0; c < columns; ++c ) { + QVERIFY( model->hasIndex ( r, c, parent ) ); + QModelIndex index = model->index ( r, c, parent ); + // rowCount() and columnCount() said that it existed... + QVERIFY( index.isValid() ); + + // index() should always return the same index when called twice in a row + QModelIndex modifiedIndex = model->index ( r, c, parent ); + QVERIFY( index == modifiedIndex ); + + // Make sure we get the same index if we request it twice in a row + QModelIndex a = model->index ( r, c, parent ); + QModelIndex b = model->index ( r, c, parent ); + QVERIFY( a == b ); + + { + const QModelIndex sibling = model->sibling( r, c, topLeftChild ); + QVERIFY( index == sibling ); + } + { + const QModelIndex sibling = topLeftChild.sibling( r, c ); + QVERIFY( index == sibling ); + } + + // Some basic checking on the index that is returned + QVERIFY( index.model() == model ); + QCOMPARE( index.row(), r ); + QCOMPARE( index.column(), c ); + // While you can technically return a QVariant usually this is a sign + // of a bug in data(). Disable if this really is ok in your model. +// QVERIFY( model->data ( index, Qt::DisplayRole ).isValid() ); + + // If the next test fails here is some somewhat useful debug you play with. + + if (model->parent(index) != parent) { + qDebug() << r << c << currentDepth << model->data(index).toString() + << model->data(parent).toString(); + qDebug() << index << parent << model->parent(index); +// And a view that you can even use to show the model. +// QTreeView view; +// view.setModel(model); +// view.show(); + } + + // Check that we can get back our real parent. + QCOMPARE( model->parent ( index ), parent ); + + // recursively go down the children + if ( model->hasChildren ( index ) && currentDepth < 10 ) { + //qDebug() << r << c << "has children" << model->rowCount(index); + checkChildren ( index, ++currentDepth ); + }/* else { if (currentDepth >= 10) qDebug() << "checked 10 deep"; };*/ + + // make sure that after testing the children that the index doesn't change. + QModelIndex newerIndex = model->index ( r, c, parent ); + QVERIFY( index == newerIndex ); + } + } +} + +/*! + Tests model's implementation of QAbstractItemModel::data() + */ +void ModelTest::data() +{ + // Invalid index should return an invalid qvariant + QVERIFY( !model->data ( QModelIndex() ).isValid() ); + + if ( model->rowCount() == 0 ) + return; + + // A valid index should have a valid QVariant data + QVERIFY( model->index ( 0, 0 ).isValid() ); + + // shouldn't be able to set data on an invalid index + QVERIFY( !model->setData ( QModelIndex(), QLatin1String ( "foo" ), Qt::DisplayRole ) ); + + // General Purpose roles that should return a QString + QVariant variant = model->data ( model->index ( 0, 0 ), Qt::ToolTipRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + variant = model->data ( model->index ( 0, 0 ), Qt::StatusTipRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + variant = model->data ( model->index ( 0, 0 ), Qt::WhatsThisRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + + // General Purpose roles that should return a QSize + variant = model->data ( model->index ( 0, 0 ), Qt::SizeHintRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + + // General Purpose roles that should return a QFont + QVariant fontVariant = model->data ( model->index ( 0, 0 ), Qt::FontRole ); + if ( fontVariant.isValid() ) { + QVERIFY( fontVariant.canConvert() ); + } + + // Check that the alignment is one we know about + QVariant textAlignmentVariant = model->data ( model->index ( 0, 0 ), Qt::TextAlignmentRole ); + if ( textAlignmentVariant.isValid() ) { + int alignment = textAlignmentVariant.toInt(); + QCOMPARE( alignment, ( alignment & ( Qt::AlignHorizontal_Mask | Qt::AlignVertical_Mask ) ) ); + } + + // General Purpose roles that should return a QColor + QVariant colorVariant = model->data ( model->index ( 0, 0 ), Qt::BackgroundColorRole ); + if ( colorVariant.isValid() ) { + QVERIFY( colorVariant.canConvert() ); + } + + colorVariant = model->data ( model->index ( 0, 0 ), Qt::TextColorRole ); + if ( colorVariant.isValid() ) { + QVERIFY( colorVariant.canConvert() ); + } + + // Check that the "check state" is one we know about. + QVariant checkStateVariant = model->data ( model->index ( 0, 0 ), Qt::CheckStateRole ); + if ( checkStateVariant.isValid() ) { + int state = checkStateVariant.toInt(); + QVERIFY( state == Qt::Unchecked || + state == Qt::PartiallyChecked || + state == Qt::Checked ); + } +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsInserted() + */ +void ModelTest::rowsAboutToBeInserted ( const QModelIndex &parent, int start, int /* end */) +{ +// Q_UNUSED(end); +// qDebug() << "rowsAboutToBeInserted" << "start=" << start << "end=" << end << "parent=" << model->data ( parent ).toString() +// << "current count of parent=" << model->rowCount ( parent ); // << "display of last=" << model->data( model->index(start-1, 0, parent) ); +// qDebug() << model->index(start-1, 0, parent) << model->data( model->index(start-1, 0, parent) ); + Changing c; + c.parent = parent; + c.oldSize = model->rowCount ( parent ); + c.last = model->data ( model->index ( start - 1, 0, parent ) ); + c.next = model->data ( model->index ( start, 0, parent ) ); + insert.push ( c ); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeInserted() + */ +void ModelTest::rowsInserted ( const QModelIndex & parent, int start, int end ) +{ + Changing c = insert.pop(); + QVERIFY( c.parent == parent ); +// qDebug() << "rowsInserted" << "start=" << start << "end=" << end << "oldsize=" << c.oldSize +// << "parent=" << model->data ( parent ).toString() << "current rowcount of parent=" << model->rowCount ( parent ); + +// for (int ii=start; ii <= end; ii++) +// { +// qDebug() << "itemWasInserted:" << ii << model->data ( model->index ( ii, 0, parent )); +// } +// qDebug(); + + QVERIFY( c.oldSize + ( end - start + 1 ) == model->rowCount ( parent ) ); + QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) ); + + if (c.next != model->data(model->index(end + 1, 0, c.parent))) { + qDebug() << start << end; + for (int i=0; i < model->rowCount(); ++i) + qDebug() << model->index(i, 0).data().toString(); + qDebug() << c.next << model->data(model->index(end + 1, 0, c.parent)); + } + + QVERIFY( c.next == model->data ( model->index ( end + 1, 0, c.parent ) ) ); +} + +void ModelTest::layoutAboutToBeChanged() +{ + for ( int i = 0; i < qBound ( 0, model->rowCount(), 100 ); ++i ) + changing.append ( QPersistentModelIndex ( model->index ( i, 0 ) ) ); +} + +void ModelTest::layoutChanged() +{ + for ( int i = 0; i < changing.count(); ++i ) { + QPersistentModelIndex p = changing[i]; + QVERIFY( p == model->index ( p.row(), p.column(), p.parent() ) ); + } + changing.clear(); +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsRemoved() + */ +void ModelTest::rowsAboutToBeRemoved ( const QModelIndex &parent, int start, int end ) +{ +qDebug() << "ratbr" << parent << start << end; + Changing c; + c.parent = parent; + c.oldSize = model->rowCount ( parent ); + c.last = model->data ( model->index ( start - 1, 0, parent ) ); + c.next = model->data ( model->index ( end + 1, 0, parent ) ); + remove.push ( c ); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeRemoved() + */ +void ModelTest::rowsRemoved ( const QModelIndex & parent, int start, int end ) +{ + qDebug() << "rr" << parent << start << end; + Changing c = remove.pop(); + QVERIFY( c.parent == parent ); + QVERIFY( c.oldSize - ( end - start + 1 ) == model->rowCount ( parent ) ); + QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) ); + QVERIFY( c.next == model->data ( model->index ( start, 0, c.parent ) ) ); +} + +void ModelTest::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + QVERIFY(topLeft.isValid()); + QVERIFY(bottomRight.isValid()); + QModelIndex commonParent = bottomRight.parent(); + QVERIFY(topLeft.parent() == commonParent); + QVERIFY(topLeft.row() <= bottomRight.row()); + QVERIFY(topLeft.column() <= bottomRight.column()); + int rowCount = model->rowCount(commonParent); + int columnCount = model->columnCount(commonParent); + QVERIFY(bottomRight.row() < rowCount); + QVERIFY(bottomRight.column() < columnCount); +} + +void ModelTest::headerDataChanged(Qt::Orientation orientation, int start, int end) +{ + QVERIFY(start >= 0); + QVERIFY(end >= 0); + QVERIFY(start <= end); + int itemCount = orientation == Qt::Vertical ? model->rowCount() : model->columnCount(); + QVERIFY(start < itemCount); + QVERIFY(end < itemCount); +} + diff --git a/qmlcomponents/modeltest/modeltest.h b/qmlcomponents/modeltest/modeltest.h new file mode 100644 index 00000000..b3d23cfb --- /dev/null +++ b/qmlcomponents/modeltest/modeltest.h @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#ifndef MODELTEST_H +#define MODELTEST_H + +#include +#include +#include + +class ModelTest : public QObject +{ + Q_OBJECT + +public: + ModelTest( QAbstractItemModel *model, QObject *parent = 0 ); + +private Q_SLOTS: + void nonDestructiveBasicTest(); + void rowCount(); + void columnCount(); + void hasIndex(); + void index(); + void parent(); + void data(); + +protected Q_SLOTS: + void runAllTests(); + void layoutAboutToBeChanged(); + void layoutChanged(); + void rowsAboutToBeInserted( const QModelIndex &parent, int start, int end ); + void rowsInserted( const QModelIndex & parent, int start, int end ); + void rowsAboutToBeRemoved( const QModelIndex &parent, int start, int end ); + void rowsRemoved( const QModelIndex & parent, int start, int end ); + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void headerDataChanged(Qt::Orientation orientation, int start, int end); + +private: + void checkChildren( const QModelIndex &parent, int currentDepth = 0 ); + + QAbstractItemModel *model; + + struct Changing { + QModelIndex parent; + int oldSize; + QVariant last; + QVariant next; + }; + QStack insert; + QStack remove; + + bool fetchingMore; + + QList changing; +}; + +#endif diff --git a/shell/contents/applet/AppletError.qml b/shell/contents/applet/AppletError.qml new file mode 100644 index 00000000..cfa83607 --- /dev/null +++ b/shell/contents/applet/AppletError.qml @@ -0,0 +1,51 @@ +/* + * Copyright 2013 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. + */ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +RowLayout { + id: root + Layout.minimumWidth: units.gridUnit * 20 + Layout.minimumHeight: units.gridUnit * 8 + + property alias reason: messageText.text + + clip: true + + PlasmaCore.IconItem { + id: icon + anchors.verticalCenter: parent.verticalCenter + Layout.minimumWidth: units.iconSizes.huge + Layout.minimumHeight: units.iconSizes.huge + source: "dialog-error" + } + + PlasmaComponents.TextArea { + id: messageText + Layout.fillWidth: true + Layout.fillHeight: true + verticalAlignment: TextEdit.AlignVCenter + backgroundVisible: false + readOnly: true + width: parent.width - icon.width + wrapMode: Text.Wrap + } +} diff --git a/shell/contents/applet/CompactApplet.qml b/shell/contents/applet/CompactApplet.qml new file mode 100644 index 00000000..f7d61a27 --- /dev/null +++ b/shell/contents/applet/CompactApplet.qml @@ -0,0 +1,117 @@ +/* + * Copyright 2013 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. + */ +import QtQuick 2.4 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.0 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.kquickcontrolsaddons 2.0 + +PlasmaCore.ToolTipArea { + id: root + objectName: "org.kde.desktop-CompactApplet" + anchors.fill: parent + + icon: plasmoid.icon + mainText: plasmoid.toolTipMainText + subText: plasmoid.toolTipSubText + location: plasmoid.location + active: !plasmoid.expanded + property Item fullRepresentation + property Item compactRepresentation + property Item expandedFeedback: expandedItem + + onCompactRepresentationChanged: { + if (compactRepresentation) { + compactRepresentation.parent = root; + compactRepresentation.anchors.fill = root; + compactRepresentation.visible = true; + } + root.visible = true; + } + + onFullRepresentationChanged: { + + if (!fullRepresentation) { + return; + } + /* //if the fullRepresentation size was restored to a stored size, or if is dragged from the desktop, restore popup size + if (fullRepresentation.width > 0) { + appletParent.width = fullRepresentation.width; + } else if (fullRepresentation.Layout && fullRepresentation.Layout.preferredWidth > 0) { + appletParent.width = fullRepresentation.Layout.preferredWidth + } else if (fullRepresentation.implicitWidth > 0) { + appletParent.width = fullRepresentation.implicitWidth + } else { + appletParent.width = theme.mSize(theme.defaultFont).width * 35 + } + + if (fullRepresentation.height > 0) { + appletParent.height = fullRepresentation.height; + } else if (fullRepresentation.Layout && fullRepresentation.Layout.preferredHeight > 0) { + appletParent.height = fullRepresentation.Layout.preferredHeight + } else if (fullRepresentation.implicitHeight > 0) { + appletParent.height = fullRepresentation.implicitHeight + } else { + appletParent.height = theme.mSize(theme.defaultFont).height * 25 + }*/ + + fullRepresentation.parent = appletParent; + fullRepresentation.anchors.fill = fullRepresentation.parent; + } + + Rectangle { + id: expandedItem + anchors.fill: parent + color: PlasmaCore.ColorScope.highlightColor + opacity: plasmoid.expanded ? 0.3 : 0 + Behavior on opacity { + OpacityAnimator { + duration: units.shortDuration + easing.type: Easing.InOutQuad + } + } + } + + Timer { + id: expandedSync + interval: 100 + onTriggered: plasmoid.expanded = popupWindow.visible; + } + + Item { + id: appletParent + opacity: plasmoid.expanded ? 1 : 0 + anchors.top: parent.bottom + Layout.minimumWidth: (fullRepresentation && fullRepresentation.Layout) ? fullRepresentation.Layout.minimumWidth : 0 + Layout.minimumHeight: (fullRepresentation && fullRepresentation.Layout) ? fullRepresentation.Layout.minimumHeight: 0 + Layout.maximumWidth: (fullRepresentation && fullRepresentation.Layout) ? fullRepresentation.Layout.maximumWidth : Infinity + Layout.maximumHeight: (fullRepresentation && fullRepresentation.Layout) ? fullRepresentation.Layout.maximumHeight: Infinity + width: Math.max(parent.width, Layout.minimumWidth) + height: Layout.minimumHeight + + Behavior on opacity { + OpacityAnimator { + duration: units.shortDuration + easing.type: Easing.InOutQuad + } + } + } + +} diff --git a/shell/contents/applet/DefaultCompactRepresentation.qml b/shell/contents/applet/DefaultCompactRepresentation.qml new file mode 100644 index 00000000..80b864fa --- /dev/null +++ b/shell/contents/applet/DefaultCompactRepresentation.qml @@ -0,0 +1,83 @@ +/* + * Copyright 2013 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. + */ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +Row { + id: main + spacing: units.largeSpacing + + Layout.minimumWidth: { + switch (plasmoid.formFactor) { + case PlasmaCore.Types.Vertical: + return 0; + case PlasmaCore.Types.Horizontal: + return height; + default: + return units.gridUnit * 3; + } + } + + Layout.minimumHeight: { + switch (plasmoid.formFactor) { + case PlasmaCore.Types.Vertical: + return width; + case PlasmaCore.Types.Horizontal: + return 0; + default: + return units.gridUnit * 3; + } + } + + PlasmaCore.IconItem { + id: icon + source: plasmoid.icon ? plasmoid.icon : "plasma" + active: mouseArea.containsMouse + colorGroup: PlasmaCore.Theme.ComplementaryColorGroup + anchors.verticalCenter: parent.verticalCenter + } + PlasmaCore.SvgItem { + svg: PlasmaCore.Svg { + id: arrowSvg + imagePath: "widgets/arrows" + colorGroup: PlasmaCore.Theme.ComplementaryColorGroup + } + width: units.iconSizes.smallMedium + height: width + elementId: plasmoid.expanded ? "up-arrow" : "down-arrow" + anchors.verticalCenter: parent.verticalCenter + } + PlasmaComponents.Label { + text: plasmoid.title + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + id: mouseArea + + property bool wasExpanded: false + + anchors.fill: parent + hoverEnabled: true + onPressed: wasExpanded = plasmoid.expanded + onClicked: plasmoid.expanded = !wasExpanded + } +} \ No newline at end of file diff --git a/shell/contents/defaults b/shell/contents/defaults index c5458f35..8dc05ec7 100644 --- a/shell/contents/defaults +++ b/shell/contents/defaults @@ -2,7 +2,7 @@ LookAndFeelPackage=org.kde.satellite.phone [Desktop] -Containment=org.kde.desktopcontainment +Containment=org.kde.phone.homescreen ToolBox= [Desktop][ContainmentActions] diff --git a/shell/contents/layout.js b/shell/contents/layout.js index 5f3eace0..cb89007f 100644 --- a/shell/contents/layout.js +++ b/shell/contents/layout.js @@ -14,3 +14,7 @@ for (var j = 0; j < desktopsArray.length; j++) { desktopsArray[j].writeConfig("Image", "org.kde.satellite.lockers"); } +desktopsForActivity(id)[0].addWidget("org.kde.phone.notifications"); + +var panel = new Panel("org.kde.phone.panel"); +panel.addWidget("org.kde.phone.quicksettings"); diff --git a/shell/contents/views/Desktop.qml b/shell/contents/views/Desktop.qml index df00f82e..ec2eb95e 100644 --- a/shell/contents/views/Desktop.qml +++ b/shell/contents/views/Desktop.qml @@ -41,6 +41,7 @@ Item { property int notificationId: 0; property int buttonHeight: width/4 +<<<<<<< HEAD /* Notification data object has the following properties: appIcon @@ -63,29 +64,19 @@ Item { tmp.summary == data.summary && tmp.body == data.body); var sameSource = tmp.source == source; +======= + onContainmentChanged: { + containment.parent = homescreen; +>>>>>>> mart/newContainmentShell - if (sameSource && matches) { - return; - } - - if (sameSource || matches) { - notificationsModel.remove(i) - break; - } + if (containment != null) { + containment.visible = true; } - - data["id"] = ++notificationId; - data["source"] = source; - if (data["summary"].length < 1) { - data["summary"] = data["body"]; - data["body"] = ''; - } - data["actions"] = actions; - - notificationsModel.insert(0, data); - if (!data["isPersistent"]) { - pendingRemovals.push(notificationId); - pendingTimer.start(); + if (containment != null) { + containment.anchors.left = homescreen.left; + containment.anchors.top = homescreen.top; + containment.anchors.right = homescreen.right; + containment.anchors.bottom = homescreen.bottom; } } @@ -131,39 +122,6 @@ Item { modemPath: ofonoManager.modems.length > 0 ? ofonoManager.modems[0] : "" } - OfonoNetworkRegistration { - id: netreg - Component.onCompleted: { - netreg.scan() - updateStrengthIcon() - } - - onNetworkOperatorsChanged : { - console.log("operators :"+netreg.currentOperator["Name"].toString()) - } - modemPath: ofonoManager.modems.length ? ofonoManager.modems[0] : "" - function updateStrengthIcon() { - if (netreg.strength >= 100) { - strengthIcon.source = "network-mobile-100"; - } else if (netreg.strength >= 80) { - strengthIcon.source = "network-mobile-80"; - } else if (netreg.strength >= 60) { - strengthIcon.source = "network-mobile-60"; - } else if (netreg.strength >= 40) { - strengthIcon.source = "network-mobile-40"; - } else if (netreg.strength >= 20) { - strengthIcon.source = "network-mobile-20"; - } else { - strengthIcon.source = "network-mobile-0"; - } - } - - onStrengthChanged: { - console.log("Strength changed to " + netreg.strength) - updateStrengthIcon() - } - } - OfonoNetworkOperator { id: netop } @@ -190,88 +148,11 @@ Item { } } - Timer { - id: pendingTimer - interval: 5000 - repeat: false - onTriggered: { - for (var i = 0; i < pendingRemovals.length; ++i) { - var id = pendingRemovals[i]; - for (var j = 0; j < notificationsModel.count; ++j) { - if (notificationsModel.get(j).id == id) { - notificationsModel.remove(j); - } - } - } - pendingRemovals = []; - } - } - - Rectangle { - z: 1 - color: Qt.rgba(0, 0, 0, 0.9 * (Math.min(applications.contentY + homescreen.height, homescreen.height) / homescreen.height)) - anchors.fill: parent - } - - PlasmaCore.DataSource { - id: timeSource - engine: "time" - connectedSources: ["Local"] - interval: 60 * 1000 - } - PlasmaCore.DataSource { - id: notificationsSource - - engine: "notifications" - interval: 0 - - onSourceAdded: { - connectSource(source); - } - - onSourceRemoved: { - for (var i = 0; i < notificationsModel.count; ++i) { - if (notificationsModel.get(i) == source) { - notificationsModel.remove(i); - break; - } - } - } - - onNewData: { - var actions = new Array() - if (data["actions"] && data["actions"].length % 2 == 0) { - for (var i = 0; i < data["actions"].length; i += 2) { - var action = new Object(); - action["id"] = data["actions"][i]; - action["text"] = data["actions"][i+1]; - actions.push(action); - } - } - - homescreen.addNotification( - sourceName, - data, - actions); - } - - } - - ListModel { - id: notificationsModel - - ListElement { - appIcon: "call-start" - summary: "Missed call from Joe" - body: "Called at 8:42 from +41 56 373 37 31" - } - ListElement { - appIcon: "im-google" - summary: "July: Hey! Are you around?" - } - ListElement { - appIcon: "im-google" - summary: "July: Hello?" + //pass the focus to the containment, so it can react to homescreen activate/inactivate + Connections { + target: desktop + onActiveChanged: { + containment.focus = desktop.active; } } @@ -304,255 +185,6 @@ Item { source: simManager.pinRequired != OfonoSimManager.NoPin ? Qt.resolvedUrl("Pin.qml") : "" } - PlasmaCore.ColorScope { - id: statusPanel - anchors { - top: parent.top - left: parent.left - right: parent.right - } - height: units.iconSizes.small - z: 2 - colorGroup: PlasmaCore.Theme.ComplementaryColorGroup - - Rectangle { - anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.9) - - PlasmaCore.IconItem { - id: strengthIcon - colorGroup: PlasmaCore.ColorScope.colorGroup - anchors { - left: parent.left - verticalCenter: parent.verticalCenter - } - width: units.iconSizes.small - height: width - } - PlasmaComponents.Label { - anchors { - left: strengthIcon.right - verticalCenter: parent.verticalCenter - } - text: netreg.strength + "% " + netreg.name - color: PlasmaCore.ColorScope.textColor - font.pixelSize: parent.height / 2 - } - PlasmaComponents.Label { - id: clock - anchors.fill: parent - text: Qt.formatTime(timeSource.data.Local.DateTime, "hh:mm") - color: PlasmaCore.ColorScope.textColor - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - font.pixelSize: height / 2 - } - MouseArea { - property int oldMouseY: 0 - - anchors.fill: parent - enabled: !dialerOverlay.item.visible - onPressed: { - oldMouseY = mouse.y; - slidingPanel.visible = true; - } - onPositionChanged: { - slidingPanel.offset = slidingPanel.offset + (mouse.y - oldMouseY); - oldMouseY = mouse.y; - } - onReleased: slidingPanel.updateState(); - } - - - PlasmaWorkspace.BatteryIcon { - id: batteryIcon - anchors { - right: parent.right - verticalCenter: parent.verticalCenter - } - width: units.iconSizes.small - height: width - hasBattery: pmSource.data["Battery"]["Has Battery"] - batteryType: "Phone" - percent: pmSource.data["Battery0"] ? pmSource.data["Battery0"]["Percent"] : 0 - - PlasmaCore.DataSource { - id: pmSource - engine: "powermanagement" - connectedSources: sources - onSourceAdded: { - disconnectSource(source); - connectSource(source); - } - onSourceRemoved: { - disconnectSource(source); - } - } - } - } - } - - SlidingPanel { - id: slidingPanel - width: homescreen.width - height: homescreen.height - } - - - PlasmaCore.ColorScope { - z: 1 - anchors { - fill: parent - } - - colorGroup: PlasmaCore.Theme.ComplementaryColorGroup - - SatelliteComponents.ApplicationListModel { - id: appListModel - } - - GridView { - id: applications - anchors { - top: parent.top - bottom: parent.bottom - left: parent.left - right: parent.right - } - z: 1 - cellWidth: homescreen.buttonHeight - cellHeight: cellWidth - model: appListModel - snapMode: GridView.SnapToRow - clip: true - header: MouseArea { - z: 999 - width: homescreen.width - height: homescreen.height - units.iconSizes.medium - - onPressAndHold: { - containment.action("configure").trigger(); - } - - PlasmaComponents.Label { - id: bigClock - anchors { - horizontalCenter: parent.horizontalCenter - top: parent.top - bottom: notificationView.top - } - text: Qt.formatTime(timeSource.data.Local.DateTime, "hh:mm") - color: PlasmaCore.ColorScope.textColor - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - font.pointSize: 40 - style: Text.Raised - styleColor: "black" - } - - ListView { - id: notificationView - spacing: units.smallSpacing - anchors { - bottom: parent.bottom - left: parent.left - right: parent.right - bottomMargin: stripe.height * 2 - } - height: parent.height / 3 - interactive: false - - z: 1 - verticalLayoutDirection: ListView.BottomToTop - model: notificationsModel - - add: Transition { - NumberAnimation { - properties: "x" - from: notificationView.width - duration: 100 - } - } - - remove: Transition { - NumberAnimation { - properties: "x" - to: notificationView.width - duration: 500 - } - NumberAnimation { - properties: "opacity" - to: 0 - duration: 500 - } - } - - removeDisplaced: Transition { - SequentialAnimation { - PauseAnimation { duration: 600 } - NumberAnimation { properties: "x,y"; duration: 100 } - } - } - - delegate: NotificationStripe {} - - } - SatelliteStripe { - id: stripe - z: 99 - y: Math.max(applications.contentY + parent.height, parent.height - height) - - PlasmaCore.Svg { - id: stripeIcons - imagePath: Qt.resolvedUrl("../images/homescreenicons.svg") - } - - Row { - anchors.fill: parent - property int columns: 4 - property alias buttonHeight: stripe.height - - HomeLauncherSvg { - id: phoneIcon - svg: stripeIcons - elementId: "phone" - callback: function() { - dialerOverlay.open() - } - } - - HomeLauncherSvg { - id: messagingIcon - svg: stripeIcons - elementId: "messaging" - callback: function() { console.log("Start messaging") } - } - - - HomeLauncherSvg { - id: emailIcon - svg: stripeIcons - elementId: "email" - callback: function() { console.log("Start email") } - } - - - HomeLauncherSvg { - id: webIcon - svg: stripeIcons - elementId: "web" - callback: function() { console.log("Start web") } - } - } - } - } - delegate: HomeLauncher {} - Component.onCompleted : { console.log("WTF " + width) } - - - } - } - Component.onCompleted: { //configure the view behavior if (desktop) { diff --git a/shell/contents/views/Panel.qml b/shell/contents/views/Panel.qml new file mode 100644 index 00000000..01cdbd2f --- /dev/null +++ b/shell/contents/views/Panel.qml @@ -0,0 +1,41 @@ +/* + * Copyright 2012 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. + */ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore + + +PlasmaCore.FrameSvgItem { + id: root + + visible: false //adjust borders is run during setup. We want to avoid painting till completed + + property Item containment + + onContainmentChanged: { + containment.parent = root; + containment.visible = true; + containment.anchors.fill = root; + } + + Component.onCompleted: { + visible = true + } +}