Commit 2fc81275 authored by Claudio Cambra's avatar Claudio Cambra
Browse files

Display super/sub tasks in the incidence info drawer

This allows users to see a task's related sub or super tasks in views other than the tasks view.

![image](/uploads/faafd5a80fa5cb8d4aaaba9e01daf8ed/image.png)
parent a4350106
Pipeline #185744 failed with stage
in 3 minutes and 5 seconds
......@@ -55,7 +55,9 @@
#else
#include <etmcalendar.h>
#endif
#include <colorproxymodel.h>
#include <incidencewrapper.h>
using namespace Akonadi;
......@@ -183,6 +185,13 @@ protected:
}
};
Q_GLOBAL_STATIC(CalendarManager, calendarManagerGlobalInstance)
CalendarManager *CalendarManager::instance()
{
return calendarManagerGlobalInstance;
}
CalendarManager::CalendarManager(QObject *parent)
: QObject(parent)
, m_calendar(nullptr)
......@@ -300,7 +309,9 @@ CalendarManager::CalendarManager(QObject *parent)
KConfigGroup rColorsConfig(config, "Resources Colors");
m_colorWatcher = KConfigWatcher::create(config);
QObject::connect(m_colorWatcher.data(), &KConfigWatcher::configChanged, this, &CalendarManager::collectionColorsChanged);
connect(m_colorWatcher.data(), &KConfigWatcher::configChanged, this, &CalendarManager::collectionColorsChanged);
connect(m_calendar.data(), &Akonadi::ETMCalendar::calendarChanged, this, &CalendarManager::calendarChanged);
}
CalendarManager::~CalendarManager()
......@@ -323,7 +334,6 @@ void CalendarManager::save()
void CalendarManager::delayedInit()
{
Q_EMIT entityTreeModelChanged();
Q_EMIT loadingChanged();
}
......@@ -498,11 +508,21 @@ QVariantMap CalendarManager::undoRedoData()
};
}
Akonadi::Item CalendarManager::incidenceItem(KCalendarCore::Incidence::Ptr incidence)
Akonadi::Item CalendarManager::incidenceItem(KCalendarCore::Incidence::Ptr incidence) const
{
return m_calendar->item(incidence);
}
Akonadi::Item CalendarManager::incidenceItem(const QString &uid) const
{
return incidenceItem(m_calendar->incidence(uid));
}
KCalendarCore::Incidence::List CalendarManager::childIncidences(const QString &uid) const
{
return m_calendar->childIncidences(uid);
}
void CalendarManager::addIncidence(IncidenceWrapper *incidenceWrapper)
{
Akonadi::Collection collection(incidenceWrapper->collectionId());
......
......@@ -19,7 +19,8 @@
#include <KConfigWatcher>
#include <QObject>
#include <akonadi-calendar_version.h>
#include <incidencewrapper.h>
class IncidenceWrapper;
namespace Akonadi
{
......@@ -50,6 +51,7 @@ class CalendarManager : public QObject
Q_PROPERTY(QVariantMap undoRedoData READ undoRedoData NOTIFY undoRedoDataChanged)
public:
static CalendarManager *instance();
explicit CalendarManager(QObject *parent = nullptr);
~CalendarManager() override;
......@@ -74,7 +76,10 @@ public:
Q_INVOKABLE int getCalendarSelectableIndex(IncidenceWrapper *incidenceWrapper);
QVariantMap undoRedoData();
Q_INVOKABLE Akonadi::Item incidenceItem(KCalendarCore::Incidence::Ptr incidence);
Q_INVOKABLE Akonadi::Item incidenceItem(KCalendarCore::Incidence::Ptr incidence) const;
Akonadi::Item incidenceItem(const QString &uid) const;
KCalendarCore::Incidence::List childIncidences(const QString &uid) const;
Q_INVOKABLE void addIncidence(IncidenceWrapper *incidenceWrapper);
Q_INVOKABLE void editIncidence(IncidenceWrapper *incidenceWrapper);
Q_INVOKABLE void updateIncidenceDates(IncidenceWrapper *incidenceWrapper,
......@@ -104,12 +109,12 @@ private Q_SLOTS:
Q_SIGNALS:
void loadingChanged();
void entityTreeModelChanged();
void calendarChanged();
void undoRedoDataChanged();
void incidenceChanged();
void enabledTodoCollectionsChanged();
void updateIncidenceDatesCompleted();
void collectionColorsChanged();
void incidenceAdded();
private:
Akonadi::ETMCalendar::Ptr m_calendar = nullptr;
......
......@@ -16,7 +16,7 @@ RowLayout {
signal removeFilterTag(string tagName)
signal searchTextChanged(string text)
property bool isDark: false
property bool isDark: KalendarUiUtils.darkMode
property var mode: Kalendar.KalendarApplication.Event
property var filter: {
"tags": [],
......
......@@ -25,6 +25,8 @@ MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: {
if (mouse.button == Qt.LeftButton) {
collectionDetails = Kalendar.CalendarManager.getCollectionDetails(mouseArea.collectionId)
......
// SPDX-FileCopyrightText: 2022 Claudio Cambra <claudio.cambra@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.14 as Kirigami
import org.kde.kalendar.utils 1.0
ColoredCheckbox {
id: todoCheckbox
property bool todoCompleted
property int todoCompletion
property var todoPtr
color: incidenceColor
radius: 100
checked: todoCompleted || todoCompletion === 100
onClicked: KalendarUiUtils.completeTodo(todoPtr)
indicator: Item {
height: parent.height
width: height
x: todoCheckbox.leftPadding
y: parent.height / 2 - height / 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
// The icon provides the main circle for the checkbox when not checked,
// whereas the rectangle provides the circle when it is checked.
// The rectangle always provides the tinted background.
Kirigami.Icon {
isMask: true
color: todoCheckbox.color
anchors.fill: parent
anchors.margins: parent.height * -0.16
visible: !todoCheckbox.checked
source: {
if (todoCheckbox.todoCompletion >= 75) {
return 'task-process-3';
}
if (todoCheckbox.todoCompletion >= 50) {
return 'task-process-2';
}
if (todoCheckbox.todoCompletion >= 25) {
return 'task-process-1';
}
return 'task-process-0';
}
}
Rectangle {
anchors.fill: parent
radius: todoCheckbox.radius
border.color: todoCheckbox.checked ? todoCheckbox.color : Qt.rgba(0,0,0,0)
color: Qt.rgba(todoCheckbox.color.r, todoCheckbox.color.g, todoCheckbox.color.b, 0.1)
Rectangle {
anchors.margins: parent.height * 0.2
anchors.fill: parent
radius: todoCheckbox.radius / 3
color: todoCheckbox.color
visible: todoCheckbox.checked
}
}
}
}
......@@ -52,7 +52,7 @@ Item {
readonly property bool isInCurrentMonth: reactToCurrentMonth ?
modelData.endTime.getMonth() == root.month || modelData.startTime.getMonth() == root.month :
true
property bool isDark: false
property bool isDark: KalendarUiUtils.darkMode
property alias mouseArea: mouseArea
property var incidencePtr: modelData.incidencePtr
......
......@@ -14,11 +14,11 @@ Kirigami.ShadowedRectangle {
property bool isOpenOccurrence: false
property bool reactToCurrentMonth: false
property bool isInCurrentMonth: true
property bool isDark: false
property bool isDark: KalendarUiUtils.darkMode
anchors.fill: parent
color: isOpenOccurrence ? modelData.color :
LabelUtils.getIncidenceDelegateBackgroundColor(modelData, root.isDark, Kalendar.Config.pastEventsTransparencyLevel)
LabelUtils.getIncidenceDelegateBackgroundColor(modelData.color, root.isDark, modelData.endTime, Kalendar.Config.pastEventsTransparencyLevel)
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } }
opacity: isOpenOccurrence || isInCurrentMonth ? 1.0 : 0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } }
......
// SPDX-FileCopyrightText: 2022 Claudio Cambra <claudio.cambra@gmail.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import org.kde.kirigami 2.14 as Kirigami
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import "labelutils.js" as LabelUtils
import org.kde.kalendar 1.0
import org.kde.kalendar.utils 1.0
Item {
property var incidenceWrapper: modelData
property var collectionData: CalendarManager.getCollectionDetails(incidenceWrapper.collectionId)
Layout.fillWidth: true
IncidenceMouseArea {
id: mouseArea
incidenceData: KalendarUiUtils.fakeModelDataFromIncidenceWrapper(incidenceWrapper)
collectionId: incidenceWrapper.collectionId
preventStealing: !Kirigami.Settings.tabletMode && !Kirigami.Settings.isMobile
// TODO: Add drag support
//drag.target: !Kirigami.Settings.isMobile && !modelData.isReadOnly && incidenceDelegate.dragDropEnabled ? parent : undefined
//onReleased: parent.Drag.drop()
onViewClicked: KalendarUiUtils.setUpView(incidenceData)
onEditClicked: KalendarUiUtils.setUpEdit(incidencePtr)
onDeleteClicked: KalendarUiUtils.setUpDelete(incidencePtr, deleteDate)
onTodoCompletedClicked: KalendarUiUtils.completeTodo(incidencePtr)
onAddSubTodoClicked: KalendarUiUtils.setUpAddSubTodo(parentWrapper)
}
IncidenceDelegateBackground {
color: LabelUtils.getIncidenceDelegateBackgroundColor(collectionData.color, KalendarUiUtils.darkMode)
}
RowLayout {
id: incidenceContents
clip: true
property color textColor: LabelUtils.getIncidenceLabelColor(collectionData.color, KalendarUiUtils.darkMode)
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
Rectangle {
width: todoCheckBox.implicitWidth + 3
height: todoCheckBox.implicitWidth + 3
color: Kirigami.Theme.backgroundColor
radius: 100
TodoCheckBox {
id: todoCheckBox
// anchors.centerIn doesn't really work correctly here so we position manually
x: (parent.width / 2) - (implicitWidth / 2)
y: parent.height / 2 - height / 2
todoCompleted: incidenceWrapper.todoCompleted
todoCompletion: incidenceWrapper.todoPercentComplete
todoPtr: incidenceWrapper.incidencePtr
color: collectionData.color
visible: incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo
}
}
Kirigami.Icon {
Layout.maximumHeight: parent.height
Layout.maximumWidth: height
source: incidenceWrapper.incidenceIconName
isMask: true
color: incidenceContents.textColor
visible: incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo
}
QQC2.Label {
Layout.fillWidth: true
text: incidenceWrapper.summary
elide: Text.ElideRight
font.weight: Font.Medium
font.strikeout: incidenceWrapper.todoCompleted
color: incidenceContents.textColor
}
Kirigami.Icon {
id: recurringIcon
Layout.fillHeight: true
source: "appointment-recurring"
isMask: true
color: incidenceContents.textColor
visible: incidenceWrapper.recurrenceData.type
}
Kirigami.Icon {
id: reminderIcon
Layout.fillHeight: true
source: "appointment-reminder"
isMask: true
color: incidenceContents.textColor
visible: incidenceWrapper.remindersModel.rowCount() > 0
}
QQC2.Label {
text: incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo ?
incidenceWrapper.incidenceEnd.toLocaleTimeString(Qt.locale(), Locale.NarrowFormat) :
incidenceWrapper.incidenceStart.toLocaleTimeString(Qt.locale(), Locale.NarrowFormat)
color: incidenceContents.textColor
visible: !incidenceWrapper.allDay
}
}
}
......@@ -32,6 +32,8 @@ Kirigami.OverlayDrawer {
property var collectionData
property var activeTags : []
readonly property int relatedIncidenceDelegateHeight: Kirigami.Units.gridUnit * 3
component HoverLabel: QQC2.Label {
Layout.fillWidth: true
textFormat: Text.MarkdownText
......@@ -218,6 +220,8 @@ Kirigami.OverlayDrawer {
Kirigami.Separator {
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
visible: todoCompletionLayout.visible
}
......@@ -601,6 +605,59 @@ Kirigami.OverlayDrawer {
delegate: HoverLabel {}
}
}
Kirigami.Separator {
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
visible: superTaskColumn.visible || subTaskColumn.visible
}
ColumnLayout {
id: superTaskColumn
Layout.columnSpan: 2
Layout.fillWidth: true
visible: incidenceInfoDrawer.incidenceWrapper.parent !== ""
Kirigami.Heading {
text: i18n("Super-task")
level: 2
font.weight: Font.Bold
}
Loader {
Layout.fillWidth: true
height: incidenceInfoDrawer.relatedIncidenceDelegateHeight
active: incidenceInfoDrawer.incidenceWrapper.parent !== ""
sourceComponent: RelatedIncidenceDelegate {
incidenceWrapper: incidenceInfoDrawer.incidenceWrapper.parentIncidence
}
}
}
ColumnLayout {
id: subTaskColumn
Layout.topMargin: superTaskColumn.visible ? Kirigami.Units.largeSpacing : 0
Layout.columnSpan: 2
Layout.fillWidth: true
visible: incidenceInfoDrawer.incidenceWrapper.childIncidences.length > 0
Kirigami.Heading {
text: i18np("Sub-task", "Sub-tasks", incidenceInfoDrawer.incidenceWrapper.childIncidences.length)
level: 2
font.weight: Font.Bold
}
Repeater {
model: incidenceInfoDrawer.incidenceWrapper.childIncidences
delegate: RelatedIncidenceDelegate {
implicitHeight: incidenceInfoDrawer.relatedIncidenceDelegateHeight
incidenceWrapper: modelData
}
}
}
}
}
}
......
......@@ -15,6 +15,8 @@ QtObject {
id: utilsObject
property var appMain
readonly property bool darkMode: LabelUtils.isDarkColor(Kirigami.Theme.backgroundColor)
function switchView(newViewComponent, viewSettings) {
if(appMain.pageStack.layers.depth > 1) {
appMain.pageStack.layers.pop(appMain.pageStack.layers.initialItem);
......@@ -109,8 +111,45 @@ QtObject {
}
function setUpView(modelData) {
appMain.contextDrawer.incidenceData = modelData;
appMain.contextDrawer.open();
appMain.incidenceInfoDrawer.incidenceData = modelData;
appMain.incidenceInfoDrawer.open();
}
function fakeModelDataFromIncidenceWrapper(incidenceWrapper) {
// Spoof what a modelData would look like from the model
const collectionDetails = CalendarManager.getCollectionDetails(incidenceWrapper.collectionId)
const fakeModelData = {
"text": incidenceWrapper.summary,
"description": incidenceWrapper.description,
"location": incidenceWrapper.location,
"startTime": incidenceWrapper.incidenceStart,
"endTime": incidenceWrapper.incidenceEnd,
"allDay": incidenceWrapper.allDay,
"todoCompleted": incidenceWrapper.todoCompleted,
"priority": incidenceWrapper.priority,
// These next two are mainly used in the hourly and day grid views, and we don't use this for
// anything but the incidence info drawer -- for now. Remember that they are different to
// the incidence's actual startTime and duration time -- these are just for positioning!
//"starts":
//"duration":
"durationString": incidenceWrapper.durationDisplayString,
"recurs": incidenceWrapper.recurrenceData.type !== 0,
"hasReminders": incidenceWrapper.remindersModel.rowCount() > 0,
"isOverdue": incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo &&
!isNaN(incidenceWrapper.incidenceEnd.getTime()) &&
incidenceWrapper.incidenceEnd < appMain.currentDate,
"isReadOnly": collectionDetails.readOnly,
"color": collectionDetails.color,
"collectionId": incidenceWrapper.collectionId,
"incidenceId": incidenceWrapper.uid,
"incidenceType": incidenceWrapper.incidenceType,
"incidenceTypeStr": incidenceWrapper.incidenceTypeStr,
"incidenceTypeIcon": incidenceWrapper.incidenceIconName,
"incidencePtr": incidenceWrapper.incidencePtr,
//"incidenceOccurrence":
};
return fakeModelData;
}
function setUpEdit(incidencePtr) {
......
......@@ -168,10 +168,10 @@ function isDarkColor(background) {
return temp.a > 0 && getDarkness(background) >= 0.4;
}
function getIncidenceDelegateBackgroundColor(modelData, darkMode, pastEventsDimmLevel=0.0) {
let bgColor = getDarkness(modelData.color) > 0.9 ? Qt.lighter(modelData.color, 1.5) : modelData.color;
function getIncidenceDelegateBackgroundColor(backgroundColor, darkMode, eventEnd = null, pastEventsDimLevel = 0.0) {
let bgColor = getDarkness(backgroundColor) > 0.9 ? Qt.lighter(backgroundColor, 1.5) : backgroundColor;
if(darkMode) {
if(getDarkness(modelData.color) >= 0.5) {
if(getDarkness(backgroundColor) >= 0.5) {
bgColor.a = 0.6;
} else {
bgColor.a = 0.4;
......@@ -179,9 +179,12 @@ function getIncidenceDelegateBackgroundColor(modelData, darkMode, pastEventsDimm
} else {
bgColor.a = 0.7;
}
const now = new Date();
if (modelData.endTime < now) {
bgColor.a = Math.max(0.0, bgColor.a - pastEventsDimmLevel);
if(pastEventsDimLevel > 0 && eventEnd) {
const now = new Date();
if (modelData.endTime < now) {
bgColor.a = Math.max(0.0, bgColor.a - pastEventsDimLevel);
}
}
return bgColor;
}
......
......@@ -47,7 +47,7 @@ Item {
property int spacing: Kalendar.Config.monthGridBorderWidth // Between grid squares in background
property int listViewSpacing: root.dayWidth < (Kirigami.Units.gridUnit * 5 + Kirigami.Units.smallSpacing * 2) ?
Kirigami.Units.smallSpacing / 2 : Kirigami.Units.smallSpacing // Between lines of incidences ( ====== <- )
readonly property bool isDark: LabelUtils.isDarkColor(Kirigami.Theme.backgroundColor)
readonly property bool isDark: KalendarUiUtils.darkMode
readonly property int mode: Kalendar.KalendarApplication.Event
implicitHeight: (numberOfRows > 1 ? Kirigami.Units.gridUnit * 10 * numberOfRows : numberOfLinesShown * Kirigami.Units.gridUnit) + bgLoader.dayLabelsBar.height
......
......@@ -30,7 +30,7 @@ Kirigami.Page {
property bool initialWeek: true
property int daysToShow: 7
readonly property int minutesFromStartOfDay: (root.currentDate.getHours() * 60) + root.currentDate.getMinutes()
readonly property bool isDark: LabelUtils.isDarkColor(Kirigami.Theme.backgroundColor)
readonly property bool isDark: KalendarUiUtils.darkMode
property bool dragDropEnabled: true
property real scrollbarWidth: 0
......
......@@ -59,7 +59,7 @@ Kirigami.Page {
property int year: selectedDate.getFullYear()
property bool initialMonth: true
readonly property bool isLarge: width > Kirigami.Units.gridUnit * 30
readonly property bool isDark: LabelUtils.isDarkColor(Kirigami.Theme.backgroundColor)
readonly property bool isDark: KalendarUiUtils.darkMode
property real maxTimeLabelWidth: 0
property bool dragDropEnabled: true
readonly property int mode: Kalendar.KalendarApplication.Event
......
......@@ -43,7 +43,7 @@ TreeListView {
property bool dragDropEnabled: true
property alias model: todoModel
readonly property bool isDark: LabelUtils.isDarkColor(Kirigami.Theme.backgroundColor)
readonly property bool isDark: KalendarUiUtils.darkMode
currentIndex: -1
clip: true
......@@ -145,7 +145,7 @@ TreeListView {
objectName: "taskDelegate"
decoration.decorationHighlightColor: model.color
activeBackgroundColor: LabelUtils.getIncidenceDelegateBackgroundColor(model, root.isDark)
activeBackgroundColor: LabelUtils.getIncidenceDelegateBackgroundColor(model.color, root.isDark)
onActiveBackgroundColorChanged: activeBackgroundColor.a = 0.15
Kirigami.Theme.inherit: false
Kirigami.Theme.highlightColor: activeBackgroundColor
......@@ -241,62 +241,15 @@ TreeListView {
rows: 2
columnSpacing: Kirigami.Settings.isMobile ? Kirigami.Units.largeSpacing * 2 : Kirigami.Units.largeSpacing
ColoredCheckbox {
id: todoCheckbox
TodoCheckBox {
Layout.row: 0
Layout.column: 0
Layout.rowSpan: root.width < Kirigami.Units.gridUnit * 28 || recurIcon.visible || dateLabel.visible ? 1 : 2
indicator: Item {
height: parent.height
width: height
x: todoCheckbox.leftPadding
y: parent.height / 2 - height / 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
// The icon provides the main circle for the checkbox when not checked,
// whereas the rectangle provides the circle when it is checked.
// The rectangle always provides the tinted background.
Kirigami.Icon {
isMask: true
color: model.color
anchors.fill: parent
anchors.margins: parent.height * -0.16
visible: !todoCheckbox.checked
source: {
if (model.percent >= 75) {
return 'task-process-3';
}
if (model.percent >= 50) {
return 'task-process-2';
}
if (model.percent >= 25) {
return 'task-process-1';