diff --git a/framework/src/domain/eventcontroller.cpp b/framework/src/domain/eventcontroller.cpp index e9b01250..f41efdf8 100644 --- a/framework/src/domain/eventcontroller.cpp +++ b/framework/src/domain/eventcontroller.cpp @@ -1,142 +1,149 @@ /* * Copyright (C) 2017 Michael Bohlender, * Copyright (C) 2018 Christian Mollekopf, * * 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 "eventcontroller.h" #include #include #include #include #include #include using namespace Sink::ApplicationDomain; EventController::EventController() : Kube::Controller(), action_save{new Kube::ControllerAction{this, &EventController::save}} { updateSaveAction(); } void EventController::save() { using namespace Sink; using namespace Sink::ApplicationDomain; const auto calendar = getCalendar(); if (!calendar) { qWarning() << "No calendar selected"; return; } - if (auto e = mEvent.value()) { - Event event = *e; + if (auto e = getEvent().value()) { + Sink::ApplicationDomain::Event event = *e; //Apply the changed properties on top of what's existing auto calcoreEvent = KCalCore::ICalFormat().readIncidence(event.getIcal()).dynamicCast(); if(!calcoreEvent) { SinkWarning() << "Invalid ICal to process, ignoring..."; return; } calcoreEvent->setSummary(getSummary()); calcoreEvent->setDescription(getDescription()); calcoreEvent->setDtStart(getStart()); calcoreEvent->setDtEnd(getEnd()); calcoreEvent->setAllDay(getAllDay()); event.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); event.setCalendar(*calendar); auto job = Store::modify(event) .then([&] (const KAsync::Error &error) { if (error) { SinkWarning() << "Failed to save the event: " << error; } emit done(); }); run(job); } else { - Event event(calendar->resourceInstanceIdentifier()); + Sink::ApplicationDomain::Event event(calendar->resourceInstanceIdentifier()); auto calcoreEvent = QSharedPointer::create(); calcoreEvent->setUid(QUuid::createUuid().toString()); calcoreEvent->setSummary(getSummary()); calcoreEvent->setDescription(getDescription()); calcoreEvent->setLocation(getLocation()); calcoreEvent->setDtStart(getStart()); calcoreEvent->setDtEnd(getEnd()); calcoreEvent->setAllDay(getAllDay()); event.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); event.setCalendar(*calendar); auto job = Store::create(event) .then([&] (const KAsync::Error &error) { if (error) { SinkWarning() << "Failed to save the event: " << error; } emit done(); }); run(job); } } void EventController::updateSaveAction() { saveAction()->setEnabled(!getSummary().isEmpty()); } -void EventController::loadEvent(const QVariant &variant) +void EventController::init() { using namespace Sink; - mEvent = variant; - if (auto event = variant.value()) { + if (auto event = getEvent().value()) { setCalendar(ApplicationDomainType::Ptr::create(ApplicationDomainType::createEntity(event->resourceInstanceIdentifier(), event->getCalendar()))); auto icalEvent = KCalCore::ICalFormat().readIncidence(event->getIcal()).dynamicCast(); if(!icalEvent) { SinkWarning() << "Invalid ICal to process, ignoring..."; return; } setSummary(icalEvent->summary()); setDescription(icalEvent->description()); setLocation(icalEvent->location()); - setStart(icalEvent->dtStart()); - setEnd(icalEvent->dtEnd()); + + setRecurring(icalEvent->recurs()); + //TODO translate recurrence to string (e.g. weekly) + setRecurrenceString(""); + auto occurrenceStart = getOccurrenceStart(); + if (occurrenceStart.isValid()) { + setStart(occurrenceStart); + if (icalEvent->dtEnd().isValid()) { + setEnd(icalEvent->endDateForStart(occurrenceStart)); + } + } else { + setStart(icalEvent->dtStart()); + setEnd(icalEvent->dtEnd()); + } + setAllDay(icalEvent->allDay()); } } void EventController::remove() { - if (auto c = mEvent.value()) { + if (auto c = getEvent().value()) { run(Sink::Store::remove(*c)); } } - -QVariant EventController::getEvent() const -{ - return mEvent; -} diff --git a/framework/src/domain/eventcontroller.h b/framework/src/domain/eventcontroller.h index 91d2b7f7..29b8418b 100644 --- a/framework/src/domain/eventcontroller.h +++ b/framework/src/domain/eventcontroller.h @@ -1,63 +1,61 @@ /* * Copyright (C) 2017 Michael Bohlender, * Copyright (C) 2018 Christian Mollekopf, * * 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. */ #pragma once #include "kube_export.h" #include #include #include #include "controller.h" class KUBE_EXPORT EventController : public Kube::Controller { Q_OBJECT // Input properties - Q_PROPERTY(QVariant event READ getEvent WRITE loadEvent) + KUBE_CONTROLLER_PROPERTY(QVariant, Event, event) + KUBE_CONTROLLER_PROPERTY(QDateTime, OccurrenceStart, occurrenceStart) //Interface properties KUBE_CONTROLLER_PROPERTY(QByteArray, AccountId, accountId) KUBE_CONTROLLER_PROPERTY(QString, Summary, summary) KUBE_CONTROLLER_PROPERTY(QString, Description, description) KUBE_CONTROLLER_PROPERTY(QString, Location, location) KUBE_CONTROLLER_PROPERTY(QDateTime, Start, start) KUBE_CONTROLLER_PROPERTY(QDateTime, End, end) + KUBE_CONTROLLER_PROPERTY(QString, RecurrenceString, recurrenceString) KUBE_CONTROLLER_PROPERTY(bool, AllDay, allDay) + KUBE_CONTROLLER_PROPERTY(bool, Recurring, recurring) KUBE_CONTROLLER_PROPERTY(Sink::ApplicationDomain::ApplicationDomainType::Ptr, Calendar, calendar) KUBE_CONTROLLER_ACTION(save) public: explicit EventController(); - Q_INVOKABLE void loadEvent(const QVariant &event); + void init() override; Q_INVOKABLE void remove(); - QVariant getEvent() const; - private slots: void updateSaveAction(); - -private: - QVariant mEvent; }; diff --git a/framework/src/domain/perioddayeventmodel.cpp b/framework/src/domain/perioddayeventmodel.cpp index c9c08cf0..220ecab6 100644 --- a/framework/src/domain/perioddayeventmodel.cpp +++ b/framework/src/domain/perioddayeventmodel.cpp @@ -1,194 +1,193 @@ /* Copyright (c) 2018 Michael Bohlender Copyright (c) 2018 Christian Mollekopf Copyright (c) 2018 RĂ©mi Nicole This library 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 of the License, or (at your option) any later version. This library 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 library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "perioddayeventmodel.h" #include -#include -#include enum Roles { Events = EventModel::LastRole, Date }; PeriodDayEventModel::PeriodDayEventModel(QObject *parent) : QAbstractItemModel(parent) { } void PeriodDayEventModel::setModel(EventModel *model) { beginResetModel(); mSourceModel = model; auto resetModel = [this] { beginResetModel(); endResetModel(); }; QObject::connect(model, &QAbstractItemModel::dataChanged, this, resetModel); QObject::connect(model, &QAbstractItemModel::layoutChanged, this, resetModel); QObject::connect(model, &QAbstractItemModel::modelReset, this, resetModel); QObject::connect(model, &QAbstractItemModel::rowsInserted, this, resetModel); QObject::connect(model, &QAbstractItemModel::rowsMoved, this, resetModel); QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, resetModel); endResetModel(); } QModelIndex PeriodDayEventModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) { return {}; } if (!parent.isValid()) { // Asking for a day return createIndex(row, column, DAY_ID); } return {}; } QModelIndex PeriodDayEventModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return {}; } if (index.internalId() == DAY_ID) { return {}; } auto day = index.internalId(); return this->index(day, 0); } int PeriodDayEventModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid() && mSourceModel) { return mSourceModel->length(); } return 0; } int PeriodDayEventModel::columnCount(const QModelIndex &) const { return 1; } QDateTime PeriodDayEventModel::getStartTimeOfDay(const QDateTime &dateTime, const QDate &today) const { if (dateTime.date() != today) { return QDateTime{today, QTime{0,0}}; } return dateTime; } QDateTime PeriodDayEventModel::getEndTimeOfDay(const QDateTime &dateTime, const QDate &today) const { if (dateTime.date() != today) { return QDateTime{today, QTime{23, 59, 59}}; } return dateTime; } QVariant PeriodDayEventModel::data(const QModelIndex &idx, int role) const { if (!mSourceModel) { return {}; } const auto dayOffset = idx.row(); const QDate startDate = mSourceModel->start(); const auto today = startDate.addDays(dayOffset); switch (role) { case Date: return today; case Events: { auto result = QVariantList{}; QMultiMap sorted; for (int row = 0; row < mSourceModel->rowCount(); row++) { const auto srcIdx = mSourceModel->index(row, 0, {}); if (srcIdx.data(EventModel::AllDay).toBool()) { continue; } const auto start = srcIdx.data(EventModel::StartTime).toDateTime(); const auto end = srcIdx.data(EventModel::EndTime).toDateTime(); if (end.date() < today || start.date() > today) { continue; } sorted.insert(srcIdx.data(EventModel::StartTime).toDateTime().time(), srcIdx); } QMap indentationStack; for (auto it = sorted.begin(); it != sorted.end(); it++) { // auto eventid = index(it.value(), 0, idx); const auto srcIdx = it.value(); const auto start = getStartTimeOfDay(srcIdx.data(EventModel::StartTime).toDateTime(), today); const auto startTime = start.time(); const auto end = getEndTimeOfDay(srcIdx.data(EventModel::EndTime).toDateTime(), today); auto endTime = end.time(); if (!endTime.isValid()) { //Even without duration we still take some space visually endTime = startTime.addSecs(60 * 20); } const auto duration = qRound(startTime.secsTo(endTime) / 3600.0); SinkTrace() << "Appending event:" << srcIdx.data(EventModel::Summary) << start << end; //Remove all dates before startTime for (auto it = indentationStack.begin(); it != indentationStack.end();) { if (it.key() < startTime) { it = indentationStack.erase(it); } else { ++it; } } const int indentation = indentationStack.size(); indentationStack.insert(endTime, 0); result.append(QVariantMap{ {"text", srcIdx.data(EventModel::Summary)}, {"description", srcIdx.data(EventModel::Description)}, {"starts", startTime.hour() + startTime.minute() / 60.}, {"duration", duration}, {"color", srcIdx.data(EventModel::Color)}, {"indentation", indentation}, + {"occurrenceDate", srcIdx.data(EventModel::StartTime)}, {"event", srcIdx.data(EventModel::Event)} }); } return result; } default: Q_ASSERT(false); return {}; } } QHash PeriodDayEventModel::roleNames() const { return { {Events, "events"}, {Date, "date"} }; } diff --git a/views/calendar/qml/EventView.qml b/views/calendar/qml/EventView.qml index 1a285ab4..acbc794a 100644 --- a/views/calendar/qml/EventView.qml +++ b/views/calendar/qml/EventView.qml @@ -1,122 +1,127 @@ /* * Copyright (C) 2018 Michael Bohlender, * * 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. */ import QtQuick 2.4 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 import org.kube.framework 1.0 as Kube import "dateutils.js" as DateUtils FocusScope { id: root property var controller width: stackView.width height: stackView.height signal done() StackView { id: stackView anchors.centerIn: parent width: stackView.currentItem.implicitWidth height: stackView.currentItem.implicitHeight initialItem: eventDetails clip: true } Component { id: eventDetails Rectangle { implicitWidth: contentLayout.implicitWidth + 2 * Kube.Units.largeSpacing implicitHeight: contentLayout.implicitHeight + 2 * Kube.Units.largeSpacing color: Kube.Colors.viewBackgroundColor ColumnLayout { id: contentLayout anchors { centerIn: parent } spacing: Kube.Units.smallSpacing Kube.Heading { width: parent.width text: controller.summary } Kube.SelectableLabel { visible: controller.allDay text: controller.start.toLocaleString(Qt.locale(), "dd. MMMM") + (DateUtils.sameDay(controller.start, controller.end) ? "" : " - " + controller.end.toLocaleString(Qt.locale(), "dd. MMMM")) } Kube.SelectableLabel { visible: !controller.allDay text: controller.start.toLocaleString(Qt.locale(), "dd. MMMM hh:mm") + " - " + (DateUtils.sameDay(controller.start, controller.end) ? controller.end.toLocaleString(Qt.locale(), "hh:mm") : controller.end.toLocaleString(Qt.locale(), "dd. MMMM hh:mm")) } + Kube.SelectableLabel { + visible: controller.recurring + text: qsTr("repeats %s").arg(controller.recurrenceString) + } + Kube.SelectableLabel { text: "@" + controller.location visible: controller.location } TextEdit { text: controller.description readOnly: true selectByMouse: true color: Kube.Colors.textColor font.family: Kube.Font.fontFamily } Item { width: 1 height: Kube.Units.largeSpacing } RowLayout { Kube.Button { text: qsTr("Remove") onClicked: { root.controller.remove() } } Item { Layout.fillWidth: true } Kube.Button { text: qsTr("Edit") onClicked: { stackView.push(editor, StackView.Immediate) } } } } } } Component { id: editor EventEditor { controller: root.controller editMode: true onDone: root.done() } } } diff --git a/views/calendar/qml/WeekView.qml b/views/calendar/qml/WeekView.qml index 3f3272ac..e555ff85 100644 --- a/views/calendar/qml/WeekView.qml +++ b/views/calendar/qml/WeekView.qml @@ -1,326 +1,327 @@ /* * Copyright (C) 2018 Michael Bohlender, * Copyright (C) 2018 Christian Mollekopf, * * 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. */ import QtQuick 2.4 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.2 import org.kube.framework 1.0 as Kube import "dateutils.js" as DateUtils FocusScope { id: root property int daysToShow: 7 property var dayWidth: (root.width - Kube.Units.gridUnit - Kube.Units.largeSpacing) / root.daysToShow property var hourHeight: Kube.Units.gridUnit * 2 property date currentDate property date startDate: currentDate property var calendarFilter Item { anchors { top: parent.top right: parent.right } width: root.dayWidth * root.daysToShow + Kube.Units.gridUnit * 2 height: root.height Item { id: weekNumber anchors { top: parent.top left: parent.left } width: Kube.Units.gridUnit * 2 height: Kube.Units.gridUnit * 2 Label { anchors.centerIn: parent text: DateUtils.getWeek(startDate, Qt.locale().firstDayOfWeek) font.bold: true } } MultiDayView { id: daylong anchors { top: parent.top right: parent.right left: parent.left leftMargin: Kube.Units.gridUnit * 2 } dayWidth: root.dayWidth daysToShow: root.daysToShow currentDate: root.currentDate startDate: root.startDate calendarFilter: root.calendarFilter filter: {"allDay": true} paintGrid: true showDayIndicator: false dayHeaderDelegate: Item { height: Kube.Units.gridUnit + Kube.Units.smallSpacing * 3 Column { anchors.centerIn: parent Kube.Label { anchors.horizontalCenter: parent.horizontalCenter font.bold: true text: day.toLocaleString(Qt.locale(), "dddd") } Kube.Label { anchors.horizontalCenter: parent.horizontalCenter text: day.toLocaleString(Qt.locale(), "d") color: Kube.Colors.disabledTextColor font.pointSize: Kube.Units.tinyFontSize } } } } Flickable { id: mainWeekViewer anchors { top: daylong.bottom } Layout.fillWidth: true height: root.height - daylong.height - Kube.Units.largeSpacing width: root.dayWidth * root.daysToShow + Kube.Units.gridUnit * 2 contentHeight: root.hourHeight * 24 contentWidth: width clip: true boundsBehavior: Flickable.StopAtBounds ScrollBar.vertical: Kube.ScrollBar {} Kube.ScrollHelper { id: scrollHelper flickable: mainWeekViewer anchors.fill: parent } Row { height: root.hourHeight * 24 width: root.dayWidth * root.daysToShow + Kube.Units.gridUnit * 2 spacing: 0 //BEGIN time labels Column { anchors { bottom: parent.bottom //offset so the label is center aligned to the line bottomMargin: root.hourHeight - Kube.Units.gridUnit / 2 } Repeater { model: ["1:00","2:00","3:00","4:00","5:00","6:00","7:00","8:00","9:00","10:00","11:00","12:00", "13:00","14:00","15:00","16:00","17:00","18:00","19:00","20:00","21:00","22:00","23:00"] delegate: Item { height: root.hourHeight width: Kube.Units.gridUnit * 2 Kube.Label { anchors { right: parent.right rightMargin: Kube.Units.smallSpacing bottom: parent.bottom } text: model.modelData } } } } //END time labels Repeater { model: Kube.PeriodDayEventModel { model: Kube.EventModel { start: root.startDate length: root.daysToShow calendarFilter: root.calendarFilter } } delegate: Rectangle { id: dayDelegate width: root.dayWidth height: root.hourHeight * 24 clip: true color: Kube.Colors.viewBackgroundColor property bool isInPast: DateUtils.roundToDay(root.currentDate) > DateUtils.roundToDay(date) property bool isToday: DateUtils.sameDay(root.currentDate, date) property var todaysDate: date //Dimm days in the past Rectangle { anchors.fill: parent color: Kube.Colors.buttonColor opacity: 0.2 visible: isInPast } //Grid Column { anchors.fill: parent Repeater { model: 12 delegate: Rectangle { height: root.hourHeight * 2 width: parent.width color: "transparent" border.width: 1 border.color: Kube.Colors.lightgrey MouseArea { anchors.fill: parent onClicked: { var d = dayDelegate.todaysDate var hours = index * 2 var minuteOffset = 120 / parent.height * mouse.y var minutes = minuteOffset % 60 hours += (minuteOffset - minutes) / 60 d.setHours(hours) d.setMinutes(minutes) Kube.Fabric.postMessage(Kube.Messages.eventEditor, {"start": d, "allDay": false}) } } } } } Repeater { model: events delegate: Rectangle { id: eventDelegate states: [ State { name: "dnd" when: mouseArea.drag.active PropertyChanges {target: mouseArea; cursorShape: Qt.ClosedHandCursor} PropertyChanges {target: eventDelegate; x: x; y: y} PropertyChanges {target: eventDelegate; parent: root} PropertyChanges {target: eventDelegate; opacity: 0.7} PropertyChanges {target: eventDelegate; anchors.right: ""} PropertyChanges {target: eventDelegate; width: root.dayWidth - Kube.Units.smallSpacing * 2} } ] anchors { right: parent.right rightMargin: Kube.Units.smallSpacing } radius: 2 width: root.dayWidth - Kube.Units.smallSpacing * 2 - Kube.Units.gridUnit * model.modelData.indentation height: Math.max(root.hourHeight * 0.5, root.hourHeight * model.modelData.duration) y: root.hourHeight * model.modelData.starts x: Kube.Units.gridUnit * model.modelData.indentation color: model.modelData.color border.width: 1 border.color: Kube.Colors.viewBackgroundColor Kube.Label { anchors { fill: parent leftMargin: Kube.Units.smallSpacing rightMargin: Kube.Units.smallSpacing } text: model.modelData.text color: Kube.Colors.highlightedTextColor wrapMode: Text.Wrap elide: Text.ElideRight } Drag.active: mouseArea.drag.active Drag.hotSpot.x: mouseArea.mouseX Drag.hotSpot.y: mouseArea.mouseY Drag.source: eventDelegate MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true drag.target: parent onReleased: eventDelegate.Drag.drop() onClicked: eventDetails.createObject(root, {}).open() Component { id: eventDetails Kube.Popup { id: popup parent: ApplicationWindow.overlay x: Math.round((parent.width - width) / 2) y: Math.round((parent.height - height) / 2) width: eventView.width height: eventView.height padding: 0 EventView { id: eventView controller: Kube.EventController { event: model.modelData.event + occurrenceStart: model.modelData.occurrenceDate } onDone: popup.close() } } } } } } Rectangle { id: currentTimeLine anchors { right: parent.right left: parent.left } y: root.hourHeight * root.currentDate.getHours() + root.hourHeight / 60 * root.currentDate.getMinutes() height: 2 color: Kube.Colors.plasmaBlue visible: isToday opacity: 0.8 } DropArea { anchors.fill: parent onDropped: { console.log("DROP") drop.accept(Qt.MoveAction) //drop.source.visible = false console.log((drop.source.y - mainWeekViewer.y + mainWeekViewer.contentY) / hourHeight) } } } } } } } }