diff --git a/framework/src/domain/eventcontroller.cpp b/framework/src/domain/eventcontroller.cpp index 3ea840fa..c2a2c4b4 100644 --- a/framework/src/domain/eventcontroller.cpp +++ b/framework/src/domain/eventcontroller.cpp @@ -1,210 +1,262 @@ /* * 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 #include #include "eventoccurrencemodel.h" +#include "recepientautocompletionmodel.h" +#include "identitiesmodel.h" using namespace Sink::ApplicationDomain; +class OrganizerSelector : public Selector { + Q_OBJECT +public: + OrganizerSelector(EventController &controller) : Selector(new IdentitiesModel), mController(controller) + { + } + + void setCurrent(const QModelIndex &index) Q_DECL_OVERRIDE + { + if (index.isValid()) { + auto currentAccountId = index.data(IdentitiesModel::AccountId).toByteArray(); + + KMime::Types::Mailbox mb; + mb.setName(index.data(IdentitiesModel::Username).toString()); + mb.setAddress(index.data(IdentitiesModel::Address).toString().toUtf8()); + mController.setOrganizer(mb.prettyAddress()); + } else { + SinkWarning() << "No valid identity for index: " << index; + mController.clearOrganizer(); + } + } +private: + EventController &mController; +}; + +class AttendeeCompleter : public Completer { +public: + AttendeeCompleter() : Completer(new RecipientAutocompletionModel) + { + } + + void setSearchString(const QString &s) { + static_cast(model())->setFilter(s); + Completer::setSearchString(s); + } +}; + class AttendeeController : public Kube::ListPropertyController { Q_OBJECT public: AttendeeController() : Kube::ListPropertyController{{"name", "email", "status"}} { } }; EventController::EventController() : Kube::Controller(), controller_attendees{new AttendeeController}, - action_save{new Kube::ControllerAction{this, &EventController::save}} + action_save{new Kube::ControllerAction{this, &EventController::save}}, + mAttendeeCompleter{new AttendeeCompleter}, + mIdentitySelector{new OrganizerSelector{*this}} { updateSaveAction(); } +Completer *EventController::attendeeCompleter() const +{ + return mAttendeeCompleter.data(); +} + +Selector *EventController::identitySelector() const +{ + return mIdentitySelector.data(); +} + void EventController::save() { using namespace Sink; using namespace Sink::ApplicationDomain; const auto calendar = getCalendar(); if (!calendar) { qWarning() << "No calendar selected"; return; } const auto occurrenceVariant = getEventOccurrence(); if (occurrenceVariant.isValid()) { const auto occurrence = occurrenceVariant.value(); Sink::ApplicationDomain::Event event = *occurrence.domainObject; //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; } saveToEvent(*calcoreEvent); 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 { Sink::ApplicationDomain::Event event(calendar->resourceInstanceIdentifier()); auto calcoreEvent = QSharedPointer::create(); calcoreEvent->setUid(QUuid::createUuid().toString()); saveToEvent(*calcoreEvent); 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()); } static EventController::ParticipantStatus toStatus(KCalCore::Attendee::PartStat status) { switch(status) { case KCalCore::Attendee::Accepted: return EventController::Accepted; case KCalCore::Attendee::Declined: return EventController::Declined; case KCalCore::Attendee::NeedsAction: default: break; } return EventController::Unknown; } static KCalCore::Attendee::PartStat fromStatus(EventController::ParticipantStatus status) { switch(status) { case EventController::Accepted: return KCalCore::Attendee::Accepted; case EventController::Declined: return KCalCore::Attendee::Declined; case EventController::Unknown: break; } return KCalCore::Attendee::NeedsAction; } void EventController::populateFromEvent(const KCalCore::Event &event) { setSummary(event.summary()); setDescription(event.description()); setLocation(event.location()); setRecurring(event.recurs()); setAllDay(event.allDay()); setOrganizer(event.organizer()->fullName()); for (const auto &attendee : event.attendees()) { attendeesController()->add({{"name", attendee->name()}, {"email", attendee->email()}, {"status", toStatus(attendee->status())}}); } } void EventController::saveToEvent(KCalCore::Event &event) { event.setSummary(getSummary()); event.setDescription(getDescription()); event.setLocation(getLocation()); event.setDtStart(getStart()); event.setDtEnd(getEnd()); event.setAllDay(getAllDay()); event.setOrganizer(getOrganizer()); event.clearAttendees(); KCalCore::Attendee::List attendees; attendeesController()->traverse([&] (const QVariantMap &map) { bool rsvp = true; KCalCore::Attendee::PartStat status = fromStatus(map["status"].value()); KCalCore::Attendee::Role role = KCalCore::Attendee::ReqParticipant; event.addAttendee(KCalCore::Attendee::Ptr::create(map["name"].toString(), map["email"].toString(), rsvp, status, role, QString{})); }); } void EventController::init() { using namespace Sink; const auto occurrenceVariant = getEventOccurrence(); if (occurrenceVariant.isValid()) { const auto occurrence = occurrenceVariant.value(); Sink::ApplicationDomain::Event event = *occurrence.domainObject; 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; } populateFromEvent(*icalEvent); setStart(occurrence.start); setEnd(occurrence.end); } } void EventController::remove() { const auto occurrenceVariant = getEventOccurrence(); if (occurrenceVariant.isValid()) { const auto occurrence = occurrenceVariant.value(); Sink::ApplicationDomain::Event event = *occurrence.domainObject; run(Sink::Store::remove(event)); } } #include "eventcontroller.moc" diff --git a/framework/src/domain/eventcontroller.h b/framework/src/domain/eventcontroller.h index 4110c74b..917f4def 100644 --- a/framework/src/domain/eventcontroller.h +++ b/framework/src/domain/eventcontroller.h @@ -1,78 +1,90 @@ /* * 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" +#include "completer.h" +#include "selector.h" namespace KCalCore { class Event; }; class KUBE_EXPORT EventController : public Kube::Controller { Q_OBJECT // Input properties KUBE_CONTROLLER_PROPERTY(QVariant, EventOccurrence, eventOccurrence) //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_PROPERTY(QString, Organizer, organizer) KUBE_CONTROLLER_LISTCONTROLLER(attendees) + Q_PROPERTY (Completer* attendeeCompleter READ attendeeCompleter CONSTANT) + Q_PROPERTY (Selector* identitySelector READ identitySelector CONSTANT) + KUBE_CONTROLLER_ACTION(save) public: enum ParticipantStatus { Unknown, Accepted, Declined, }; Q_ENUM(ParticipantStatus); explicit EventController(); void init() override; Q_INVOKABLE void remove(); + Completer *attendeeCompleter() const; + Selector *identitySelector() const; + protected: void populateFromEvent(const KCalCore::Event &event); void saveToEvent(KCalCore::Event &event); private slots: void updateSaveAction(); + +private: + QScopedPointer mAttendeeCompleter; + QScopedPointer mIdentitySelector; }; diff --git a/views/calendar/qml/AttendeeListEditor.qml b/views/calendar/qml/AttendeeListEditor.qml new file mode 100644 index 00000000..739450d7 --- /dev/null +++ b/views/calendar/qml/AttendeeListEditor.qml @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2017 Michael Bohlender, + * Copyright (C) 2017 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.7 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 + +import org.kube.framework 1.0 as Kube + +FocusScope { + id: root + property var controller + property var completer + + property alias count: listView.count + + implicitHeight: listView.height + lineEdit.height + implicitWidth: listView.width + height: implicitHeight + + Column { + anchors.fill: parent + + spacing: Kube.Units.smallSpacing + + ListView { + id: listView + anchors { + left: parent.left + right: parent.right + } + height: contentHeight + spacing: Kube.Units.smallSpacing + model: controller.model + delegate: Rectangle { + height: Kube.Units.gridUnit + Kube.Units.smallSpacing * 2 //smallSpacing for padding + width: parent.width + color: "transparent" + Kube.Label { + id: label + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: status.left + margins: Kube.Units.smallSpacing + } + text: model.name + elide: Text.ElideRight + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + } + ToolTip.visible: mouseArea.containsMouse + ToolTip.text: text + } + Kube.Label { + id: status + anchors { + verticalCenter: parent.verticalCenter + right: removeButton.left + rightMargin: Kube.Units.smallSpacing + } + text: model.status == Kube.EventController.Accepted ? qsTr("Attending") : qsTr("Invited") + font.italic: true + font.pointSize: Kube.Units.smallFontSize + } + Kube.IconButton { + id: removeButton + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + margins: Kube.Units.smallSpacing + } + height: Kube.Units.gridUnit + width: height + onClicked: root.controller.remove(model.id) + padding: 0 + iconName: Kube.Icons.remove + } + } + } + + FocusScope { + height: Kube.Units.gridUnit + Kube.Units.smallSpacing * 2 + width: parent.width + focus: true + + Kube.TextButton { + id: button + text: "+ " + qsTr("Add recipient") + textColor: Kube.Colors.highlightColor + focus: true + onClicked: { + lineEdit.visible = true + lineEdit.forceActiveFocus() + } + } + + Kube.AutocompleteLineEdit { + id: lineEdit + anchors { + left: parent.left + right: parent.right + } + visible: false + + placeholderText: "+ " + qsTr("Add attendee") + model: root.completer.model + onSearchTermChanged: root.completer.searchString = searchTerm + onAccepted: { + root.controller.add({name: text}); + clear() + visible = false + button.forceActiveFocus(Qt.TabFocusReason) + } + onAborted: { + clear() + visible = false + button.forceActiveFocus(Qt.TabFocusReason) + } + } + } + } +} diff --git a/views/calendar/qml/EventEditor.qml b/views/calendar/qml/EventEditor.qml index cec081bb..274d2a20 100644 --- a/views/calendar/qml/EventEditor.qml +++ b/views/calendar/qml/EventEditor.qml @@ -1,204 +1,240 @@ /* * Copyright (C) 2018 Michael Bohlender, * Copyright (C) 2019 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 org.kube.framework 1.0 as Kube import "dateutils.js" as DateUtils Item { id: root property bool editMode: false property date start: new Date() property bool allDay: false property var controller: Kube.EventController { allDay: root.allDay } property var accountId: null signal done() implicitWidth: contentLayout.implicitWidth + 2 * Kube.Units.largeSpacing implicitHeight: contentLayout.implicitHeight + buttons.implicitHeight + 2 * Kube.Units.largeSpacing states: [ State { name: "edit" PropertyChanges { target: deleteButton; visible: true } PropertyChanges { target: abortButton; visible: false } PropertyChanges { target: saveButton; visible: true } PropertyChanges { target: discardButton; visible: true } PropertyChanges { target: createButton; visible: false } PropertyChanges { target: calendarSelector; visible: false } }, State { name: "new" PropertyChanges { target: deleteButton; visible: false } PropertyChanges { target: abortButton; visible: true } PropertyChanges { target: saveButton; visible: false } PropertyChanges { target: discardButton; visible: false } PropertyChanges { target: createButton; visible: true } PropertyChanges { target: calendarSelector; visible: true } } ] state: editMode ? "edit" : "new" ColumnLayout { id: contentLayout anchors { fill: parent margins: Kube.Units.largeSpacing } spacing: Kube.Units.largeSpacing ColumnLayout { spacing: Kube.Units.largeSpacing Kube.HeaderField { id: titleEdit Layout.fillWidth: true placeholderText: qsTr("Event Title") text: controller.summary onTextChanged: controller.summary = text } ColumnLayout { id: dateAndTimeChooser spacing: Kube.Units.smallSpacing DateRangeChooser { Layout.fillWidth: true enableTime: !controller.allDay initialStart: root.editMode ? controller.start : root.start initialEnd: root.editMode ? controller.end : DateUtils.addMinutesToDate(root.start, 30) onStartChanged: controller.start = start onEndChanged: controller.end = end } RowLayout { spacing: Kube.Units.smallSpacing Kube.CheckBox { checked: controller.allDay onCheckedChanged: { if (controller.allDay != checked) { controller.allDay = checked } } } Kube.Label { text: qsTr("All day") } } } ColumnLayout { spacing: Kube.Units.smallSpacing Layout.fillWidth: true - //FIXME location doesn't exist yet - // Kube.TextField { - // Layout.fillWidth: true - // placeholderText: qsTr("Location") - // text: controller.location - // onTextChanged: controller.location = text - // } + + Kube.TextField { + Layout.fillWidth: true + placeholderText: qsTr("Location") + text: controller.location + onTextChanged: controller.location = text + } + + RowLayout { + visible: attendees.count + Layout.maximumWidth: parent.width + Layout.fillWidth: true + Kube.Label { + id: fromLabel + text: qsTr("Organizer:") + } + + Kube.ComboBox { + id: identityCombo + objectName: "identityCombo" + + width: parent.width - Kube.Units.largeSpacing * 2 + + model: root.controller.identitySelector.model + textRole: "address" + Layout.fillWidth: true + //A regular binding is not enough in this case, we have to use the Binding element + Binding { target: identityCombo; property: "currentIndex"; value: root.controller.identitySelector.currentIndex } + onCurrentIndexChanged: { + root.controller.identitySelector.currentIndex = currentIndex + } + } + } + + AttendeeListEditor { + id: attendees + Layout.preferredHeight: implicitHeight + Layout.preferredWidth: Kube.Units.gridUnit * 12 + focus: true + activeFocusOnTab: true + controller: root.controller.attendees + completer: root.controller.attendeeCompleter + } Kube.TextEditor { Layout.fillWidth: true Layout.fillHeight: true Layout.minimumHeight: Kube.Units.gridUnit * 4 placeholderText: "Description" initialText: controller.description onTextChanged: controller.description = text } Kube.CalendarComboBox { id: calendarSelector Layout.fillWidth: true accountId: root.accountId contentType: "event" onSelected: { if (!root.editMode) { controller.calendar = calendar } } } } } RowLayout { id: buttons spacing: Kube.Units.smallSpacing Kube.Button { id: deleteButton text: qsTr("Delete") onClicked: { controller.remove() root.done() } } Kube.Button { id: abortButton text: qsTr("Abort") onClicked: { root.done() } } Item { Layout.fillWidth: true } Kube.Button { id: discardButton text: qsTr("Discard Changes") onClicked: { root.done() } } Kube.PositiveButton { id: saveButton text: qsTr("Save Changes") onClicked: { controller.saveAction.execute() root.done() } } Kube.PositiveButton { id: createButton text: qsTr("Create Event") onClicked: { controller.saveAction.execute() root.done() } } } } }