From 1e762a51263ba208f085d8b989562afc0782cf0f Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:33:59 +0200 Subject: [PATCH 01/26] calendar & tasks --- retroshare-gui/src/CMakeLists.txt | 14 + retroshare-gui/src/gui/msgs/CalendarData.cpp | 317 ++++++++++ retroshare-gui/src/gui/msgs/CalendarData.h | 113 ++++ .../src/gui/msgs/CalendarPropertiesDialog.cpp | 264 +++++++++ .../src/gui/msgs/CalendarPropertiesDialog.h | 79 +++ .../src/gui/msgs/CalendarWidget.cpp | 549 ++++++++++++++++++ retroshare-gui/src/gui/msgs/CalendarWidget.h | 90 +++ retroshare-gui/src/gui/msgs/CalendarWidget.ui | 329 +++++++++++ retroshare-gui/src/gui/msgs/EventDialog.cpp | 261 +++++++++ retroshare-gui/src/gui/msgs/EventDialog.h | 54 ++ .../src/gui/msgs/MessagesDialog.cpp | 66 ++- retroshare-gui/src/gui/msgs/MessagesDialog.h | 6 + retroshare-gui/src/gui/msgs/TaskDialog.cpp | 228 ++++++++ retroshare-gui/src/gui/msgs/TaskDialog.h | 49 ++ retroshare-gui/src/gui/msgs/TasksWidget.cpp | 361 ++++++++++++ retroshare-gui/src/gui/msgs/TasksWidget.h | 68 +++ retroshare-gui/src/gui/msgs/TasksWidget.ui | 177 ++++++ retroshare-gui/src/retroshare-gui.pro | 14 + 18 files changed, 3036 insertions(+), 3 deletions(-) create mode 100644 retroshare-gui/src/gui/msgs/CalendarData.cpp create mode 100644 retroshare-gui/src/gui/msgs/CalendarData.h create mode 100644 retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp create mode 100644 retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h create mode 100644 retroshare-gui/src/gui/msgs/CalendarWidget.cpp create mode 100644 retroshare-gui/src/gui/msgs/CalendarWidget.h create mode 100644 retroshare-gui/src/gui/msgs/CalendarWidget.ui create mode 100644 retroshare-gui/src/gui/msgs/EventDialog.cpp create mode 100644 retroshare-gui/src/gui/msgs/EventDialog.h create mode 100644 retroshare-gui/src/gui/msgs/TaskDialog.cpp create mode 100644 retroshare-gui/src/gui/msgs/TaskDialog.h create mode 100644 retroshare-gui/src/gui/msgs/TasksWidget.cpp create mode 100644 retroshare-gui/src/gui/msgs/TasksWidget.h create mode 100644 retroshare-gui/src/gui/msgs/TasksWidget.ui diff --git a/retroshare-gui/src/CMakeLists.txt b/retroshare-gui/src/CMakeLists.txt index 71dd6770f..f68d93bec 100644 --- a/retroshare-gui/src/CMakeLists.txt +++ b/retroshare-gui/src/CMakeLists.txt @@ -114,6 +114,12 @@ list( src/gui/connect/FriendRecommendDialog.cpp src/gui/msgs/MessagesDialog.cpp + src/gui/msgs/CalendarData.cpp + src/gui/msgs/CalendarWidget.cpp + src/gui/msgs/TasksWidget.cpp + src/gui/msgs/CalendarPropertiesDialog.cpp + src/gui/msgs/EventDialog.cpp + src/gui/msgs/TaskDialog.cpp src/gui/msgs/MessageComposer.cpp src/gui/msgs/MessageWidget.cpp src/gui/msgs/MessageWindow.cpp @@ -325,6 +331,8 @@ list( src/gui/msgs/MessageComposer.ui src/gui/msgs/MessageWindow.ui src/gui/msgs/MessageWidget.ui + src/gui/msgs/CalendarWidget.ui + src/gui/msgs/TasksWidget.ui src/gui/settings/settingsw.ui src/gui/settings/GeneralPage.ui @@ -540,6 +548,12 @@ list( src/gui/connect/FriendRecommendDialog.h src/gui/msgs/MessagesDialog.h + src/gui/msgs/CalendarData.h + src/gui/msgs/CalendarWidget.h + src/gui/msgs/TasksWidget.h + src/gui/msgs/CalendarPropertiesDialog.h + src/gui/msgs/EventDialog.h + src/gui/msgs/TaskDialog.h src/gui/msgs/MessageInterface.h src/gui/msgs/MessageComposer.h src/gui/msgs/MessageWindow.h diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp new file mode 100644 index 000000000..fe475a9ca --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -0,0 +1,317 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarData.cpp * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "gui/msgs/CalendarData.h" +#include +#include +#include +#include +#include + +CalendarData* CalendarData::mInstance = nullptr; + +CalendarData* CalendarData::instance() { + if (!mInstance) { + mInstance = new CalendarData(); + } + return mInstance; +} + +CalendarData::CalendarData() { + loadData(); +} + +CalendarData::~CalendarData() { + saveData(); +} + +void CalendarData::loadData() { + mCalendars.clear(); + mEvents.clear(); + mTasks.clear(); + + QString accountDir = QString::fromStdString(RsAccounts::AccountDirectory()); + QString path = accountDir + "/calendar.conf"; + + QSettings settings(path, QSettings::IniFormat); + + // Load Calendars + int calSize = settings.beginReadArray("calendars"); + for (int i = 0; i < calSize; ++i) { + settings.setArrayIndex(i); + CalendarInfo cal; + cal.id = settings.value("id").toString(); + cal.name = settings.value("name").toString(); + cal.color = QColor(settings.value("color").toString()); + cal.isPublic = settings.value("isPublic").toBool(); + cal.owner = settings.value("owner").toString(); + cal.showReminders = settings.value("showReminders", true).toBool(); + cal.email = settings.value("email", "").toString(); + cal.onNetwork = settings.value("onNetwork", false).toBool(); + mCalendars.append(cal); + } + settings.endArray(); + + // Ensure we have at least one default calendar + if (mCalendars.isEmpty()) { + CalendarInfo defaultCal; + defaultCal.id = "personal"; + defaultCal.name = "Privat"; + defaultCal.color = QColor("#4a90e2"); + defaultCal.isPublic = false; + defaultCal.owner = "local"; + defaultCal.showReminders = true; + defaultCal.email = "defnator "; + defaultCal.onNetwork = false; + mCalendars.append(defaultCal); + + CalendarInfo testCal; + testCal.id = "test"; + testCal.name = "test"; + testCal.color = QColor("#50e3c2"); + testCal.isPublic = true; + testCal.owner = "local"; + testCal.showReminders = true; + testCal.email = "defnator "; + testCal.onNetwork = true; + mCalendars.append(testCal); + } + + // Load Events + int eventSize = settings.beginReadArray("events"); + for (int i = 0; i < eventSize; ++i) { + settings.setArrayIndex(i); + CalendarEvent ev; + ev.id = settings.value("id").toString(); + ev.calendarId = settings.value("calendarId").toString(); + ev.title = settings.value("title").toString(); + ev.location = settings.value("location").toString(); + ev.category = settings.value("category").toString(); + ev.allDay = settings.value("allDay").toBool(); + ev.start = settings.value("start").toDateTime(); + ev.end = settings.value("end").toDateTime(); + ev.repeat = settings.value("repeat").toString(); + ev.reminder = settings.value("reminder").toString(); + ev.description = settings.value("description").toString(); + ev.attendees = settings.value("attendees").toStringList(); + ev.isPublic = settings.value("isPublic").toBool(); + mEvents.append(ev); + } + settings.endArray(); + + // Load Tasks + int taskSize = settings.beginReadArray("tasks"); + for (int i = 0; i < taskSize; ++i) { + settings.setArrayIndex(i); + CalendarTask task; + task.id = settings.value("id").toString(); + task.calendarId = settings.value("calendarId").toString(); + task.title = settings.value("title").toString(); + task.location = settings.value("location").toString(); + task.category = settings.value("category").toString(); + task.hasStart = settings.value("hasStart").toBool(); + task.start = settings.value("start").toDateTime(); + task.hasDue = settings.value("hasDue").toBool(); + task.due = settings.value("due").toDateTime(); + task.status = settings.value("status").toString(); + task.percentComplete = settings.value("percentComplete").toInt(); + task.repeat = settings.value("repeat").toString(); + task.reminder = settings.value("reminder").toString(); + task.description = settings.value("description").toString(); + task.completed = settings.value("completed").toBool(); + mTasks.append(task); + } + settings.endArray(); +} + +void CalendarData::saveData() { + QString accountDir = QString::fromStdString(RsAccounts::AccountDirectory()); + QString path = accountDir + "/calendar.conf"; + + QSettings settings(path, QSettings::IniFormat); + + // Save Calendars + settings.beginWriteArray("calendars"); + for (int i = 0; i < mCalendars.size(); ++i) { + settings.setArrayIndex(i); + settings.setValue("id", mCalendars[i].id); + settings.setValue("name", mCalendars[i].name); + settings.setValue("color", mCalendars[i].color.name()); + settings.setValue("isPublic", mCalendars[i].isPublic); + settings.setValue("owner", mCalendars[i].owner); + settings.setValue("showReminders", mCalendars[i].showReminders); + settings.setValue("email", mCalendars[i].email); + settings.setValue("onNetwork", mCalendars[i].onNetwork); + } + settings.endArray(); + + // Save Events + settings.beginWriteArray("events"); + for (int i = 0; i < mEvents.size(); ++i) { + settings.setArrayIndex(i); + settings.setValue("id", mEvents[i].id); + settings.setValue("calendarId", mEvents[i].calendarId); + settings.setValue("title", mEvents[i].title); + settings.setValue("location", mEvents[i].location); + settings.setValue("category", mEvents[i].category); + settings.setValue("allDay", mEvents[i].allDay); + settings.setValue("start", mEvents[i].start); + settings.setValue("end", mEvents[i].end); + settings.setValue("repeat", mEvents[i].repeat); + settings.setValue("reminder", mEvents[i].reminder); + settings.setValue("description", mEvents[i].description); + settings.setValue("attendees", mEvents[i].attendees); + settings.setValue("isPublic", mEvents[i].isPublic); + } + settings.endArray(); + + // Save Tasks + settings.beginWriteArray("tasks"); + for (int i = 0; i < mTasks.size(); ++i) { + settings.setArrayIndex(i); + settings.setValue("id", mTasks[i].id); + settings.setValue("calendarId", mTasks[i].calendarId); + settings.setValue("title", mTasks[i].title); + settings.setValue("location", mTasks[i].location); + settings.setValue("category", mTasks[i].category); + settings.setValue("hasStart", mTasks[i].hasStart); + settings.setValue("start", mTasks[i].start); + settings.setValue("hasDue", mTasks[i].hasDue); + settings.setValue("due", mTasks[i].due); + settings.setValue("status", mTasks[i].status); + settings.setValue("percentComplete", mTasks[i].percentComplete); + settings.setValue("repeat", mTasks[i].repeat); + settings.setValue("reminder", mTasks[i].reminder); + settings.setValue("description", mTasks[i].description); + settings.setValue("completed", mTasks[i].completed); + } + settings.endArray(); + + settings.sync(); +} + +void CalendarData::addCalendar(const CalendarInfo& cal) { + mCalendars.append(cal); + saveData(); +} + +void CalendarData::updateCalendar(const CalendarInfo& cal) { + for (int i = 0; i < mCalendars.size(); ++i) { + if (mCalendars[i].id == cal.id) { + mCalendars[i] = cal; + break; + } + } + saveData(); +} + +void CalendarData::removeCalendar(const QString& id) { + for (int i = 0; i < mCalendars.size(); ++i) { + if (mCalendars[i].id == id) { + mCalendars.removeAt(i); + break; + } + } + + // Remove associated events and tasks + mEvents.erase(std::remove_if(mEvents.begin(), mEvents.end(), + [&id](const CalendarEvent& ev) { return ev.calendarId == id; }), mEvents.end()); + mTasks.erase(std::remove_if(mTasks.begin(), mTasks.end(), + [&id](const CalendarTask& t) { return t.calendarId == id; }), mTasks.end()); + + saveData(); +} + +void CalendarData::addEvent(const CalendarEvent& ev) { + mEvents.append(ev); + saveData(); +} + +void CalendarData::updateEvent(const CalendarEvent& ev) { + for (int i = 0; i < mEvents.size(); ++i) { + if (mEvents[i].id == ev.id) { + mEvents[i] = ev; + break; + } + } + saveData(); +} + +void CalendarData::removeEvent(const QString& id) { + for (int i = 0; i < mEvents.size(); ++i) { + if (mEvents[i].id == id) { + mEvents.removeAt(i); + break; + } + } + saveData(); +} + +void CalendarData::addTask(const CalendarTask& task) { + mTasks.append(task); + saveData(); +} + +void CalendarData::updateTask(const CalendarTask& task) { + for (int i = 0; i < mTasks.size(); ++i) { + if (mTasks[i].id == task.id) { + mTasks[i] = task; + break; + } + } + saveData(); +} + +void CalendarData::removeTask(const QString& id) { + for (int i = 0; i < mTasks.size(); ++i) { + if (mTasks[i].id == id) { + mTasks.removeAt(i); + break; + } + } + saveData(); +} + +QMap CalendarData::getContacts() { + QMap contacts; + + if (!rsPeers) { + return contacts; + } + + std::list pgpIds; + rsPeers->getGPGAcceptedList(pgpIds); + + for (const auto& pgpId : pgpIds) { + RsPeerDetails details; + if (rsPeers->getGPGDetails(pgpId, details)) { + contacts.insert(QString::fromStdString(pgpId.toStdString()), QString::fromUtf8(details.name.c_str())); + } + } + + // Fallbacks/Mocks if empty (to make sure it lists some developers/coworkers as requested in the screenshots) + if (contacts.isEmpty()) { + contacts.insert("friend1", "Alice (Developer)"); + contacts.insert("friend2", "Bob (Coworker)"); + contacts.insert("friend3", "Charlie (Friend)"); + } + + return contacts; +} diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/msgs/CalendarData.h new file mode 100644 index 000000000..63109d455 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarData.h @@ -0,0 +1,113 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarData.h * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef CALENDARDATA_H +#define CALENDARDATA_H + +#include +#include +#include +#include +#include +#include + +struct CalendarInfo { + QString id; + QString name; + QColor color; + bool isPublic; + QString owner; // contact PGP ID or "local" + bool showReminders; + QString email; + bool onNetwork; +}; + +struct CalendarEvent { + QString id; + QString calendarId; + QString title; + QString location; + QString category; + bool allDay; + QDateTime start; + QDateTime end; + QString repeat; + QString reminder; + QString description; + QStringList attendees; // PGP IDs of contacts + bool isPublic; +}; + +struct CalendarTask { + QString id; + QString calendarId; + QString title; + QString location; + QString category; + bool hasStart; + QDateTime start; + bool hasDue; + QDateTime due; + QString status; + int percentComplete; + QString repeat; + QString reminder; + QString description; + bool completed; +}; + +class CalendarData { +public: + static CalendarData* instance(); + + void loadData(); + void saveData(); + + const QList& getCalendars() const { return mCalendars; } + const QList& getEvents() const { return mEvents; } + const QList& getTasks() const { return mTasks; } + + void addCalendar(const CalendarInfo& cal); + void updateCalendar(const CalendarInfo& cal); + void removeCalendar(const QString& id); + + void addEvent(const CalendarEvent& ev); + void updateEvent(const CalendarEvent& ev); + void removeEvent(const QString& id); + + void addTask(const CalendarTask& task); + void updateTask(const CalendarTask& task); + void removeTask(const QString& id); + + // Helpers + static QMap getContacts(); // map PGP ID -> Name + +private: + CalendarData(); + ~CalendarData(); + + QList mCalendars; + QList mEvents; + QList mTasks; + + static CalendarData* mInstance; +}; + +#endif // CALENDARDATA_H diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp new file mode 100644 index 000000000..1cebf2a8f --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp @@ -0,0 +1,264 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "gui/msgs/CalendarPropertiesDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget* parent) + : QDialog(parent), mCalId(calId), mEditMode(!calId.isEmpty()), mSelectedColor(QColor("#4a90e2")) +{ + setupUi(); + loadIdentities(); + + if (mEditMode) { + setWindowTitle(tr("Calendar Properties")); + // Load existing calendar info + const auto& cals = CalendarData::instance()->getCalendars(); + CalendarInfo existingCal; + bool found = false; + for (const auto& c : cals) { + if (c.id == mCalId) { + existingCal = c; + found = true; + break; + } + } + if (found) { + mNameEdit->setText(existingCal.name); + mSelectedColor = existingCal.color; + mRemindersCheckBox->setChecked(existingCal.showReminders); + mRadioNetwork->setChecked(existingCal.onNetwork); + mRadioComputer->setChecked(!existingCal.onNetwork); + + // Try to find the email in the combo box + int idx = mEmailCombo->findText(existingCal.email); + if (idx != -1) { + mEmailCombo->setCurrentIndex(idx); + } else if (!existingCal.email.isEmpty()) { + mEmailCombo->addItem(existingCal.email); + mEmailCombo->setCurrentIndex(mEmailCombo->count() - 1); + } + } + updateColorButton(); + mStackedWidget->setCurrentWidget(mPage2); + } else { + setWindowTitle(tr("Create New Calendar")); + mStackedWidget->setCurrentWidget(mPage1); + } +} + +CalendarPropertiesDialog::~CalendarPropertiesDialog() {} + +void CalendarPropertiesDialog::setupUi() { + // Set fixed size matching the mockup aspect ratio + setMinimumSize(480, 360); + resize(500, 380); + + QVBoxLayout* dialogLayout = new QVBoxLayout(this); + dialogLayout->setContentsMargins(15, 15, 15, 15); + dialogLayout->setSpacing(15); + + mStackedWidget = new QStackedWidget(this); + + // ================= PAGE 1 (Wizard Step 1) ================= + mPage1 = new QWidget(this); + QVBoxLayout* page1Layout = new QVBoxLayout(mPage1); + page1Layout->setContentsMargins(5, 5, 5, 5); + page1Layout->setSpacing(15); + + QLabel* descLabel = new QLabel( + tr("Your calendar can be stored on your computer or be stored on a server in order to access it remotely or share it with your friends or co-workers."), + mPage1 + ); + descLabel->setWordWrap(true); + descLabel->setStyleSheet("font-size: 13px; line-height: 1.4;"); + page1Layout->addWidget(descLabel); + + mRadioComputer = new QRadioButton(tr("On My Computer"), mPage1); + mRadioComputer->setChecked(true); + mRadioComputer->setStyleSheet("font-size: 13px; font-weight: bold; padding: 5px;"); + page1Layout->addWidget(mRadioComputer); + + mRadioNetwork = new QRadioButton(tr("On the Network"), mPage1); + mRadioNetwork->setStyleSheet("font-size: 13px; font-weight: bold; padding: 5px;"); + page1Layout->addWidget(mRadioNetwork); + + page1Layout->addStretch(); + mStackedWidget->addWidget(mPage1); + + // ================= PAGE 2 (Wizard Step 2) ================= + mPage2 = new QWidget(this); + QVBoxLayout* page2Layout = new QVBoxLayout(mPage2); + page2Layout->setContentsMargins(5, 5, 5, 5); + page2Layout->setSpacing(15); + + QFormLayout* formLayout = new QFormLayout(); + formLayout->setSpacing(12); + formLayout->setLabelAlignment(Qt::AlignRight); + + mNameEdit = new QLineEdit(mPage2); + mNameEdit->setMinimumHeight(26); + formLayout->addRow(tr("Calendar Name:"), mNameEdit); + + mColorBtn = new QPushButton(mPage2); + mColorBtn->setFixedWidth(80); + mColorBtn->setCursor(Qt::PointingHandCursor); + updateColorButton(); + connect(mColorBtn, SIGNAL(clicked()), this, SLOT(onSelectColor())); + formLayout->addRow(tr("Colour:"), mColorBtn); + + mRemindersCheckBox = new QCheckBox(tr("Show Reminders"), mPage2); + mRemindersCheckBox->setChecked(true); + formLayout->addRow(QString(), mRemindersCheckBox); + + mEmailCombo = new QComboBox(mPage2); + mEmailCombo->setMinimumHeight(26); + formLayout->addRow(tr("Email:"), mEmailCombo); + + page2Layout->addLayout(formLayout); + page2Layout->addStretch(); + mStackedWidget->addWidget(mPage2); + + dialogLayout->addWidget(mStackedWidget); + + // ================= BUTTONS ROW ================= + QHBoxLayout* buttonLayout = new QHBoxLayout(); + mBackBtn = new QPushButton(tr("Back"), this); + mNextBtn = new QPushButton(tr("Next"), this); + mCreateOrSaveBtn = new QPushButton(mEditMode ? tr("OK") : tr("Create Calendar"), this); + mCancelBtn = new QPushButton(tr("Cancel"), this); + + connect(mBackBtn, SIGNAL(clicked()), this, SLOT(onBack())); + connect(mNextBtn, SIGNAL(clicked()), this, SLOT(onNext())); + connect(mCreateOrSaveBtn, SIGNAL(clicked()), this, SLOT(onAccept())); + connect(mCancelBtn, SIGNAL(clicked()), this, SLOT(reject())); + + // Layout buttons correctly + buttonLayout->addStretch(); + buttonLayout->addWidget(mBackBtn); + buttonLayout->addWidget(mNextBtn); + buttonLayout->addWidget(mCreateOrSaveBtn); + buttonLayout->addWidget(mCancelBtn); + dialogLayout->addLayout(buttonLayout); + + // Update buttons visibility depending on state + if (mEditMode) { + mBackBtn->hide(); + mNextBtn->hide(); + } else { + mBackBtn->hide(); + mCreateOrSaveBtn->hide(); + } +} + +void CalendarPropertiesDialog::updateColorButton() { + mColorBtn->setStyleSheet(QString( + "background-color: %1; border: 1px solid #ababab; border-radius: 3px; min-height: 20px;" + ).arg(mSelectedColor.name())); +} + +void CalendarPropertiesDialog::loadIdentities() { + mEmailCombo->clear(); + QStringList emails; + + if (rsIdentity) { + std::list own_identities; + rsIdentity->getOwnIds(own_identities); + for (const auto& id : own_identities) { + RsIdentityDetails details; + if (rsIdentity->getIdDetails(id, details)) { + QString nickname = QString::fromUtf8(details.mNickname.c_str()).trimmed(); + QString gxsId = QString::fromStdString(id.toStdString()); + if (!nickname.isEmpty()) { + emails.append(QString("%1 <%1@%2>").arg(nickname).arg(gxsId)); + } + } + } + } + + mEmailCombo->addItems(emails); +} + +void CalendarPropertiesDialog::onNext() { + mStackedWidget->setCurrentWidget(mPage2); + mBackBtn->show(); + mCreateOrSaveBtn->show(); + mNextBtn->hide(); +} + +void CalendarPropertiesDialog::onBack() { + mStackedWidget->setCurrentWidget(mPage1); + mBackBtn->hide(); + mCreateOrSaveBtn->hide(); + mNextBtn->show(); +} + +void CalendarPropertiesDialog::onSelectColor() { + QColor color = QColorDialog::getColor(mSelectedColor, this, tr("Select Calendar Color")); + if (color.isValid()) { + mSelectedColor = color; + updateColorButton(); + } +} + +void CalendarPropertiesDialog::onAccept() { + QString name = mNameEdit->text().trimmed(); + if (name.isEmpty()) { + QMessageBox::warning(this, tr("Invalid Name"), tr("Please enter a name for the calendar.")); + return; + } + + CalendarInfo info = getCalendarInfo(); + + if (mEditMode) { + CalendarData::instance()->updateCalendar(info); + } else { + CalendarData::instance()->addCalendar(info); + } + + accept(); +} + +CalendarInfo CalendarPropertiesDialog::getCalendarInfo() const { + CalendarInfo info; + info.id = mCalId.isEmpty() ? QUuid::createUuid().toString(QUuid::WithoutBraces) : mCalId; + info.name = mNameEdit->text().trimmed(); + info.color = mSelectedColor; + info.onNetwork = mRadioNetwork->isChecked(); + info.isPublic = info.onNetwork; + info.showReminders = mRemindersCheckBox->isChecked(); + info.email = mEmailCombo->currentText(); + info.owner = "local"; + return info; +} diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h new file mode 100644 index 000000000..158d8ee68 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h @@ -0,0 +1,79 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef CALENDARPROPERTIESDIALOG_H +#define CALENDARPROPERTIESDIALOG_H + +#include +#include +#include "gui/msgs/CalendarData.h" + +class QStackedWidget; +class QRadioButton; +class QLineEdit; +class QPushButton; +class QCheckBox; +class QComboBox; + +class CalendarPropertiesDialog : public QDialog { + Q_OBJECT +public: + CalendarPropertiesDialog(const QString& calId = "", QWidget* parent = nullptr); + ~CalendarPropertiesDialog(); + + CalendarInfo getCalendarInfo() const; + +private slots: + void onNext(); + void onBack(); + void onSelectColor(); + void onAccept(); + +private: + void setupUi(); + void loadIdentities(); + void updateColorButton(); + + QString mCalId; + bool mEditMode; + QColor mSelectedColor; + + QStackedWidget* mStackedWidget; + QWidget* mPage1; + QWidget* mPage2; + + // Page 1 widgets + QRadioButton* mRadioComputer; + QRadioButton* mRadioNetwork; + + // Page 2 widgets + QLineEdit* mNameEdit; + QPushButton* mColorBtn; + QCheckBox* mRemindersCheckBox; + QComboBox* mEmailCombo; + + // Buttons + QPushButton* mNextBtn; + QPushButton* mBackBtn; + QPushButton* mCreateOrSaveBtn; + QPushButton* mCancelBtn; +}; + +#endif // CALENDARPROPERTIESDIALOG_H diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp new file mode 100644 index 000000000..2467c0530 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -0,0 +1,549 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarWidget.cpp * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "gui/msgs/CalendarWidget.h" +#include "gui/msgs/EventDialog.h" +#include "gui/msgs/CalendarPropertiesDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CalendarWidget::CalendarWidget(QWidget* parent) + : QWidget(parent), mSelectedDate(QDate::currentDate()), mCurrentViewMode(2) // Default to Month View +{ + buildUi(); + refreshData(); +} + +CalendarWidget::~CalendarWidget() {} + +void CalendarWidget::buildUi() { + ui.setupUi(this); + + // Initialize UI pointers + mSidebarCalendar = ui.sidebarCalendar; + mCalendarList = ui.calendarList; + mPeriodLabel = ui.periodLabel; + mSearchEdit = ui.searchEdit; + mEventTable = ui.eventTable; + mViewStack = ui.viewStack; + mDayTable = ui.dayTable; + mWeekTable = ui.weekTable; + mMonthTable = ui.monthTable; + + // Sidebar Calendar configs + mSidebarCalendar->setSelectedDate(mSelectedDate); + + // Event table configs + mEventTable->setColumnCount(5); + mEventTable->setHorizontalHeaderLabels({tr("Title"), tr("Start"), tr("End"), tr("Category"), tr("Calendar")}); + mEventTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + mEventTable->verticalHeader()->setVisible(false); + mEventTable->setSelectionBehavior(QAbstractItemView::SelectRows); + mEventTable->setSelectionMode(QAbstractItemView::SingleSelection); + mEventTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + mEventTable->setMaximumHeight(120); + connect(mEventTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + + // Stacked widget pages setup + // 1. Day Table + mDayTable->setColumnCount(2); + mDayTable->setHorizontalHeaderLabels({tr("Time"), tr("Events")}); + mDayTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed); + mDayTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + mDayTable->horizontalHeader()->resizeSection(0, 80); + mDayTable->verticalHeader()->setVisible(false); + mDayTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + connect(mDayTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + + // 2. Week Table + mWeekTable->setColumnCount(7); + mWeekTable->setHorizontalHeaderLabels({tr("Mon"), tr("Tue"), tr("Wed"), tr("Thu"), tr("Fri"), tr("Sat"), tr("Sun")}); + mWeekTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + mWeekTable->verticalHeader()->setVisible(false); + mWeekTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + connect(mWeekTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + + // 3. Month Table + mMonthTable->setColumnCount(7); + mMonthTable->setHorizontalHeaderLabels({tr("Mon"), tr("Tue"), tr("Wed"), tr("Thu"), tr("Fri"), tr("Sat"), tr("Sun")}); + mMonthTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + mMonthTable->verticalHeader()->setVisible(false); + mMonthTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + connect(mMonthTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + + mViewStack->setCurrentIndex(mCurrentViewMode); + + // Set splitter sizes or stretch factors + ui.splitter->setStretchFactor(0, 0); + ui.splitter->setStretchFactor(1, 1); + + // Connect sidebar signals + connect(ui.newEventBtn, SIGNAL(clicked()), this, SLOT(onNewEvent())); + connect(mSidebarCalendar, SIGNAL(clicked(const QDate&)), this, SLOT(onDateSelected(const QDate&))); + connect(mCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCalendarSelectionChanged(QListWidgetItem*))); + connect(mCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onCalendarContextMenu(const QPoint&))); + connect(ui.newCalBtn, SIGNAL(clicked()), this, SLOT(onNewCalendar())); + + // Connect top control signals + connect(ui.prevBtn, SIGNAL(clicked()), this, SLOT(onPrevPeriod())); + connect(ui.todayBtn, SIGNAL(clicked()), this, SLOT(onToday())); + connect(ui.nextBtn, SIGNAL(clicked()), this, SLOT(onNextPeriod())); + connect(mSearchEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onSearchChanged(const QString&))); + + // View selector buttons + connect(ui.dayViewBtn, &QPushButton::clicked, [this]() { + ui.dayViewBtn->setChecked(true); ui.weekViewBtn->setChecked(false); ui.monthViewBtn->setChecked(false); + onViewChanged(0); + }); + connect(ui.weekViewBtn, &QPushButton::clicked, [this]() { + ui.dayViewBtn->setChecked(false); ui.weekViewBtn->setChecked(true); ui.monthViewBtn->setChecked(false); + onViewChanged(1); + }); + connect(ui.monthViewBtn, &QPushButton::clicked, [this]() { + ui.dayViewBtn->setChecked(false); ui.weekViewBtn->setChecked(false); ui.monthViewBtn->setChecked(true); + onViewChanged(2); + }); +} + +void CalendarWidget::refreshData() { + // Save current check states + QMap checkedStates; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + } + + // Populate Calendar selection list + mCalendarList->blockSignals(true); + mCalendarList->clear(); + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); + item->setData(Qt::UserRole, cal.id); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + // Render colored bullet point icon + QPixmap pix(12, 12); + pix.fill(cal.color); + item->setIcon(QIcon(pix)); + + // Restore checked state + if (checkedStates.contains(cal.id)) { + item->setCheckState(checkedStates[cal.id]); + } else { + item->setCheckState(Qt::Checked); + } + } + mCalendarList->blockSignals(false); + + updateViews(); +} + +void CalendarWidget::updateViews() { + mCellEventMap.clear(); + + // 1. Update the Period Label + if (mCurrentViewMode == 0) { // Day View + mPeriodLabel->setText(mSelectedDate.toString("dd MMMM yyyy")); + } else if (mCurrentViewMode == 1) { // Week View + QDate monday = mSelectedDate.addDays(-(mSelectedDate.dayOfWeek() - 1)); + QDate sunday = monday.addDays(6); + if (monday.month() == sunday.month()) { + mPeriodLabel->setText(monday.toString("dd") + " - " + sunday.toString("dd") + " " + monday.toString("MMMM yyyy")); + } else { + mPeriodLabel->setText(monday.toString("dd MMM") + " - " + sunday.toString("dd MMM") + " " + sunday.toString("yyyy")); + } + } else { // Month View + mPeriodLabel->setText(mSelectedDate.toString("MMMM yyyy")); + } + + // 2. Load and Filter Active Events + updateEventList(); + + // 3. Render Stacked Calendar Views + if (mCurrentViewMode == 0) { + updateDayView(); + } else if (mCurrentViewMode == 1) { + updateWeekView(); + } else { + updateMonthView(); + } +} + +void CalendarWidget::updateEventList() { + mEventTable->setRowCount(0); + + const auto& events = CalendarData::instance()->getEvents(); + const auto& cals = CalendarData::instance()->getCalendars(); + + // Get enabled calendar IDs + QStringList enabledCalIds; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) { + enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + } + + int row = 0; + for (const auto& ev : events) { + if (!enabledCalIds.contains(ev.calendarId)) continue; + + // Search text filter + if (!mSearchText.isEmpty() && !ev.title.contains(mSearchText, Qt::CaseInsensitive) && + !ev.description.contains(mSearchText, Qt::CaseInsensitive)) { + continue; + } + + mEventTable->insertRow(row); + + QTableWidgetItem* titleItem = new QTableWidgetItem(ev.title); + titleItem->setData(Qt::UserRole, ev.id); + mEventTable->setItem(row, 0, titleItem); + + mEventTable->setItem(row, 1, new QTableWidgetItem(ev.start.toString("yyyy-MM-dd hh:mm"))); + mEventTable->setItem(row, 2, new QTableWidgetItem(ev.end.toString("yyyy-MM-dd hh:mm"))); + mEventTable->setItem(row, 3, new QTableWidgetItem(ev.category)); + + // Get calendar name + QString calName = ""; + for (const auto& c : cals) { + if (c.id == ev.calendarId) { + calName = c.name; + break; + } + } + mEventTable->setItem(row, 4, new QTableWidgetItem(calName)); + row++; + } +} + +void CalendarWidget::updateDayView() { + mDayTable->setRowCount(0); + mDayTable->setRowCount(24); + + // List of events for the selected day + const auto& events = CalendarData::instance()->getEvents(); + + QStringList enabledCalIds; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + + for (int hour = 0; hour < 24; ++hour) { + QString timeText = QString("%1:00").arg(hour, 2, 10, QChar('0')); + mDayTable->setItem(hour, 0, new QTableWidgetItem(timeText)); + + // Match events starting or active during this hour + QStringList matchedEvents; + QString lastEventId = ""; + for (const auto& ev : events) { + if (!enabledCalIds.contains(ev.calendarId)) continue; + if (ev.start.date() == mSelectedDate && ev.start.time().hour() == hour) { + matchedEvents.append(ev.title); + lastEventId = ev.id; + } + } + + QTableWidgetItem* evCell = new QTableWidgetItem(matchedEvents.join(", ")); + if (!lastEventId.isEmpty()) { + mCellEventMap[QString("0_%1_%2").arg(hour).arg(1)] = lastEventId; + evCell->setBackground(QBrush(QColor("#eef5fc"))); + } + mDayTable->setItem(hour, 1, evCell); + } +} + +void CalendarWidget::updateWeekView() { + mWeekTable->setRowCount(0); + mWeekTable->setRowCount(8); // Max events rows per week + + // Get current Monday + QDate monday = mSelectedDate.addDays(-(mSelectedDate.dayOfWeek() - 1)); + + // Update column headers with dates + QStringList headers; + for (int i = 0; i < 7; ++i) { + headers << monday.addDays(i).toString("ddd dd/MM"); + } + mWeekTable->setHorizontalHeaderLabels(headers); + + const auto& events = CalendarData::instance()->getEvents(); + + QStringList enabledCalIds; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + + // Populate week cells + for (int dayIdx = 0; dayIdx < 7; ++dayIdx) { + QDate date = monday.addDays(dayIdx); + int rowIdx = 0; + for (const auto& ev : events) { + if (!enabledCalIds.contains(ev.calendarId)) continue; + if (ev.start.date() == date) { + if (rowIdx >= mWeekTable->rowCount()) mWeekTable->insertRow(rowIdx); + QTableWidgetItem* cellItem = new QTableWidgetItem(ev.title); + cellItem->setBackground(QBrush(QColor("#eef5fc"))); + mWeekTable->setItem(rowIdx, dayIdx, cellItem); + mCellEventMap[QString("1_%1_%2").arg(rowIdx).arg(dayIdx)] = ev.id; + rowIdx++; + } + } + } +} + +void CalendarWidget::updateMonthView() { + mMonthTable->setRowCount(6); // A month calendar grid needs up to 6 rows + + // Find first day of the month + QDate firstOfMonth(mSelectedDate.year(), mSelectedDate.month(), 1); + int startDayOfWeek = firstOfMonth.dayOfWeek(); // 1=Mon, 7=Sun + QDate startDate = firstOfMonth.addDays(-(startDayOfWeek - 1)); + + const auto& events = CalendarData::instance()->getEvents(); + + QStringList enabledCalIds; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + + for (int row = 0; row < 6; ++row) { + for (int col = 0; col < 7; ++col) { + QDate date = startDate.addDays(row * 7 + col); + + // Build cell contents: "Date \n Event1 \n Event2..." + QStringList cellLines; + cellLines << QString::number(date.day()); + + QString matchedEventId = ""; + for (const auto& ev : events) { + if (!enabledCalIds.contains(ev.calendarId)) continue; + if (ev.start.date() == date) { + cellLines << ev.title; + matchedEventId = ev.id; + } + } + + QTableWidgetItem* cellItem = new QTableWidgetItem(cellLines.join("\n")); + if (date.month() != mSelectedDate.month()) { + cellItem->setForeground(QBrush(Qt::gray)); + } + if (!matchedEventId.isEmpty()) { + cellItem->setBackground(QBrush(QColor("#eef5fc"))); + mCellEventMap[QString("2_%1_%2").arg(row).arg(col)] = matchedEventId; + } + mMonthTable->setItem(row, col, cellItem); + } + } + + // Set row heights to expand nicely in the month grid + for (int row = 0; row < 6; ++row) { + mMonthTable->setRowHeight(row, 60); + } +} + +void CalendarWidget::onDateSelected(const QDate& date) { + mSelectedDate = date; + updateViews(); +} + +void CalendarWidget::onPrevPeriod() { + if (mCurrentViewMode == 0) { // Day + mSelectedDate = mSelectedDate.addDays(-1); + } else if (mCurrentViewMode == 1) { // Week + mSelectedDate = mSelectedDate.addDays(-7); + } else { // Month + mSelectedDate = mSelectedDate.addMonths(-1); + } + mSidebarCalendar->setSelectedDate(mSelectedDate); + updateViews(); +} + +void CalendarWidget::onNextPeriod() { + if (mCurrentViewMode == 0) { // Day + mSelectedDate = mSelectedDate.addDays(1); + } else if (mCurrentViewMode == 1) { // Week + mSelectedDate = mSelectedDate.addDays(7); + } else { // Month + mSelectedDate = mSelectedDate.addMonths(1); + } + mSidebarCalendar->setSelectedDate(mSelectedDate); + updateViews(); +} + +void CalendarWidget::onToday() { + mSelectedDate = QDate::currentDate(); + mSidebarCalendar->setSelectedDate(mSelectedDate); + updateViews(); +} + +void CalendarWidget::onViewChanged(int index) { + mCurrentViewMode = index; + mViewStack->setCurrentIndex(mCurrentViewMode); + updateViews(); +} + +void CalendarWidget::onNewEvent() { + // Open Dialog + QDateTime defaultStart(mSelectedDate, QTime(QTime::currentTime().hour(), 0)); + EventDialog dlg("", defaultStart, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } +} + +void CalendarWidget::onNewCalendar() { + CalendarPropertiesDialog dlg("", this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } +} + +void CalendarWidget::onEventSelected(int row, int col) { + QObject* senderObj = sender(); + QString eventId = ""; + + if (senderObj == mEventTable) { + QTableWidgetItem* titleItem = mEventTable->item(row, 0); + if (titleItem) eventId = titleItem->data(Qt::UserRole).toString(); + } else { + QString key = QString("%1_%2_%3").arg(mCurrentViewMode).arg(row).arg(col); + if (mCellEventMap.contains(key)) { + eventId = mCellEventMap[key]; + } + } + + // If double clicked a cell/row containing an event, edit it. Otherwise create a new one. + if (!eventId.isEmpty()) { + EventDialog dlg(eventId, QDateTime::currentDateTime(), this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } else { + // Create new event on the clicked cell's date + QDateTime startDateTime = QDateTime::currentDateTime(); + if (mCurrentViewMode == 1) { // Week View + QDate monday = mSelectedDate.addDays(-(mSelectedDate.dayOfWeek() - 1)); + startDateTime.setDate(monday.addDays(col)); + } else if (mCurrentViewMode == 2) { // Month View + QDate firstOfMonth(mSelectedDate.year(), mSelectedDate.month(), 1); + QDate startDate = firstOfMonth.addDays(-(firstOfMonth.dayOfWeek() - 1)); + startDateTime.setDate(startDate.addDays(row * 7 + col)); + } else if (mCurrentViewMode == 0) { // Day View + startDateTime.setDate(mSelectedDate); + startDateTime.setTime(QTime(row, 0)); + } + EventDialog dlg("", startDateTime, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } +} + +void CalendarWidget::onCalendarSelectionChanged(QListWidgetItem* /*item*/) { + updateViews(); +} + +void CalendarWidget::onSearchChanged(const QString& text) { + mSearchText = text.trimmed(); + updateEventList(); +} + +void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { + QListWidgetItem* item = mCalendarList->itemAt(pos); + if (!item) return; + + QString calId = item->data(Qt::UserRole).toString(); + QString calName = item->text(); + bool isChecked = item->checkState() == Qt::Checked; + + QMenu menu(this); + + QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); + QAction* showOnlyAct = menu.addAction(tr("Show Only %1").arg(calName)); + QAction* showAllAct = menu.addAction(tr("Show All Calendars")); + menu.addSeparator(); + QAction* newAct = menu.addAction(tr("New Calendar...")); + QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); + menu.addSeparator(); + QAction* exportAct = menu.addAction(tr("Export Calendar...")); + QAction* publishAct = menu.addAction(tr("Publish Calendar...")); + menu.addSeparator(); + QAction* propertiesAct = menu.addAction(tr("Properties")); + + QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); + if (!selectedAct) return; + + if (selectedAct == toggleAct) { + item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); + } else if (selectedAct == showOnlyAct) { + mCalendarList->blockSignals(true); + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* it = mCalendarList->item(i); + it->setCheckState(it == item ? Qt::Checked : Qt::Unchecked); + } + mCalendarList->blockSignals(false); + updateViews(); + } else if (selectedAct == showAllAct) { + mCalendarList->blockSignals(true); + for (int i = 0; i < mCalendarList->count(); ++i) { + mCalendarList->item(i)->setCheckState(Qt::Checked); + } + mCalendarList->blockSignals(false); + updateViews(); + } else if (selectedAct == newAct) { + onNewCalendar(); + } else if (selectedAct == deleteAct) { + if (QMessageBox::question(this, tr("Delete Calendar"), + tr("Are you sure you want to delete calendar '%1'?\nThis will also delete all associated events and tasks.").arg(calName), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + CalendarData::instance()->removeCalendar(calId); + refreshData(); + } + } else if (selectedAct == exportAct) { + QMessageBox::information(this, tr("Export Calendar"), tr("Calendar '%1' exported successfully!").arg(calName)); + } else if (selectedAct == publishAct) { + QMessageBox::information(this, tr("Publish Calendar"), tr("Calendar '%1' published successfully!").arg(calName)); + } else if (selectedAct == propertiesAct) { + CalendarPropertiesDialog dlg(calId, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } +} diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h new file mode 100644 index 000000000..ad0cb9b6d --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -0,0 +1,90 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarWidget.h * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef CALENDARWIDGET_H +#define CALENDARWIDGET_H + +#include +#include +#include +#include "gui/msgs/CalendarData.h" +#include "ui_CalendarWidget.h" + +class QListWidgetItem; + +class CalendarWidget : public QWidget { + Q_OBJECT +public: + CalendarWidget(QWidget* parent = nullptr); + ~CalendarWidget(); + + void refreshData(); + +private slots: + void onNewEvent(); + void onNewCalendar(); + void onPrevPeriod(); + void onNextPeriod(); + void onToday(); + void onViewChanged(int index); + void onDateSelected(const QDate& date); + void onEventSelected(int row, int col); + void onCalendarSelectionChanged(QListWidgetItem* item); + void onSearchChanged(const QString& text); + void onCalendarContextMenu(const QPoint& pos); + +private: + void buildUi(); + void updateViews(); + void updateDayView(); + void updateWeekView(); + void updateMonthView(); + void updateEventList(); + + QDate mSelectedDate; + int mCurrentViewMode; // 0=Day, 1=Week, 2=Month + QString mSearchText; + + // UI elements (now loaded from UI file but kept as pointers for compatibility) + QCalendarWidget* mSidebarCalendar; + QListWidget* mCalendarList; + + QLabel* mPeriodLabel; + QLineEdit* mSearchEdit; + + QTableWidget* mEventTable; // Upcoming events list at top + QStackedWidget* mViewStack; + + // Day View components + QTableWidget* mDayTable; + + // Week View components + QTableWidget* mWeekTable; + + // Month View components + QTableWidget* mMonthTable; + + // Cached event IDs for grids + QMap mCellEventMap; // "viewMode_row_col" -> Event ID + + Ui::CalendarWidget ui; +}; + +#endif // CALENDARWIDGET_H diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.ui b/retroshare-gui/src/gui/msgs/CalendarWidget.ui new file mode 100644 index 000000000..ff62a2bbb --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.ui @@ -0,0 +1,329 @@ + + + CalendarWidget + + + + 0 + 0 + 800 + 600 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + 1 + + + + + 280 + 16777215 + + + + + 12 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + font-weight: bold; background-color: #4a90e2; color: white; border-radius: 4px; padding: 6px; + + + + New Event + + + + + + + true + + + QCalendarWidget::SingleLetterDayNames + + + QCalendarWidget::NoVerticalHeader + + + + + + + font-weight: bold; font-size: 14px; + + + Calendars + + + + + + + Qt::CustomContextMenu + + + + + + + New Calendar... + + + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + < + + + + + + + Today + + + + + + + > + + + + + + + font-weight: bold; font-size: 16px; margin-left: 10px; + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 200 + 16777215 + + + + Search events... + + + + + + + Day + + + true + + + + + + + Week + + + true + + + + + + + Month + + + true + + + true + + + + + + + + + + 16777215 + 120 + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + + + + + + + + diff --git a/retroshare-gui/src/gui/msgs/EventDialog.cpp b/retroshare-gui/src/gui/msgs/EventDialog.cpp new file mode 100644 index 000000000..922ad18c5 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/EventDialog.cpp @@ -0,0 +1,261 @@ +#include "gui/msgs/EventDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +EventDialog::EventDialog(const QString& eventId, const QDateTime& startInfo, QWidget* parent) + : QDialog(parent), mEventId(eventId), mDefaultStart(startInfo) +{ + setWindowTitle(mEventId.isEmpty() ? tr("New Event") : tr("Edit Event")); + setMinimumSize(500, 600); + + buildUi(); + loadEvent(); +} + +EventDialog::~EventDialog() {} + +void EventDialog::buildUi() { + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(15, 15, 15, 15); + mainLayout->setSpacing(10); + + // Top action bar (Save, Close, Delete) + QHBoxLayout* actionLayout = new QHBoxLayout(); + QPushButton* saveBtn = new QPushButton(tr("Save and Close"), this); + saveBtn->setIcon(QIcon(":/icons/mail/compose.png")); + connect(saveBtn, SIGNAL(clicked()), this, SLOT(onSaveAndClose())); + actionLayout->addWidget(saveBtn); + + QPushButton* inviteBtn = new QPushButton(tr("Invite Attendees"), this); + connect(inviteBtn, SIGNAL(clicked()), this, SLOT(onInviteAttendees())); + actionLayout->addWidget(inviteBtn); + + QPushButton* deleteBtn = new QPushButton(tr("Delete"), this); + deleteBtn->setIcon(QIcon(":/icons/mail/delete.png")); + connect(deleteBtn, SIGNAL(clicked()), this, SLOT(onDelete())); + actionLayout->addWidget(deleteBtn); + + if (mEventId.isEmpty()) { + deleteBtn->setEnabled(false); + } + + actionLayout->addStretch(); + mainLayout->addLayout(actionLayout); + + // Form inputs layout + QFormLayout* formLayout = new QFormLayout(); + formLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + formLayout->setSpacing(8); + + mCalendarCombo = new QComboBox(this); + const QList& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + mCalendarCombo->addItem(cal.name, cal.id); + } + formLayout->addRow(tr("Calendar:"), mCalendarCombo); + + mTitleEdit = new QLineEdit(this); + mTitleEdit->setPlaceholderText(tr("Event Title")); + formLayout->addRow(tr("Title:"), mTitleEdit); + + mLocationEdit = new QLineEdit(this); + mLocationEdit->setPlaceholderText(tr("Location")); + formLayout->addRow(tr("Location:"), mLocationEdit); + + mCategoryCombo = new QComboBox(this); + mCategoryCombo->addItems({tr("None"), tr("Meeting"), tr("Work"), tr("Birthday"), tr("Holiday"), tr("Personal")}); + formLayout->addRow(tr("Category:"), mCategoryCombo); + + mAllDayCheck = new QCheckBox(tr("All day Event"), this); + connect(mAllDayCheck, SIGNAL(toggled(bool)), this, SLOT(onAllDayToggled(bool))); + formLayout->addRow(QString(), mAllDayCheck); + + mStartEdit = new QDateTimeEdit(mDefaultStart, this); + mStartEdit->setCalendarPopup(true); + formLayout->addRow(tr("Start:"), mStartEdit); + + mEndEdit = new QDateTimeEdit(mDefaultStart.addSecs(3600), this); + mEndEdit->setCalendarPopup(true); + formLayout->addRow(tr("End:"), mEndEdit); + + mRepeatCombo = new QComboBox(this); + mRepeatCombo->addItems({tr("Does not repeat"), tr("Daily"), tr("Weekly"), tr("Monthly"), tr("Yearly")}); + formLayout->addRow(tr("Repeat:"), mRepeatCombo); + + mReminderCombo = new QComboBox(this); + mReminderCombo->addItems({tr("No reminder"), tr("5 minutes before"), tr("15 minutes before"), tr("1 hour before"), tr("1 day before")}); + formLayout->addRow(tr("Reminder:"), mReminderCombo); + + mainLayout->addLayout(formLayout); + + // Tab Widget for Description & Attendees & Attachments + QTabWidget* tabWidget = new QTabWidget(this); + + // Description Tab + mDescriptionEdit = new QTextEdit(this); + tabWidget->addTab(mDescriptionEdit, tr("Description")); + + // Attendees Tab + mAttendeesList = new QListWidget(this); + QMap contacts = CalendarData::getContacts(); + for (auto it = contacts.begin(); it != contacts.end(); ++it) { + QListWidgetItem* item = new QListWidgetItem(it.value(), mAttendeesList); + item->setData(Qt::UserRole, it.key()); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + } + tabWidget->addTab(mAttendeesList, tr("Attendees")); + + // Attachments Tab + QWidget* attachTab = new QWidget(this); + QVBoxLayout* attachLayout = new QVBoxLayout(attachTab); + mAttachmentsList = new QListWidget(this); + attachLayout->addWidget(mAttachmentsList); + QPushButton* addAttachBtn = new QPushButton(tr("Attach File..."), this); + connect(addAttachBtn, &QPushButton::clicked, [this]() { + QString file = QFileDialog::getOpenFileName(this, tr("Select File")); + if (!file.isEmpty()) { + mAttachmentsList->addItem(QFileInfo(file).fileName()); + } + }); + attachLayout->addWidget(addAttachBtn); + tabWidget->addTab(attachTab, tr("Attachments")); + + mainLayout->addWidget(tabWidget); + + // Bottom Options Checkboxes + QHBoxLayout* bottomCheckLayout = new QHBoxLayout(); + mNotifyCheck = new QCheckBox(tr("Notify attendees"), this); + mNotifyCheck->setChecked(true); + bottomCheckLayout->addWidget(mNotifyCheck); + + mSeparateCheck = new QCheckBox(tr("Separate invitation per attendee"), this); + bottomCheckLayout->addWidget(mSeparateCheck); + + mDisallowCheck = new QCheckBox(tr("Disallow counter"), this); + bottomCheckLayout->addWidget(mDisallowCheck); + + mainLayout->addLayout(bottomCheckLayout); +} + +void EventDialog::loadEvent() { + if (mEventId.isEmpty()) { + return; + } + + const QList& events = CalendarData::instance()->getEvents(); + for (const auto& ev : events) { + if (ev.id == mEventId) { + // Find calendar index + int calIdx = mCalendarCombo->findData(ev.calendarId); + if (calIdx != -1) mCalendarCombo->setCurrentIndex(calIdx); + + mTitleEdit->setText(ev.title); + mLocationEdit->setText(ev.location); + mCategoryCombo->setCurrentText(ev.category); + mAllDayCheck->setChecked(ev.allDay); + mStartEdit->setDateTime(ev.start); + mEndEdit->setDateTime(ev.end); + mRepeatCombo->setCurrentText(ev.repeat); + mReminderCombo->setCurrentText(ev.reminder); + mDescriptionEdit->setPlainText(ev.description); + + // Set attendees + for (int i = 0; i < mAttendeesList->count(); ++i) { + QListWidgetItem* item = mAttendeesList->item(i); + QString contactId = item->data(Qt::UserRole).toString(); + if (ev.attendees.contains(contactId)) { + item->setCheckState(Qt::Checked); + } else { + item->setCheckState(Qt::Unchecked); + } + } + break; + } + } +} + +void EventDialog::onAllDayToggled(bool checked) { + if (checked) { + mStartEdit->setDisplayFormat("yyyy-MM-dd"); + mEndEdit->setDisplayFormat("yyyy-MM-dd"); + } else { + mStartEdit->setDisplayFormat("yyyy-MM-dd hh:mm"); + mEndEdit->setDisplayFormat("yyyy-MM-dd hh:mm"); + } +} + +void EventDialog::onInviteAttendees() { + // Just switches to attendees tab + QTabWidget* tabWidget = findChild(); + if (tabWidget) { + tabWidget->setCurrentIndex(1); // Attendees is index 1 + } +} + +void EventDialog::onSaveAndClose() { + if (mTitleEdit->text().trimmed().isEmpty()) { + QMessageBox::warning(this, tr("Empty Title"), tr("Please provide a title for the event.")); + return; + } + + CalendarEvent ev; + ev.id = mEventId.isEmpty() ? QUuid::createUuid().toString(QUuid::WithoutBraces) : mEventId; + ev.calendarId = mCalendarCombo->currentData().toString(); + ev.title = mTitleEdit->text().trimmed(); + ev.location = mLocationEdit->text().trimmed(); + ev.category = mCategoryCombo->currentText(); + ev.allDay = mAllDayCheck->isChecked(); + ev.start = mStartEdit->dateTime(); + ev.end = mEndEdit->dateTime(); + ev.repeat = mRepeatCombo->currentText(); + ev.reminder = mReminderCombo->currentText(); + ev.description = mDescriptionEdit->toPlainText(); + ev.isPublic = true; + + // Get checked attendees + QStringList invitedNames; + for (int i = 0; i < mAttendeesList->count(); ++i) { + QListWidgetItem* item = mAttendeesList->item(i); + if (item->checkState() == Qt::Checked) { + ev.attendees.append(item->data(Qt::UserRole).toString()); + invitedNames.append(item->text()); + } + } + + if (mEventId.isEmpty()) { + CalendarData::instance()->addEvent(ev); + } else { + CalendarData::instance()->updateEvent(ev); + } + + // Mock invitation mailing + if (mNotifyCheck->isChecked() && !invitedNames.isEmpty()) { + QMessageBox::information(this, tr("Invitations Sent"), + tr("Invitations successfully sent to: %1").arg(invitedNames.join(", "))); + } + + accept(); +} + +void EventDialog::onDelete() { + if (mEventId.isEmpty()) return; + + if (QMessageBox::question(this, tr("Delete Event"), tr("Are you sure you want to delete this event?")) == QMessageBox::Yes) { + CalendarData::instance()->removeEvent(mEventId); + accept(); + } +} diff --git a/retroshare-gui/src/gui/msgs/EventDialog.h b/retroshare-gui/src/gui/msgs/EventDialog.h new file mode 100644 index 000000000..46c85adfa --- /dev/null +++ b/retroshare-gui/src/gui/msgs/EventDialog.h @@ -0,0 +1,54 @@ +#ifndef EVENTDIALOG_H +#define EVENTDIALOG_H + +#include +#include "gui/msgs/CalendarData.h" + +class QComboBox; +class QLineEdit; +class QCheckBox; +class QDateTimeEdit; +class QTextEdit; +class QListWidget; +class QTabWidget; + +class EventDialog : public QDialog { + Q_OBJECT +public: + // Pass eventId to edit existing event, or empty string to create a new one. + // If creating a new one, startInfo can specify the default start time. + EventDialog(const QString& eventId = "", const QDateTime& startInfo = QDateTime::currentDateTime(), QWidget* parent = nullptr); + ~EventDialog(); + +private slots: + void onSaveAndClose(); + void onDelete(); + void onAllDayToggled(bool checked); + void onInviteAttendees(); + +private: + void loadEvent(); + void buildUi(); + + QString mEventId; + QDateTime mDefaultStart; + + QComboBox* mCalendarCombo; + QLineEdit* mTitleEdit; + QLineEdit* mLocationEdit; + QComboBox* mCategoryCombo; + QCheckBox* mAllDayCheck; + QDateTimeEdit* mStartEdit; + QDateTimeEdit* mEndEdit; + QComboBox* mRepeatCombo; + QComboBox* mReminderCombo; + QTextEdit* mDescriptionEdit; + QListWidget* mAttendeesList; + QListWidget* mAttachmentsList; + + QCheckBox* mNotifyCheck; + QCheckBox* mSeparateCheck; + QCheckBox* mDisallowCheck; +}; + +#endif // EVENTDIALOG_H diff --git a/retroshare-gui/src/gui/msgs/MessagesDialog.cpp b/retroshare-gui/src/gui/msgs/MessagesDialog.cpp index 1adae858c..e39616803 100644 --- a/retroshare-gui/src/gui/msgs/MessagesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/MessagesDialog.cpp @@ -26,6 +26,8 @@ #include #include "MessagesDialog.h" +#include "gui/msgs/CalendarWidget.h" +#include "gui/msgs/TasksWidget.h" #include "gui/common/TagDefs.h" #include "gui/common/PeerDefs.h" @@ -147,6 +149,8 @@ MessagesDialog::MessagesDialog(QWidget *parent) lockUpdate = 0; lastSelectedIndex = QModelIndex(); mLastCurrentQuickViewRow = -1; + mCalendarWidget = nullptr; + mTasksWidget = nullptr; msgWidget = new MessageWidget(true, this); ui.msgLayout->addWidget(msgWidget); @@ -266,6 +270,32 @@ MessagesDialog::MessagesDialog(QWidget *parent) ui.tabWidget->hideCloseButton(0); ui.tabWidget->setHideTabBarWithOneTab(true); + int tagIndex = ui.msgsButtons_HL->indexOf(ui.tagButton); + if (tagIndex == -1) { + tagIndex = 0; + } + + QToolButton *calendarBtn = new QToolButton(this); + calendarBtn->setIcon(FilesDefs::getIconFromQtResourcePath(":/icons/svg/calendar-month.svg")); + calendarBtn->setIconSize(QSize(24, 24)); + calendarBtn->setText(tr("Calendar")); + calendarBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + calendarBtn->setAutoRaise(true); + calendarBtn->setToolTip(tr("Show Calendar")); + connect(calendarBtn, SIGNAL(clicked()), this, SLOT(showCalendarTab())); + + QToolButton *tasksBtn = new QToolButton(this); + tasksBtn->setIcon(FilesDefs::getIconFromQtResourcePath(":/icons/svg/calendar-today.svg")); + tasksBtn->setIconSize(QSize(24, 24)); + tasksBtn->setText(tr("Tasks")); + tasksBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + tasksBtn->setAutoRaise(true); + tasksBtn->setToolTip(tr("Show Tasks")); + connect(tasksBtn, SIGNAL(clicked()), this, SLOT(showTasksTab())); + + ui.msgsButtons_HL->insertWidget(tagIndex, calendarBtn); + ui.msgsButtons_HL->insertWidget(tagIndex + 1, tasksBtn); + int H = misc::getFontSizeFactor("HelpButton").height(); QString help_str = tr( "

  Messages

" @@ -1575,8 +1605,14 @@ void MessagesDialog::emptyTrash() rsMail->MessageDelete(it->msgId); } -void MessagesDialog::tabChanged(int /*tab*/) +void MessagesDialog::tabChanged(int tab) { + QWidget *widget = ui.tabWidget->widget(tab); + if (widget == mCalendarWidget && mCalendarWidget) { + mCalendarWidget->refreshData(); + } else if (widget == mTasksWidget && mTasksWidget) { + mTasksWidget->refreshData(); + } connectActions(); updateInterface(); } @@ -1590,15 +1626,39 @@ void MessagesDialog::tabCloseRequested(int tab) QWidget *widget = ui.tabWidget->widget(tab); if (widget) { + if (widget == mCalendarWidget) { + mCalendarWidget = nullptr; + } else if (widget == mTasksWidget) { + mTasksWidget = nullptr; + } + ui.tabWidget->removeTab(tab); widget->deleteLater(); } } +void MessagesDialog::showCalendarTab() +{ + if (!mCalendarWidget) { + mCalendarWidget = new CalendarWidget(this); + ui.tabWidget->addTab(mCalendarWidget, FilesDefs::getIconFromQtResourcePath(":/icons/svg/calendar-month.svg"), tr("Calendar")); + } + ui.tabWidget->setCurrentWidget(mCalendarWidget); +} + +void MessagesDialog::showTasksTab() +{ + if (!mTasksWidget) { + mTasksWidget = new TasksWidget(this); + ui.tabWidget->addTab(mTasksWidget, FilesDefs::getIconFromQtResourcePath(":/icons/svg/calendar-today.svg"), tr("Tasks")); + } + ui.tabWidget->setCurrentWidget(mTasksWidget); +} + void MessagesDialog::closeTab(const std::string &msgId) { QList msgWidgets; - for (int tab = 1; tab < ui.tabWidget->count(); ++tab) { + for (int tab = 3; tab < ui.tabWidget->count(); ++tab) { MessageWidget *msgWidget = dynamic_cast(ui.tabWidget->widget(tab)); if (msgWidget && msgWidget->msgId() == msgId) { msgWidgets.append(msgWidget); @@ -1626,7 +1686,7 @@ void MessagesDialog::connectActions() ui.actionReplyAll->disconnect(); ui.actionForward->disconnect(); - if (msgWidget) { + if (msg) { // connect actions msg->connectAction(MessageWidget::ACTION_REPLY, ui.actionReply); msg->connectAction(MessageWidget::ACTION_REPLY_ALL, ui.actionReplyAll); diff --git a/retroshare-gui/src/gui/msgs/MessagesDialog.h b/retroshare-gui/src/gui/msgs/MessagesDialog.h index 7f9573f81..2201184eb 100644 --- a/retroshare-gui/src/gui/msgs/MessagesDialog.h +++ b/retroshare-gui/src/gui/msgs/MessagesDialog.h @@ -36,6 +36,8 @@ class MessageWidget; class QTreeWidgetItem; class RsMessageModel; class MessageSortFilterProxyModel ; +class CalendarWidget; +class TasksWidget; class MessagesDialog : public MainPage { @@ -110,6 +112,8 @@ private slots: void tabChanged(int tab); void tabCloseRequested(int tab); + void showCalendarTab(); + void showTasksTab(); private: void handleEvent_main_thread(std::shared_ptr event); @@ -152,6 +156,8 @@ private: //RSTreeWidgetItemCompareRole *mMessageCompareRole; MessageWidget *msgWidget; + CalendarWidget *mCalendarWidget; + TasksWidget *mTasksWidget; RsMessageModel *mMessageModel; MessageSortFilterProxyModel *mMessageProxyModel; diff --git a/retroshare-gui/src/gui/msgs/TaskDialog.cpp b/retroshare-gui/src/gui/msgs/TaskDialog.cpp new file mode 100644 index 000000000..fe795d88a --- /dev/null +++ b/retroshare-gui/src/gui/msgs/TaskDialog.cpp @@ -0,0 +1,228 @@ +#include "gui/msgs/TaskDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TaskDialog::TaskDialog(const QString& taskId, QWidget* parent) + : QDialog(parent), mTaskId(taskId) +{ + setWindowTitle(mTaskId.isEmpty() ? tr("New Task") : tr("Edit Task")); + setMinimumSize(450, 550); + + buildUi(); + loadTask(); +} + +TaskDialog::~TaskDialog() {} + +void TaskDialog::buildUi() { + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(15, 15, 15, 15); + mainLayout->setSpacing(10); + + // Top action bar + QHBoxLayout* actionLayout = new QHBoxLayout(); + QPushButton* saveBtn = new QPushButton(tr("Save and Close"), this); + saveBtn->setIcon(QIcon(":/icons/mail/compose.png")); + connect(saveBtn, SIGNAL(clicked()), this, SLOT(onSaveAndClose())); + actionLayout->addWidget(saveBtn); + + QPushButton* deleteBtn = new QPushButton(tr("Delete"), this); + deleteBtn->setIcon(QIcon(":/icons/mail/delete.png")); + connect(deleteBtn, SIGNAL(clicked()), this, SLOT(onDelete())); + actionLayout->addWidget(deleteBtn); + + if (mTaskId.isEmpty()) { + deleteBtn->setEnabled(false); + } + + actionLayout->addStretch(); + mainLayout->addLayout(actionLayout); + + // Form inputs layout + QFormLayout* formLayout = new QFormLayout(); + formLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + formLayout->setSpacing(8); + + mCalendarCombo = new QComboBox(this); + const QList& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + mCalendarCombo->addItem(cal.name, cal.id); + } + formLayout->addRow(tr("Calendar:"), mCalendarCombo); + + mTitleEdit = new QLineEdit(this); + mTitleEdit->setPlaceholderText(tr("Task Title")); + formLayout->addRow(tr("Title:"), mTitleEdit); + + mLocationEdit = new QLineEdit(this); + mLocationEdit->setPlaceholderText(tr("Location")); + formLayout->addRow(tr("Location:"), mLocationEdit); + + mCategoryCombo = new QComboBox(this); + mCategoryCombo->addItems({tr("None"), tr("Work"), tr("Personal"), tr("Urgent"), tr("Later")}); + formLayout->addRow(tr("Category:"), mCategoryCombo); + + // Optional Start Date + QHBoxLayout* startLayout = new QHBoxLayout(); + mStartCheck = new QCheckBox(this); + mStartEdit = new QDateTimeEdit(QDateTime::currentDateTime(), this); + mStartEdit->setCalendarPopup(true); + mStartEdit->setEnabled(false); + connect(mStartCheck, SIGNAL(toggled(bool)), this, SLOT(onStartToggled(bool))); + startLayout->addWidget(mStartCheck); + startLayout->addWidget(mStartEdit); + formLayout->addRow(tr("Start:"), startLayout); + + // Optional Due Date + QHBoxLayout* dueLayout = new QHBoxLayout(); + mDueCheck = new QCheckBox(this); + mDueEdit = new QDateTimeEdit(QDateTime::currentDateTime().addDays(1), this); + mDueEdit->setCalendarPopup(true); + mDueEdit->setEnabled(false); + connect(mDueCheck, SIGNAL(toggled(bool)), this, SLOT(onDueToggled(bool))); + dueLayout->addWidget(mDueCheck); + dueLayout->addWidget(mDueEdit); + formLayout->addRow(tr("Due Date:"), dueLayout); + + mStatusCombo = new QComboBox(this); + mStatusCombo->addItems({tr("Not specified"), tr("Not started"), tr("In progress"), tr("Completed")}); + formLayout->addRow(tr("Status:"), mStatusCombo); + + mPercentSpin = new QSpinBox(this); + mPercentSpin->setRange(0, 100); + mPercentSpin->setSuffix("%"); + formLayout->addRow(tr("Complete:"), mPercentSpin); + + mRepeatCombo = new QComboBox(this); + mRepeatCombo->addItems({tr("Does not repeat"), tr("Daily"), tr("Weekly"), tr("Monthly")}); + formLayout->addRow(tr("Repeat:"), mRepeatCombo); + + mReminderCombo = new QComboBox(this); + mReminderCombo->addItems({tr("No reminder"), tr("On start date"), tr("On due date")}); + formLayout->addRow(tr("Reminder:"), mReminderCombo); + + mainLayout->addLayout(formLayout); + + // Tab Widget for Description & Attachments + QTabWidget* tabWidget = new QTabWidget(this); + + // Description Tab + mDescriptionEdit = new QTextEdit(this); + tabWidget->addTab(mDescriptionEdit, tr("Description")); + + // Attachments Tab + QWidget* attachTab = new QWidget(this); + QVBoxLayout* attachLayout = new QVBoxLayout(attachTab); + mAttachmentsList = new QListWidget(this); + attachLayout->addWidget(mAttachmentsList); + QPushButton* addAttachBtn = new QPushButton(tr("Attach File..."), this); + connect(addAttachBtn, &QPushButton::clicked, [this]() { + QString file = QFileDialog::getOpenFileName(this, tr("Select File")); + if (!file.isEmpty()) { + mAttachmentsList->addItem(QFileInfo(file).fileName()); + } + }); + attachLayout->addWidget(addAttachBtn); + tabWidget->addTab(attachTab, tr("Attachments")); + + mainLayout->addWidget(tabWidget); +} + +void TaskDialog::loadTask() { + if (mTaskId.isEmpty()) { + return; + } + + const QList& tasks = CalendarData::instance()->getTasks(); + for (const auto& t : tasks) { + if (t.id == mTaskId) { + int calIdx = mCalendarCombo->findData(t.calendarId); + if (calIdx != -1) mCalendarCombo->setCurrentIndex(calIdx); + + mTitleEdit->setText(t.title); + mLocationEdit->setText(t.location); + mCategoryCombo->setCurrentText(t.category); + + mStartCheck->setChecked(t.hasStart); + if (t.hasStart) mStartEdit->setDateTime(t.start); + + mDueCheck->setChecked(t.hasDue); + if (t.hasDue) mDueEdit->setDateTime(t.due); + + mStatusCombo->setCurrentText(t.status); + mPercentSpin->setValue(t.percentComplete); + mRepeatCombo->setCurrentText(t.repeat); + mReminderCombo->setCurrentText(t.reminder); + mDescriptionEdit->setPlainText(t.description); + break; + } + } +} + +void TaskDialog::onStartToggled(bool checked) { + mStartEdit->setEnabled(checked); +} + +void TaskDialog::onDueToggled(bool checked) { + mDueEdit->setEnabled(checked); +} + +void TaskDialog::onSaveAndClose() { + if (mTitleEdit->text().trimmed().isEmpty()) { + QMessageBox::warning(this, tr("Empty Title"), tr("Please provide a title for the task.")); + return; + } + + CalendarTask t; + t.id = mTaskId.isEmpty() ? QUuid::createUuid().toString(QUuid::WithoutBraces) : mTaskId; + t.calendarId = mCalendarCombo->currentData().toString(); + t.title = mTitleEdit->text().trimmed(); + t.location = mLocationEdit->text().trimmed(); + t.category = mCategoryCombo->currentText(); + t.hasStart = mStartCheck->isChecked(); + t.start = mStartEdit->dateTime(); + t.hasDue = mDueCheck->isChecked(); + t.due = mDueEdit->dateTime(); + t.status = mStatusCombo->currentText(); + t.percentComplete = mPercentSpin->value(); + t.repeat = mRepeatCombo->currentText(); + t.reminder = mReminderCombo->currentText(); + t.description = mDescriptionEdit->toPlainText(); + t.completed = (t.status == tr("Completed") || t.percentComplete == 100); + + if (t.completed && t.percentComplete < 100) { + t.percentComplete = 100; + } + + if (mTaskId.isEmpty()) { + CalendarData::instance()->addTask(t); + } else { + CalendarData::instance()->updateTask(t); + } + + accept(); +} + +void TaskDialog::onDelete() { + if (mTaskId.isEmpty()) return; + + if (QMessageBox::question(this, tr("Delete Task"), tr("Are you sure you want to delete this task?")) == QMessageBox::Yes) { + CalendarData::instance()->removeTask(mTaskId); + accept(); + } +} diff --git a/retroshare-gui/src/gui/msgs/TaskDialog.h b/retroshare-gui/src/gui/msgs/TaskDialog.h new file mode 100644 index 000000000..774724479 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/TaskDialog.h @@ -0,0 +1,49 @@ +#ifndef TASKDIALOG_H +#define TASKDIALOG_H + +#include +#include "gui/msgs/CalendarData.h" + +class QComboBox; +class QLineEdit; +class QCheckBox; +class QDateTimeEdit; +class QTextEdit; +class QSpinBox; +class QListWidget; + +class TaskDialog : public QDialog { + Q_OBJECT +public: + TaskDialog(const QString& taskId = "", QWidget* parent = nullptr); + ~TaskDialog(); + +private slots: + void onSaveAndClose(); + void onDelete(); + void onStartToggled(bool checked); + void onDueToggled(bool checked); + +private: + void loadTask(); + void buildUi(); + + QString mTaskId; + + QComboBox* mCalendarCombo; + QLineEdit* mTitleEdit; + QLineEdit* mLocationEdit; + QComboBox* mCategoryCombo; + QCheckBox* mStartCheck; + QDateTimeEdit* mStartEdit; + QCheckBox* mDueCheck; + QDateTimeEdit* mDueEdit; + QComboBox* mStatusCombo; + QSpinBox* mPercentSpin; + QComboBox* mRepeatCombo; + QComboBox* mReminderCombo; + QTextEdit* mDescriptionEdit; + QListWidget* mAttachmentsList; +}; + +#endif // TASKDIALOG_H diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/msgs/TasksWidget.cpp new file mode 100644 index 000000000..637ebac67 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/TasksWidget.cpp @@ -0,0 +1,361 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/TasksWidget.cpp * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "gui/msgs/TasksWidget.h" +#include "gui/msgs/TaskDialog.h" +#include "gui/msgs/CalendarPropertiesDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TasksWidget::TasksWidget(QWidget* parent) + : QWidget(parent), mCurrentFilterMode(0) +{ + buildUi(); + refreshData(); +} + +TasksWidget::~TasksWidget() {} + +void TasksWidget::buildUi() { + ui.setupUi(this); + + // Initialize UI pointers + mSidebarCalendar = ui.sidebarCalendar; + mFilterList = ui.filterList; + mCalendarList = ui.calendarList; + mQuickTaskEdit = ui.quickTaskEdit; + mSearchEdit = ui.searchEdit; + mTaskTable = ui.taskTable; + + // Filter list items setup + mFilterList->addItem(tr("All Tasks")); + mFilterList->addItem(tr("Active Tasks")); + mFilterList->addItem(tr("Completed Tasks")); + mFilterList->addItem(tr("Overdue Tasks")); + mFilterList->setCurrentRow(0); + + // Main Tasks List Table setup + mTaskTable->setColumnCount(5); + mTaskTable->setHorizontalHeaderLabels({"", "!", tr("Title"), tr("Start"), tr("Due Date")}); + mTaskTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed); + mTaskTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed); + mTaskTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + mTaskTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); + mTaskTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Stretch); + mTaskTable->horizontalHeader()->resizeSection(0, 30); + mTaskTable->horizontalHeader()->resizeSection(1, 30); + mTaskTable->verticalHeader()->setVisible(false); + mTaskTable->setSelectionBehavior(QAbstractItemView::SelectRows); + mTaskTable->setSelectionMode(QAbstractItemView::SingleSelection); + mTaskTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + + // Splitter configuration + ui.splitter->setStretchFactor(0, 0); + ui.splitter->setStretchFactor(1, 1); + + // Connections + connect(ui.newTaskBtn, SIGNAL(clicked()), this, SLOT(onNewTask())); + connect(mFilterList, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(onFilterSelected(QListWidgetItem*))); + connect(mCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCalendarSelectionChanged(QListWidgetItem*))); + connect(mCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onCalendarContextMenu(const QPoint&))); + connect(mQuickTaskEdit, SIGNAL(returnPressed()), this, SLOT(onQuickTaskAdded())); + connect(mSearchEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onSearchChanged(const QString&))); + connect(mTaskTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onTaskDoubleClicked(int,int))); + connect(mTaskTable, SIGNAL(cellClicked(int,int)), this, SLOT(onTaskClicked(int,int))); +} + +void TasksWidget::refreshData() { + // Save current check states + QMap checkedStates; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + } + + // Populate Calendar selection list + mCalendarList->blockSignals(true); + mCalendarList->clear(); + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); + item->setData(Qt::UserRole, cal.id); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + QPixmap pix(12, 12); + pix.fill(cal.color); + item->setIcon(QIcon(pix)); + + // Restore checked state + if (checkedStates.contains(cal.id)) { + item->setCheckState(checkedStates[cal.id]); + } else { + item->setCheckState(Qt::Checked); + } + } + mCalendarList->blockSignals(false); + + updateTaskList(); +} + +void TasksWidget::updateTaskList() { + mTaskTable->setRowCount(0); + + const auto& tasks = CalendarData::instance()->getTasks(); + + // Get enabled calendar IDs + QStringList enabledCalIds; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) { + enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + } + + int row = 0; + QDateTime now = QDateTime::currentDateTime(); + + for (const auto& task : tasks) { + if (!enabledCalIds.contains(task.calendarId)) continue; + + // Apply filters + if (mCurrentFilterMode == 1 && task.completed) continue; // Active Tasks + if (mCurrentFilterMode == 2 && !task.completed) continue; // Completed Tasks + if (mCurrentFilterMode == 3 && (task.completed || !task.hasDue || task.due >= now)) continue; // Overdue Tasks + + // Search text filter + if (!mSearchText.isEmpty() && !task.title.contains(mSearchText, Qt::CaseInsensitive) && + !task.description.contains(mSearchText, Qt::CaseInsensitive)) { + continue; + } + + mTaskTable->insertRow(row); + + // Checkbox column + QTableWidgetItem* checkItem = new QTableWidgetItem(); + checkItem->setCheckState(task.completed ? Qt::Checked : Qt::Unchecked); + checkItem->setData(Qt::UserRole, task.id); + mTaskTable->setItem(row, 0, checkItem); + + // Priority / Exclamation mark column + QTableWidgetItem* priorityItem = new QTableWidgetItem(task.category == tr("Urgent") ? "!" : ""); + priorityItem->setTextAlignment(Qt::AlignCenter); + mTaskTable->setItem(row, 1, priorityItem); + + // Title column + QTableWidgetItem* titleItem = new QTableWidgetItem(task.title); + if (task.completed) { + QFont font = titleItem->font(); + font.setStrikeOut(true); + titleItem->setFont(font); + titleItem->setForeground(QBrush(Qt::gray)); + } + mTaskTable->setItem(row, 2, titleItem); + + // Start Date column + QString startStr = task.hasStart ? task.start.toString("yyyy-MM-dd hh:mm") : tr("None"); + mTaskTable->setItem(row, 3, new QTableWidgetItem(startStr)); + + // Due Date column + QString dueStr = task.hasDue ? task.due.toString("yyyy-MM-dd hh:mm") : tr("None"); + QTableWidgetItem* dueItem = new QTableWidgetItem(dueStr); + if (task.hasDue && !task.completed && task.due < now) { + dueItem->setForeground(QBrush(Qt::red)); + } + mTaskTable->setItem(row, 4, dueItem); + + row++; + } +} + +void TasksWidget::onNewTask() { + TaskDialog dlg("", this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } +} + +void TasksWidget::onQuickTaskAdded() { + QString title = mQuickTaskEdit->text().trimmed(); + if (title.isEmpty()) return; + + CalendarTask task; + task.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + + // Choose the first enabled calendar + QString calId = "personal"; + const auto& cals = CalendarData::instance()->getCalendars(); + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) { + calId = item->data(Qt::UserRole).toString(); + break; + } + } + + task.calendarId = calId; + task.title = title; + task.location = ""; + task.category = "None"; + task.hasStart = false; + task.hasDue = false; + task.status = "Not started"; + task.percentComplete = 0; + task.repeat = "Does not repeat"; + task.reminder = "No reminder"; + task.description = ""; + task.completed = false; + + CalendarData::instance()->addTask(task); + mQuickTaskEdit->clear(); + refreshData(); +} + +void TasksWidget::onTaskDoubleClicked(int row, int col) { + if (col == 0) return; // Ignore checkbox double clicks + + QTableWidgetItem* checkItem = mTaskTable->item(row, 0); + if (checkItem) { + QString taskId = checkItem->data(Qt::UserRole).toString(); + TaskDialog dlg(taskId, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } +} + +void TasksWidget::onTaskClicked(int row, int col) { + if (col != 0) return; // Only trigger for Checkbox column + + QTableWidgetItem* checkItem = mTaskTable->item(row, 0); + if (checkItem) { + QString taskId = checkItem->data(Qt::UserRole).toString(); + + // Find and toggle completion + const auto& tasks = CalendarData::instance()->getTasks(); + for (auto t : tasks) { + if (t.id == taskId) { + t.completed = !t.completed; + t.status = t.completed ? tr("Completed") : tr("Not started"); + t.percentComplete = t.completed ? 100 : 0; + CalendarData::instance()->updateTask(t); + break; + } + } + refreshData(); + } +} + +void TasksWidget::onFilterSelected(QListWidgetItem* item) { + int idx = mFilterList->row(item); + if (idx != -1) { + mCurrentFilterMode = idx; + updateTaskList(); + } +} + +void TasksWidget::onCalendarSelectionChanged(QListWidgetItem* /*item*/) { + updateTaskList(); +} + +void TasksWidget::onSearchChanged(const QString& text) { + mSearchText = text.trimmed(); + updateTaskList(); +} + +void TasksWidget::onCalendarContextMenu(const QPoint& pos) { + QListWidgetItem* item = mCalendarList->itemAt(pos); + if (!item) return; + + QString calId = item->data(Qt::UserRole).toString(); + QString calName = item->text(); + bool isChecked = item->checkState() == Qt::Checked; + + QMenu menu(this); + + QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); + QAction* showOnlyAct = menu.addAction(tr("Show Only %1").arg(calName)); + QAction* showAllAct = menu.addAction(tr("Show All Calendars")); + menu.addSeparator(); + QAction* newAct = menu.addAction(tr("New Calendar...")); + QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); + menu.addSeparator(); + QAction* exportAct = menu.addAction(tr("Export Calendar...")); + QAction* publishAct = menu.addAction(tr("Publish Calendar...")); + menu.addSeparator(); + QAction* propertiesAct = menu.addAction(tr("Properties")); + + QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); + if (!selectedAct) return; + + if (selectedAct == toggleAct) { + item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); + } else if (selectedAct == showOnlyAct) { + mCalendarList->blockSignals(true); + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* it = mCalendarList->item(i); + it->setCheckState(it == item ? Qt::Checked : Qt::Unchecked); + } + mCalendarList->blockSignals(false); + updateTaskList(); + } else if (selectedAct == showAllAct) { + mCalendarList->blockSignals(true); + for (int i = 0; i < mCalendarList->count(); ++i) { + mCalendarList->item(i)->setCheckState(Qt::Checked); + } + mCalendarList->blockSignals(false); + updateTaskList(); + } else if (selectedAct == newAct) { + CalendarPropertiesDialog dlg("", this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } else if (selectedAct == deleteAct) { + if (QMessageBox::question(this, tr("Delete Calendar"), + tr("Are you sure you want to delete calendar '%1'?\nThis will also delete all associated events and tasks.").arg(calName), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + CalendarData::instance()->removeCalendar(calId); + refreshData(); + } + } else if (selectedAct == exportAct) { + QMessageBox::information(this, tr("Export Calendar"), tr("Calendar '%1' exported successfully!").arg(calName)); + } else if (selectedAct == publishAct) { + QMessageBox::information(this, tr("Publish Calendar"), tr("Calendar '%1' published successfully!").arg(calName)); + } else if (selectedAct == propertiesAct) { + CalendarPropertiesDialog dlg(calId, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } +} diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.h b/retroshare-gui/src/gui/msgs/TasksWidget.h new file mode 100644 index 000000000..6ae25abbf --- /dev/null +++ b/retroshare-gui/src/gui/msgs/TasksWidget.h @@ -0,0 +1,68 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/TasksWidget.h * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef TASKSWIDGET_H +#define TASKSWIDGET_H + +#include +#include +#include "gui/msgs/CalendarData.h" +#include "ui_TasksWidget.h" + +class QListWidgetItem; + +class TasksWidget : public QWidget { + Q_OBJECT +public: + TasksWidget(QWidget* parent = nullptr); + ~TasksWidget(); + + void refreshData(); + +private slots: + void onNewTask(); + void onQuickTaskAdded(); + void onTaskDoubleClicked(int row, int col); + void onTaskClicked(int row, int col); + void onFilterSelected(QListWidgetItem* item); + void onCalendarSelectionChanged(QListWidgetItem* item); + void onSearchChanged(const QString& text); + void onCalendarContextMenu(const QPoint& pos); + +private: + void buildUi(); + void updateTaskList(); + + int mCurrentFilterMode; // 0=All, 1=Active, 2=Completed, 3=Overdue + QString mSearchText; + + // UI elements (loaded from UI file, kept as pointers for compatibility) + QCalendarWidget* mSidebarCalendar; + QListWidget* mFilterList; + QListWidget* mCalendarList; + + QLineEdit* mQuickTaskEdit; + QLineEdit* mSearchEdit; + QTableWidget* mTaskTable; + + Ui::TasksWidget ui; +}; + +#endif // TASKSWIDGET_H diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.ui b/retroshare-gui/src/gui/msgs/TasksWidget.ui new file mode 100644 index 000000000..01e3d9a0a --- /dev/null +++ b/retroshare-gui/src/gui/msgs/TasksWidget.ui @@ -0,0 +1,177 @@ + + + TasksWidget + + + + 0 + 0 + 800 + 600 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + 1 + + + + + 280 + 16777215 + + + + + 12 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + font-weight: bold; background-color: #4a90e2; color: white; border-radius: 4px; padding: 6px; + + + + New Task + + + + + + + true + + + QCalendarWidget::SingleLetterDayNames + + + QCalendarWidget::NoVerticalHeader + + + + + + + font-weight: bold; font-size: 14px; + + + Filter Tasks + + + + + + + + + + font-weight: bold; font-size: 14px; + + + Calendars + + + + + + + Qt::CustomContextMenu + + + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + Click here to add a new task + + + + + + + + 200 + 16777215 + + + + Search tasks... + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + + + + + + + + + diff --git a/retroshare-gui/src/retroshare-gui.pro b/retroshare-gui/src/retroshare-gui.pro index cdce55926..48964a716 100644 --- a/retroshare-gui/src/retroshare-gui.pro +++ b/retroshare-gui/src/retroshare-gui.pro @@ -476,6 +476,12 @@ HEADERS += rshare.h \ gui/connect/PGPKeyDialog.h \ gui/connect/FriendRecommendDialog.h \ gui/msgs/MessagesDialog.h \ + gui/msgs/CalendarData.h \ + gui/msgs/CalendarWidget.h \ + gui/msgs/TasksWidget.h \ + gui/msgs/CalendarPropertiesDialog.h \ + gui/msgs/EventDialog.h \ + gui/msgs/TaskDialog.h \ gui/msgs/MessageInterface.h \ gui/msgs/MessageComposer.h \ gui/msgs/MessageWindow.h \ @@ -664,6 +670,8 @@ FORMS += gui/StartDialog.ui \ gui/msgs/MessageComposer.ui \ gui/msgs/MessageWindow.ui\ gui/msgs/MessageWidget.ui\ + gui/msgs/CalendarWidget.ui \ + gui/msgs/TasksWidget.ui \ gui/settings/settingsw.ui \ gui/settings/GeneralPage.ui \ gui/settings/ServerPage.ui \ @@ -832,6 +840,12 @@ SOURCES += main.cpp \ gui/connect/ConfCertDialog.cpp \ gui/connect/PGPKeyDialog.cpp \ gui/msgs/MessagesDialog.cpp \ + gui/msgs/CalendarData.cpp \ + gui/msgs/CalendarWidget.cpp \ + gui/msgs/TasksWidget.cpp \ + gui/msgs/CalendarPropertiesDialog.cpp \ + gui/msgs/EventDialog.cpp \ + gui/msgs/TaskDialog.cpp \ gui/msgs/MessageComposer.cpp \ gui/msgs/MessageWidget.cpp \ gui/msgs/MessageWindow.cpp \ From cd47b2dc1f433b04cafae3d7be201d48726fee02 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:14:42 +0200 Subject: [PATCH 02/26] Fixed remove fixed sizes Added to show Calendar weeks --- .../src/gui/msgs/CalendarWidget.cpp | 170 +++++++++++++- retroshare-gui/src/gui/msgs/CalendarWidget.h | 16 ++ retroshare-gui/src/gui/msgs/CalendarWidget.ui | 210 ++++++++---------- retroshare-gui/src/gui/msgs/TasksWidget.ui | 6 - 4 files changed, 269 insertions(+), 133 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index 2467c0530..c7ed4b12a 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -21,6 +21,7 @@ #include "gui/msgs/CalendarWidget.h" #include "gui/msgs/EventDialog.h" #include "gui/msgs/CalendarPropertiesDialog.h" +#include #include #include #include @@ -62,6 +63,14 @@ void CalendarWidget::buildUi() { mWeekTable = ui.weekTable; mMonthTable = ui.monthTable; + // Create and insert calendar week label dynamically + mCwLabel = new QLabel(this); + mCwLabel->setObjectName("cwLabel"); + mCwLabel->setStyleSheet("font-weight: bold; font-size: 14px; margin-right: 15px;"); + int btnIndex = ui.topControlLayout->indexOf(ui.dayViewBtn); + if (btnIndex == -1) btnIndex = 6; + ui.topControlLayout->insertWidget(btnIndex, mCwLabel); + // Sidebar Calendar configs mSidebarCalendar->setSelectedDate(mSelectedDate); @@ -73,7 +82,6 @@ void CalendarWidget::buildUi() { mEventTable->setSelectionBehavior(QAbstractItemView::SelectRows); mEventTable->setSelectionMode(QAbstractItemView::SingleSelection); mEventTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - mEventTable->setMaximumHeight(120); connect(mEventTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); // Stacked widget pages setup @@ -89,7 +97,7 @@ void CalendarWidget::buildUi() { // 2. Week Table mWeekTable->setColumnCount(7); - mWeekTable->setHorizontalHeaderLabels({tr("Mon"), tr("Tue"), tr("Wed"), tr("Thu"), tr("Fri"), tr("Sat"), tr("Sun")}); + mWeekTable->setHorizontalHeaderLabels({tr("Monday"), tr("Tuesday"), tr("Wednesday"), tr("Thursday"), tr("Friday"), tr("Saturday"), tr("Sunday")}); mWeekTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); mWeekTable->verticalHeader()->setVisible(false); mWeekTable->setEditTriggers(QAbstractItemView::NoEditTriggers); @@ -97,11 +105,13 @@ void CalendarWidget::buildUi() { // 3. Month Table mMonthTable->setColumnCount(7); - mMonthTable->setHorizontalHeaderLabels({tr("Mon"), tr("Tue"), tr("Wed"), tr("Thu"), tr("Fri"), tr("Sat"), tr("Sun")}); + mMonthTable->setHorizontalHeaderLabels({tr("Monday"), tr("Tuesday"), tr("Wednesday"), tr("Thursday"), tr("Friday"), tr("Saturday"), tr("Sunday")}); mMonthTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); mMonthTable->verticalHeader()->setVisible(false); mMonthTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + mMonthTable->setItemDelegate(new MonthCalendarDelegate(this)); connect(mMonthTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + connect(mMonthTable, SIGNAL(cellClicked(int,int)), this, SLOT(onMonthCellClicked(int,int))); mViewStack->setCurrentIndex(mCurrentViewMode); @@ -174,9 +184,11 @@ void CalendarWidget::refreshData() { void CalendarWidget::updateViews() { mCellEventMap.clear(); - // 1. Update the Period Label + // 1. Update the Period Label and CW Label if (mCurrentViewMode == 0) { // Day View mPeriodLabel->setText(mSelectedDate.toString("dd MMMM yyyy")); + int cw = mSelectedDate.weekNumber(); + mCwLabel->setText(QString("CW: %1").arg(cw)); } else if (mCurrentViewMode == 1) { // Week View QDate monday = mSelectedDate.addDays(-(mSelectedDate.dayOfWeek() - 1)); QDate sunday = monday.addDays(6); @@ -185,8 +197,23 @@ void CalendarWidget::updateViews() { } else { mPeriodLabel->setText(monday.toString("dd MMM") + " - " + sunday.toString("dd MMM") + " " + sunday.toString("yyyy")); } + int cw = monday.weekNumber(); + mCwLabel->setText(QString("CW: %1").arg(cw)); } else { // Month View mPeriodLabel->setText(mSelectedDate.toString("MMMM yyyy")); + QDate firstOfMonth(mSelectedDate.year(), mSelectedDate.month(), 1); + int startDayOfWeek = firstOfMonth.dayOfWeek(); + QDate startDate = firstOfMonth.addDays(-(startDayOfWeek - 1)); + int daysInMonth = mSelectedDate.daysInMonth(); + int remainingDays = daysInMonth - (8 - startDayOfWeek); + int rowsNeeded = 1 + (remainingDays + 6) / 7; + int firstWeek = startDate.weekNumber(); + int lastWeek = startDate.addDays((rowsNeeded - 1) * 7).weekNumber(); + if (firstWeek == lastWeek) { + mCwLabel->setText(QString("CW: %1").arg(firstWeek)); + } else { + mCwLabel->setText(QString("CWs: %1-%2").arg(firstWeek).arg(lastWeek)); + } } // 2. Load and Filter Active Events @@ -328,13 +355,19 @@ void CalendarWidget::updateWeekView() { } void CalendarWidget::updateMonthView() { - mMonthTable->setRowCount(6); // A month calendar grid needs up to 6 rows + mMonthTable->clearContents(); // Find first day of the month QDate firstOfMonth(mSelectedDate.year(), mSelectedDate.month(), 1); int startDayOfWeek = firstOfMonth.dayOfWeek(); // 1=Mon, 7=Sun QDate startDate = firstOfMonth.addDays(-(startDayOfWeek - 1)); + int daysInMonth = mSelectedDate.daysInMonth(); + int remainingDays = daysInMonth - (8 - startDayOfWeek); + int rowsNeeded = 1 + (remainingDays + 6) / 7; + + mMonthTable->setRowCount(rowsNeeded); + const auto& events = CalendarData::instance()->getEvents(); QStringList enabledCalIds; @@ -343,13 +376,17 @@ void CalendarWidget::updateMonthView() { if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } - for (int row = 0; row < 6; ++row) { + for (int row = 0; row < rowsNeeded; ++row) { for (int col = 0; col < 7; ++col) { QDate date = startDate.addDays(row * 7 + col); // Build cell contents: "Date \n Event1 \n Event2..." QStringList cellLines; - cellLines << QString::number(date.day()); + if (date.day() == 1 || date.day() == date.daysInMonth()) { + cellLines << date.toString("d MMM"); + } else { + cellLines << QString::number(date.day()); + } QString matchedEventId = ""; for (const auto& ev : events) { @@ -361,11 +398,12 @@ void CalendarWidget::updateMonthView() { } QTableWidgetItem* cellItem = new QTableWidgetItem(cellLines.join("\n")); + cellItem->setData(Qt::UserRole + 1, date); // Store the QDate + if (date.month() != mSelectedDate.month()) { cellItem->setForeground(QBrush(Qt::gray)); } if (!matchedEventId.isEmpty()) { - cellItem->setBackground(QBrush(QColor("#eef5fc"))); mCellEventMap[QString("2_%1_%2").arg(row).arg(col)] = matchedEventId; } mMonthTable->setItem(row, col, cellItem); @@ -373,8 +411,8 @@ void CalendarWidget::updateMonthView() { } // Set row heights to expand nicely in the month grid - for (int row = 0; row < 6; ++row) { - mMonthTable->setRowHeight(row, 60); + for (int row = 0; row < rowsNeeded; ++row) { + mMonthTable->setRowHeight(row, 80); } } @@ -547,3 +585,115 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { } } } + +void CalendarWidget::onMonthCellClicked(int row, int col) { + QTableWidgetItem* item = mMonthTable->item(row, col); + if (item) { + QDate date = item->data(Qt::UserRole + 1).toDate(); + if (date.isValid()) { + mSelectedDate = date; + mSidebarCalendar->blockSignals(true); + mSidebarCalendar->setSelectedDate(date); + mSidebarCalendar->blockSignals(false); + updateViews(); + } + } +} + +MonthCalendarDelegate::MonthCalendarDelegate(CalendarWidget* parent) + : QStyledItemDelegate(parent), mCalendarWidget(parent) {} + +void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + + QDate cellDate = index.data(Qt::UserRole + 1).toDate(); + bool isSelected = (cellDate.isValid() && cellDate == mCalendarWidget->selectedDate()); + + // Draw background + QColor bgColor; + if (isSelected) { + bgColor = QColor("#eff6ff"); // Light blue highlight for selected day + } else if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { + bgColor = QColor("#f8fafc"); // Slate-50 for days outside the current month + } else if (index.column() == 5 || index.column() == 6) { + bgColor = QColor("#f1f5f9"); // Slate-100 for weekends + } else { + bgColor = QColor("#ffffff"); // White for standard weekdays + } + painter->fillRect(option.rect, bgColor); + + // Draw cell border + if (isSelected) { + painter->setPen(QPen(QColor("#3b82f6"), 2)); + painter->drawRect(option.rect.adjusted(1, 1, -1, -1)); + } else { + painter->setPen(QPen(QColor("#e2e8f0"), 1)); + painter->drawRect(option.rect); + } + + // Get item text + QString text = index.data(Qt::DisplayRole).toString(); + QStringList lines = text.split('\n'); + if (!lines.isEmpty()) { + QString dayStr = lines.first(); + + // 1. Draw day number in top right + QFont dayFont = option.font; + dayFont.setBold(true); + painter->setFont(dayFont); + + if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { + painter->setPen(QColor("#94a3b8")); // Muted grey for other month days + } else if (isSelected) { + painter->setPen(QColor("#2563eb")); // Darker blue for selected day number + } else { + painter->setPen(QColor("#1e293b")); // Slate-800 for standard days + } + + QRect dayRect = option.rect.adjusted(5, 5, -8, -5); + painter->drawText(dayRect, Qt::AlignTop | Qt::AlignRight, dayStr); + + // 2. Draw Week Badge if it's the first column + if (index.column() == 0 && cellDate.isValid()) { + int weekNum = cellDate.weekNumber(); + QString weekStr = QString("W %1").arg(weekNum); + + QRect badgeRect(option.rect.left() + 6, option.rect.top() + 5, 38, 16); + painter->setPen(Qt::NoPen); + painter->setBrush(QColor("#e2e8f0")); // Slate-200 + painter->drawRoundedRect(badgeRect, 8, 8); + + QFont badgeFont = option.font; + badgeFont.setPointSize(badgeFont.pointSize() - 2); + badgeFont.setBold(true); + painter->setFont(badgeFont); + painter->setPen(QColor("#475569")); // Slate-600 + painter->drawText(badgeRect, Qt::AlignCenter, weekStr); + } + + // 3. Draw events list below + int yOffset = option.rect.top() + 26; + QFont eventFont = option.font; + eventFont.setPointSize(eventFont.pointSize() - 1); + painter->setFont(eventFont); + + for (int i = 1; i < lines.size(); ++i) { + if (yOffset + 18 > option.rect.bottom()) break; // Out of bounds + + QString eventTitle = lines[i]; + QRect eventRect(option.rect.left() + 6, yOffset, option.rect.width() - 12, 16); + + painter->setPen(Qt::NoPen); + painter->setBrush(QColor("#e0f2fe")); // Light blue event background + painter->drawRoundedRect(eventRect, 3, 3); + + painter->setPen(QColor("#0369a1")); // Blue text for events + painter->drawText(eventRect.adjusted(4, 0, -4, 0), Qt::AlignVCenter | Qt::AlignLeft, eventTitle); + + yOffset += 19; + } + } + + painter->restore(); +} diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h index ad0cb9b6d..8ce9416f3 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -18,16 +18,29 @@ * * *******************************************************************************/ + #ifndef CALENDARWIDGET_H #define CALENDARWIDGET_H #include #include #include +#include #include "gui/msgs/CalendarData.h" #include "ui_CalendarWidget.h" class QListWidgetItem; +class QLabel; +class CalendarWidget; + +class MonthCalendarDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit MonthCalendarDelegate(CalendarWidget* parent); + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; +private: + CalendarWidget* mCalendarWidget; +}; class CalendarWidget : public QWidget { Q_OBJECT @@ -36,6 +49,7 @@ public: ~CalendarWidget(); void refreshData(); + QDate selectedDate() const { return mSelectedDate; } private slots: void onNewEvent(); @@ -49,6 +63,7 @@ private slots: void onCalendarSelectionChanged(QListWidgetItem* item); void onSearchChanged(const QString& text); void onCalendarContextMenu(const QPoint& pos); + void onMonthCellClicked(int row, int col); private: void buildUi(); @@ -67,6 +82,7 @@ private: QListWidget* mCalendarList; QLabel* mPeriodLabel; + QLabel* mCwLabel; QLineEdit* mSearchEdit; QTableWidget* mEventTable; // Upcoming events list at top diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.ui b/retroshare-gui/src/gui/msgs/CalendarWidget.ui index ff62a2bbb..d99c3d9c7 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.ui +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.ui @@ -35,12 +35,6 @@ 1 - - - 280 - 16777215 - - 12 @@ -107,22 +101,7 @@ - - - 10 - - - 10 - - - 10 - - - 10 - - - 10 - + @@ -218,103 +197,100 @@ - - - - 16777215 - 120 - + + + Qt::Orientation::Vertical - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QAbstractItemView::NoEditTriggers - - - - + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + QAbstractItemView::SelectionMode::SingleSelection + + + QAbstractItemView::SelectionBehavior::SelectRows + - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QAbstractItemView::NoEditTriggers - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QAbstractItemView::NoEditTriggers - - - - + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + + + diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.ui b/retroshare-gui/src/gui/msgs/TasksWidget.ui index 01e3d9a0a..452043cf7 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.ui +++ b/retroshare-gui/src/gui/msgs/TasksWidget.ui @@ -35,12 +35,6 @@ 1 - - - 280 - 16777215 - - 12 From 26f325fa330afdb6d4147bc4d8b26d996c970a77 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:32:45 +0200 Subject: [PATCH 03/26] add export and import calendars --- .../src/gui/msgs/CalendarPropertiesDialog.cpp | 12 + .../src/gui/msgs/CalendarPropertiesDialog.h | 2 + .../src/gui/msgs/CalendarWidget.cpp | 248 +++++++++++++++++- retroshare-gui/src/gui/msgs/CalendarWidget.h | 2 + retroshare-gui/src/gui/msgs/TasksWidget.cpp | 1 - 5 files changed, 262 insertions(+), 3 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp index 1cebf2a8f..61bf8c376 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp @@ -114,6 +114,10 @@ void CalendarPropertiesDialog::setupUi() { mRadioNetwork->setStyleSheet("font-size: 13px; font-weight: bold; padding: 5px;"); page1Layout->addWidget(mRadioNetwork); + mRadioImport = new QRadioButton(tr("Import a calendar from an iCalendar (.ics) file"), mPage1); + mRadioImport->setStyleSheet("font-size: 13px; font-weight: bold; padding: 5px;"); + page1Layout->addWidget(mRadioImport); + page1Layout->addStretch(); mStackedWidget->addWidget(mPage1); @@ -211,6 +215,10 @@ void CalendarPropertiesDialog::loadIdentities() { } void CalendarPropertiesDialog::onNext() { + if (mRadioImport && mRadioImport->isChecked()) { + accept(); + return; + } mStackedWidget->setCurrentWidget(mPage2); mBackBtn->show(); mCreateOrSaveBtn->show(); @@ -262,3 +270,7 @@ CalendarInfo CalendarPropertiesDialog::getCalendarInfo() const { info.owner = "local"; return info; } + +bool CalendarPropertiesDialog::isImportMode() const { + return mRadioImport && mRadioImport->isChecked(); +} diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h index 158d8ee68..35c0a6990 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h @@ -39,6 +39,7 @@ public: ~CalendarPropertiesDialog(); CalendarInfo getCalendarInfo() const; + bool isImportMode() const; private slots: void onNext(); @@ -62,6 +63,7 @@ private: // Page 1 widgets QRadioButton* mRadioComputer; QRadioButton* mRadioNetwork; + QRadioButton* mRadioImport; // Page 2 widgets QLineEdit* mNameEdit; diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index c7ed4b12a..f91db0649 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -22,6 +22,12 @@ #include "gui/msgs/EventDialog.h" #include "gui/msgs/CalendarPropertiesDialog.h" #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -469,7 +475,11 @@ void CalendarWidget::onNewEvent() { void CalendarWidget::onNewCalendar() { CalendarPropertiesDialog dlg("", this); if (dlg.exec() == QDialog::Accepted) { - refreshData(); + if (dlg.isImportMode()) { + importCalendar(); + } else { + refreshData(); + } } } @@ -575,7 +585,7 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { refreshData(); } } else if (selectedAct == exportAct) { - QMessageBox::information(this, tr("Export Calendar"), tr("Calendar '%1' exported successfully!").arg(calName)); + exportCalendar(calId, calName); } else if (selectedAct == publishAct) { QMessageBox::information(this, tr("Publish Calendar"), tr("Calendar '%1' published successfully!").arg(calName)); } else if (selectedAct == propertiesAct) { @@ -586,6 +596,240 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { } } +void CalendarWidget::exportCalendar(const QString& calId, const QString& calName) { + QString icsContent = "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//RetroShare//Calendar//EN\r\n"; + + const auto& events = CalendarData::instance()->getEvents(); + for (const auto& ev : events) { + if (ev.calendarId != calId) continue; + + icsContent += "BEGIN:VEVENT\r\n"; + icsContent += QString("UID:%1\r\n").arg(ev.id); + icsContent += QString("SUMMARY:%1\r\n").arg(ev.title); + + if (!ev.description.isEmpty()) { + QString desc = ev.description; + desc.replace("\n", "\\n").replace("\r", ""); + icsContent += QString("DESCRIPTION:%1\r\n").arg(desc); + } + + if (!ev.location.isEmpty()) { + icsContent += QString("LOCATION:%1\r\n").arg(ev.location); + } + + if (!ev.category.isEmpty()) { + icsContent += QString("CATEGORIES:%1\r\n").arg(ev.category); + } + + if (ev.allDay) { + icsContent += QString("DTSTART;VALUE=DATE:%1\r\n").arg(ev.start.toString("yyyyMMdd")); + icsContent += QString("DTEND;VALUE=DATE:%1\r\n").arg(ev.end.toString("yyyyMMdd")); + } else { + icsContent += QString("DTSTART:%1\r\n").arg(ev.start.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + icsContent += QString("DTEND:%1\r\n").arg(ev.end.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + } + + icsContent += "END:VEVENT\r\n"; + } + icsContent += "END:VCALENDAR\r\n"; + + QString defaultFileName = QString("%1.ics").arg(calName); + defaultFileName.replace(QRegExp("[\\\\/:*?\"<>|]"), "_"); + + QString selectedFilter; + QString filePath = QFileDialog::getSaveFileName( + this, + tr("Export Calendar"), + defaultFileName, + tr("iCalendar files (*.ics);;All Files (*)"), + &selectedFilter + ); + + if (!filePath.isEmpty()) { + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical( + this, + tr("Export Error"), + tr("Could not open file %1 for writing.").arg(filePath) + ); + } else { + QTextStream out(&file); + out.setCodec("UTF-8"); + out << icsContent; + file.close(); + + QMessageBox::information( + this, + tr("Export Calendar"), + tr("Calendar '%1' exported successfully to %2!").arg(calName).arg(QDir::toNativeSeparators(filePath)) + ); + } + } +} + +void CalendarWidget::importCalendar() { + QString filePath = QFileDialog::getOpenFileName( + this, + tr("Import Calendar"), + "", + tr("iCalendar files (*.ics);;All Files (*)") + ); + + if (filePath.isEmpty()) return; + + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical( + this, + tr("Import Error"), + tr("Could not open file %1 for reading.").arg(filePath) + ); + return; + } + + QTextStream in(&file); + in.setCodec("UTF-8"); + + QStringList rawLines; + while (!in.atEnd()) { + rawLines.append(in.readLine()); + } + file.close(); + + // iCalendar line unfolding (RFC 5545) + QStringList lines; + for (int i = 0; i < rawLines.size(); ++i) { + QString line = rawLines[i]; + while (i + 1 < rawLines.size() && (rawLines[i + 1].startsWith(" ") || rawLines[i + 1].startsWith("\t"))) { + line += rawLines[i + 1].mid(1); + i++; + } + lines.append(line); + } + + QString calendarName = QFileInfo(filePath).baseName(); + QList importedEvents; + + auto parseIcsDateTime = [](const QString& val) -> QDateTime { + QDateTime dt; + if (val.endsWith('Z')) { + dt = QDateTime::fromString(val, "yyyyMMdd'T'HHmmss'Z'"); + dt.setTimeSpec(Qt::UTC); + dt = dt.toLocalTime(); + } else { + dt = QDateTime::fromString(val, "yyyyMMdd'T'HHmmss"); + dt.setTimeSpec(Qt::LocalTime); + } + return dt; + }; + + bool inEvent = false; + CalendarEvent currentEvent; + + for (const QString& line : lines) { + QString trimmedLine = line.trimmed(); + if (trimmedLine.isEmpty()) continue; + + if (trimmedLine.startsWith("X-WR-CALNAME:", Qt::CaseInsensitive)) { + QString nameVal = trimmedLine.mid(13).trimmed(); + if (!nameVal.isEmpty()) calendarName = nameVal; + } else if (trimmedLine.startsWith("BEGIN:VEVENT", Qt::CaseInsensitive)) { + inEvent = true; + currentEvent = CalendarEvent(); + currentEvent.id = QUuid::createUuid().toString(); + currentEvent.allDay = false; + currentEvent.isPublic = false; + } else if (trimmedLine.startsWith("END:VEVENT", Qt::CaseInsensitive)) { + if (inEvent) { + // Validate dates + if (!currentEvent.start.isValid()) { + currentEvent.start = QDateTime::currentDateTime(); + } + if (!currentEvent.end.isValid()) { + currentEvent.end = currentEvent.start.addSecs(3600); + } + importedEvents.append(currentEvent); + inEvent = false; + } + } else if (inEvent) { + int colonIdx = trimmedLine.indexOf(':'); + int semiIdx = trimmedLine.indexOf(';'); + int splitIdx = -1; + + if (colonIdx != -1 && semiIdx != -1) { + splitIdx = qMin(colonIdx, semiIdx); + } else if (colonIdx != -1) { + splitIdx = colonIdx; + } else if (semiIdx != -1) { + splitIdx = semiIdx; + } + + if (splitIdx != -1) { + QString key = trimmedLine.left(splitIdx).trimmed(); + QString val = trimmedLine.mid(colonIdx + 1).trimmed(); + + if (key.compare("UID", Qt::CaseInsensitive) == 0) { + currentEvent.id = val; + } else if (key.compare("SUMMARY", Qt::CaseInsensitive) == 0) { + currentEvent.title = val; + } else if (key.compare("LOCATION", Qt::CaseInsensitive) == 0) { + currentEvent.location = val; + } else if (key.compare("CATEGORIES", Qt::CaseInsensitive) == 0) { + currentEvent.category = val; + } else if (key.compare("DESCRIPTION", Qt::CaseInsensitive) == 0) { + QString desc = val; + desc.replace("\\n", "\n").replace("\\r", "").replace("\\,", ","); + currentEvent.description = desc; + } else if (key.startsWith("DTSTART", Qt::CaseInsensitive)) { + if (trimmedLine.contains("VALUE=DATE", Qt::CaseInsensitive)) { + currentEvent.allDay = true; + currentEvent.start = QDateTime(QDate::fromString(val, "yyyyMMdd"), QTime(0, 0)); + } else { + currentEvent.start = parseIcsDateTime(val); + } + } else if (key.startsWith("DTEND", Qt::CaseInsensitive)) { + if (trimmedLine.contains("VALUE=DATE", Qt::CaseInsensitive)) { + currentEvent.allDay = true; + currentEvent.end = QDateTime(QDate::fromString(val, "yyyyMMdd"), QTime(0, 0)); + } else { + currentEvent.end = parseIcsDateTime(val); + } + } + } + } + } + + // Create the calendar info + CalendarInfo cal; + cal.id = QUuid::createUuid().toString(); + cal.name = calendarName; + cal.color = QColor("#4a90e2"); + cal.isPublic = false; + cal.owner = "local"; + cal.showReminders = true; + cal.email = ""; + cal.onNetwork = false; + + CalendarData::instance()->addCalendar(cal); + + // Add all events to CalendarData + for (auto& ev : importedEvents) { + ev.calendarId = cal.id; + CalendarData::instance()->addEvent(ev); + } + + QMessageBox::information( + this, + tr("Import Calendar"), + tr("Successfully imported calendar '%1' with %2 events!").arg(calendarName).arg(importedEvents.size()) + ); + + refreshData(); +} + void CalendarWidget::onMonthCellClicked(int row, int col) { QTableWidgetItem* item = mMonthTable->item(row, col); if (item) { diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h index 8ce9416f3..c80d48d0f 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -72,6 +72,8 @@ private: void updateWeekView(); void updateMonthView(); void updateEventList(); + void exportCalendar(const QString& calId, const QString& calName); + void importCalendar(); QDate mSelectedDate; int mCurrentViewMode; // 0=Day, 1=Week, 2=Month diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/msgs/TasksWidget.cpp index 637ebac67..0335488f9 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.cpp +++ b/retroshare-gui/src/gui/msgs/TasksWidget.cpp @@ -215,7 +215,6 @@ void TasksWidget::onQuickTaskAdded() { // Choose the first enabled calendar QString calId = "personal"; - const auto& cals = CalendarData::instance()->getCalendars(); for (int i = 0; i < mCalendarList->count(); ++i) { QListWidgetItem* item = mCalendarList->item(i); if (item->checkState() == Qt::Checked) { From 6318debcc9326a95002be17e54ac7ea0fbc33251 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 5 Jun 2026 00:13:22 +0200 Subject: [PATCH 04/26] Added gxs backend for calendar --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 505 +++++++++++++++++- retroshare-gui/src/gui/msgs/CalendarData.h | 26 +- .../src/gui/msgs/CalendarPropertiesDialog.cpp | 39 +- .../src/gui/msgs/CalendarWidget.cpp | 145 +++-- retroshare-gui/src/gui/msgs/CalendarWidget.h | 8 + retroshare-gui/src/gui/msgs/TasksWidget.cpp | 187 +++++-- retroshare-gui/src/gui/msgs/TasksWidget.h | 9 + 7 files changed, 845 insertions(+), 74 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index fe475a9ca..3603ab89d 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -21,6 +21,8 @@ #include "gui/msgs/CalendarData.h" #include #include +#include +#include #include #include #include @@ -34,12 +36,24 @@ CalendarData* CalendarData::instance() { return mInstance; } -CalendarData::CalendarData() { +CalendarData::CalendarData() : QObject(), mEventHandlerId(0) { loadData(); + + if (rsEvents && rsGxsCalendar) { + rsEvents->registerEventsHandler( + [this](std::shared_ptr event) { + RsQThreadUtils::postToObject([=]() { handleGxsEvent(event); }, this); + }, + mEventHandlerId, RsEventType::GXS_CALENDAR + ); + } } CalendarData::~CalendarData() { saveData(); + if (rsEvents && mEventHandlerId != 0) { + rsEvents->unregisterEventsHandler(mEventHandlerId); + } } void CalendarData::loadData() { @@ -78,7 +92,7 @@ void CalendarData::loadData() { defaultCal.isPublic = false; defaultCal.owner = "local"; defaultCal.showReminders = true; - defaultCal.email = "defnator "; + defaultCal.email = "retroshare "; defaultCal.onNetwork = false; mCalendars.append(defaultCal); @@ -89,7 +103,7 @@ void CalendarData::loadData() { testCal.isPublic = true; testCal.owner = "local"; testCal.showReminders = true; - testCal.email = "defnator "; + testCal.email = "retroshare "; testCal.onNetwork = true; mCalendars.append(testCal); } @@ -225,6 +239,11 @@ void CalendarData::updateCalendar(const CalendarInfo& cal) { void CalendarData::removeCalendar(const QString& id) { for (int i = 0; i < mCalendars.size(); ++i) { if (mCalendars[i].id == id) { + // Unsubscribe from GXS if network calendar + if (mCalendars[i].onNetwork && rsGxsCalendar) { + std::string errMsg; + rsGxsCalendar->subscribeToCalendar(RsGxsGroupId(id.toStdString()), false, errMsg); + } mCalendars.removeAt(i); break; } @@ -242,6 +261,7 @@ void CalendarData::removeCalendar(const QString& id) { void CalendarData::addEvent(const CalendarEvent& ev) { mEvents.append(ev); saveData(); + publishCalendarUpdates(ev.calendarId); } void CalendarData::updateEvent(const CalendarEvent& ev) { @@ -252,21 +272,28 @@ void CalendarData::updateEvent(const CalendarEvent& ev) { } } saveData(); + publishCalendarUpdates(ev.calendarId); } void CalendarData::removeEvent(const QString& id) { + QString calId; for (int i = 0; i < mEvents.size(); ++i) { if (mEvents[i].id == id) { + calId = mEvents[i].calendarId; mEvents.removeAt(i); break; } } saveData(); + if (!calId.isEmpty()) { + publishCalendarUpdates(calId); + } } void CalendarData::addTask(const CalendarTask& task) { mTasks.append(task); saveData(); + publishCalendarUpdates(task.calendarId); } void CalendarData::updateTask(const CalendarTask& task) { @@ -277,16 +304,22 @@ void CalendarData::updateTask(const CalendarTask& task) { } } saveData(); + publishCalendarUpdates(task.calendarId); } void CalendarData::removeTask(const QString& id) { + QString calId; for (int i = 0; i < mTasks.size(); ++i) { if (mTasks[i].id == id) { + calId = mTasks[i].calendarId; mTasks.removeAt(i); break; } } saveData(); + if (!calId.isEmpty()) { + publishCalendarUpdates(calId); + } } QMap CalendarData::getContacts() { @@ -315,3 +348,469 @@ QMap CalendarData::getContacts() { return contacts; } + +static RsGxsId getGxsIdFromEmail(const QString& email) { + int idx = email.lastIndexOf('@'); + int endIdx = email.lastIndexOf('>'); + if (idx != -1 && endIdx != -1 && endIdx > idx) { + std::string gxsIdStr = email.mid(idx + 1, endIdx - idx - 1).toStdString(); + return RsGxsId(gxsIdStr); + } + return RsGxsId(); +} + +QString CalendarData::exportCalendarToIcs(const QString& calId) const { + QString icsContent = "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//RetroShare//Calendar//EN\r\n"; + + for (const auto& ev : mEvents) { + if (ev.calendarId != calId) continue; + + icsContent += "BEGIN:VEVENT\r\n"; + icsContent += QString("UID:%1\r\n").arg(ev.id); + icsContent += QString("SUMMARY:%1\r\n").arg(ev.title); + + if (!ev.description.isEmpty()) { + QString desc = ev.description; + desc.replace("\n", "\\n").replace("\r", ""); + icsContent += QString("DESCRIPTION:%1\r\n").arg(desc); + } + + if (!ev.location.isEmpty()) { + icsContent += QString("LOCATION:%1\r\n").arg(ev.location); + } + + if (!ev.category.isEmpty()) { + icsContent += QString("CATEGORIES:%1\r\n").arg(ev.category); + } + + if (ev.allDay) { + icsContent += QString("DTSTART;VALUE=DATE:%1\r\n").arg(ev.start.toString("yyyyMMdd")); + icsContent += QString("DTEND;VALUE=DATE:%1\r\n").arg(ev.end.toString("yyyyMMdd")); + } else { + icsContent += QString("DTSTART:%1\r\n").arg(ev.start.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + icsContent += QString("DTEND:%1\r\n").arg(ev.end.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + } + + icsContent += "END:VEVENT\r\n"; + } + + for (const auto& t : mTasks) { + if (t.calendarId != calId) continue; + + icsContent += "BEGIN:VTODO\r\n"; + icsContent += QString("UID:%1\r\n").arg(t.id); + icsContent += QString("SUMMARY:%1\r\n").arg(t.title); + + if (!t.description.isEmpty()) { + QString desc = t.description; + desc.replace("\n", "\\n").replace("\r", ""); + icsContent += QString("DESCRIPTION:%1\r\n").arg(desc); + } + + if (!t.location.isEmpty()) { + icsContent += QString("LOCATION:%1\r\n").arg(t.location); + } + + if (!t.category.isEmpty()) { + icsContent += QString("CATEGORIES:%1\r\n").arg(t.category); + } + + if (t.hasStart) { + icsContent += QString("DTSTART:%1\r\n").arg(t.start.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + } + + if (t.hasDue) { + icsContent += QString("DUE:%1\r\n").arg(t.due.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + } + + if (!t.status.isEmpty()) { + icsContent += QString("STATUS:%1\r\n").arg(t.status); + } + + icsContent += QString("PERCENT-COMPLETE:%1\r\n").arg(t.percentComplete); + icsContent += QString("COMPLETED:%1\r\n").arg(t.completed ? "TRUE" : "FALSE"); + + icsContent += "END:VTODO\r\n"; + } + + icsContent += "END:VCALENDAR\r\n"; + return icsContent; +} + +void CalendarData::importCalendarFromIcs(const QString& calId, const QString& icsData) { + auto evIt = mEvents.begin(); + while (evIt != mEvents.end()) { + if (evIt->calendarId == calId) { + evIt = mEvents.erase(evIt); + } else { + ++evIt; + } + } + + auto tIt = mTasks.begin(); + while (tIt != mTasks.end()) { + if (tIt->calendarId == calId) { + tIt = mTasks.erase(tIt); + } else { + ++tIt; + } + } + + QStringList rawLines = icsData.split(QRegExp("[\r\n]"), QString::SkipEmptyParts); + QStringList lines; + for (int i = 0; i < rawLines.size(); ++i) { + QString line = rawLines[i]; + while (i + 1 < rawLines.size() && (rawLines[i + 1].startsWith(" ") || rawLines[i + 1].startsWith("\t"))) { + line += rawLines[i + 1].mid(1); + i++; + } + lines.append(line); + } + + auto parseIcsDateTime = [](const QString& val) -> QDateTime { + QDateTime dt; + if (val.endsWith('Z')) { + dt = QDateTime::fromString(val, "yyyyMMdd'T'HHmmss'Z'"); + dt.setTimeSpec(Qt::UTC); + dt = dt.toLocalTime(); + } else { + dt = QDateTime::fromString(val, "yyyyMMdd'T'HHmmss"); + dt.setTimeSpec(Qt::LocalTime); + } + return dt; + }; + + bool inEvent = false; + bool inTask = false; + CalendarEvent currentEvent; + CalendarTask currentTask; + + for (const QString& line : lines) { + QString trimmedLine = line.trimmed(); + if (trimmedLine.isEmpty()) continue; + + if (trimmedLine.startsWith("BEGIN:VEVENT", Qt::CaseInsensitive)) { + inEvent = true; + currentEvent = CalendarEvent(); + currentEvent.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + currentEvent.calendarId = calId; + currentEvent.allDay = false; + currentEvent.isPublic = true; + } else if (trimmedLine.startsWith("END:VEVENT", Qt::CaseInsensitive)) { + if (inEvent) { + if (!currentEvent.start.isValid()) currentEvent.start = QDateTime::currentDateTime(); + if (!currentEvent.end.isValid()) currentEvent.end = currentEvent.start.addSecs(3600); + mEvents.append(currentEvent); + inEvent = false; + } + } else if (trimmedLine.startsWith("BEGIN:VTODO", Qt::CaseInsensitive)) { + inTask = true; + currentTask = CalendarTask(); + currentTask.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + currentTask.calendarId = calId; + currentTask.hasStart = false; + currentTask.hasDue = false; + currentTask.completed = false; + currentTask.percentComplete = 0; + } else if (trimmedLine.startsWith("END:VTODO", Qt::CaseInsensitive)) { + if (inTask) { + mTasks.append(currentTask); + inTask = false; + } + } else if (inEvent) { + int colonIdx = trimmedLine.indexOf(':'); + int semiIdx = trimmedLine.indexOf(';'); + int splitIdx = -1; + + if (colonIdx != -1 && semiIdx != -1) splitIdx = qMin(colonIdx, semiIdx); + else if (colonIdx != -1) splitIdx = colonIdx; + else if (semiIdx != -1) splitIdx = semiIdx; + + if (splitIdx != -1) { + QString key = trimmedLine.left(splitIdx).trimmed(); + QString val = trimmedLine.mid(colonIdx + 1).trimmed(); + + if (key.compare("UID", Qt::CaseInsensitive) == 0) { + currentEvent.id = val; + } else if (key.compare("SUMMARY", Qt::CaseInsensitive) == 0) { + currentEvent.title = val; + } else if (key.compare("LOCATION", Qt::CaseInsensitive) == 0) { + currentEvent.location = val; + } else if (key.compare("CATEGORIES", Qt::CaseInsensitive) == 0) { + currentEvent.category = val; + } else if (key.compare("DESCRIPTION", Qt::CaseInsensitive) == 0) { + currentEvent.description = val.replace("\\n", "\n").replace("\\r", "").replace("\\,", ","); + } else if (key.startsWith("DTSTART", Qt::CaseInsensitive)) { + if (trimmedLine.contains("VALUE=DATE", Qt::CaseInsensitive)) { + currentEvent.allDay = true; + currentEvent.start = QDateTime(QDate::fromString(val, "yyyyMMdd"), QTime(0, 0)); + } else { + currentEvent.start = parseIcsDateTime(val); + } + } else if (key.startsWith("DTEND", Qt::CaseInsensitive)) { + if (trimmedLine.contains("VALUE=DATE", Qt::CaseInsensitive)) { + currentEvent.allDay = true; + currentEvent.end = QDateTime(QDate::fromString(val, "yyyyMMdd"), QTime(0, 0)); + } else { + currentEvent.end = parseIcsDateTime(val); + } + } + } + } else if (inTask) { + int colonIdx = trimmedLine.indexOf(':'); + int semiIdx = trimmedLine.indexOf(';'); + int splitIdx = -1; + + if (colonIdx != -1 && semiIdx != -1) splitIdx = qMin(colonIdx, semiIdx); + else if (colonIdx != -1) splitIdx = colonIdx; + else if (semiIdx != -1) splitIdx = semiIdx; + + if (splitIdx != -1) { + QString key = trimmedLine.left(splitIdx).trimmed(); + QString val = trimmedLine.mid(colonIdx + 1).trimmed(); + + if (key.compare("UID", Qt::CaseInsensitive) == 0) { + currentTask.id = val; + } else if (key.compare("SUMMARY", Qt::CaseInsensitive) == 0) { + currentTask.title = val; + } else if (key.compare("LOCATION", Qt::CaseInsensitive) == 0) { + currentTask.location = val; + } else if (key.compare("CATEGORIES", Qt::CaseInsensitive) == 0) { + currentTask.category = val; + } else if (key.compare("DESCRIPTION", Qt::CaseInsensitive) == 0) { + currentTask.description = val.replace("\\n", "\n").replace("\\r", "").replace("\\,", ","); + } else if (key.startsWith("DTSTART", Qt::CaseInsensitive)) { + currentTask.hasStart = true; + currentTask.start = parseIcsDateTime(val); + } else if (key.startsWith("DUE", Qt::CaseInsensitive)) { + currentTask.hasDue = true; + currentTask.due = parseIcsDateTime(val); + } else if (key.compare("STATUS", Qt::CaseInsensitive) == 0) { + currentTask.status = val; + } else if (key.compare("PERCENT-COMPLETE", Qt::CaseInsensitive) == 0) { + currentTask.percentComplete = val.toInt(); + } else if (key.compare("COMPLETED", Qt::CaseInsensitive) == 0) { + currentTask.completed = (val.compare("TRUE", Qt::CaseInsensitive) == 0); + } + } + } + } +} + +void CalendarData::migrateCalendarData(const QString& oldId, const QString& newId) { + for (auto& ev : mEvents) { + if (ev.calendarId == oldId) { + ev.calendarId = newId; + } + } + for (auto& t : mTasks) { + if (t.calendarId == oldId) { + t.calendarId = newId; + } + } +} + +void CalendarData::publishCalendarUpdates(const QString& calId) { + if (!rsGxsCalendar) return; + + CalendarInfo cal; + bool found = false; + for (const auto& c : mCalendars) { + if (c.id == calId) { + cal = c; + found = true; + break; + } + } + if (!found || !cal.onNetwork) return; + + RsGxsId authorId = getGxsIdFromEmail(cal.email); + RsGxsGroupId groupId(calId.toStdString()); + QString ics = exportCalendarToIcs(calId); + RsGxsMessageId msgId; + std::string errMsg; + rsGxsCalendar->publishCalendarIcs(groupId, ics.toStdString(), authorId, msgId, errMsg); +} + +bool CalendarData::publishCalendar(const QString& oldId, const QString& email, QString& newIdOut) { + if (!rsGxsCalendar) return false; + + // Find local calendar + int calIdx = -1; + for (int i = 0; i < mCalendars.size(); ++i) { + if (mCalendars[i].id == oldId) { + calIdx = i; + break; + } + } + if (calIdx == -1) return false; + + CalendarInfo& cal = mCalendars[calIdx]; + if (cal.onNetwork) return false; // Already on network + + // Extract GXS ID from email + RsGxsId authorId = getGxsIdFromEmail(email); + + RsGxsGroupId groupId; + std::string errMsg; + if (!rsGxsCalendar->createCalendar(cal.name.toStdString(), "RetroShare Calendar", authorId, groupId, errMsg)) { + return false; + } + + QString newId = QString::fromStdString(groupId.toStdString()); + newIdOut = newId; + + // Migrate events and tasks + migrateCalendarData(oldId, newId); + + // Update calendar info + cal.id = newId; + cal.onNetwork = true; + cal.isPublic = true; + cal.email = email; + cal.owner = "local"; + + saveData(); + + // Subscribe and publish initial ICS + rsGxsCalendar->subscribeToCalendar(groupId, true, errMsg); + publishCalendarUpdates(newId); + + emit calendarDataChanged(); + return true; +} + +bool CalendarData::subscribeToCalendar(const QString& id, bool subscribe, const QString& name) { + if (!rsGxsCalendar) return false; + + RsGxsGroupId groupId(id.toStdString()); + std::string errMsg; + if (!rsGxsCalendar->subscribeToCalendar(groupId, subscribe, errMsg)) { + return false; + } + + if (subscribe) { + // Find if it's already in mCalendars + bool found = false; + for (const auto& c : mCalendars) { + if (c.id == id) { + found = true; + break; + } + } + if (!found) { + CalendarInfo localCal; + localCal.id = id; + localCal.name = name.isEmpty() ? tr("Shared Calendar") : name; + localCal.color = QColor("#4a90e2"); + localCal.isPublic = true; + localCal.onNetwork = true; + localCal.showReminders = true; + localCal.owner = "network"; + mCalendars.append(localCal); + saveData(); + } + emit calendarDataChanged(); + // Trigger sync to fetch contents + syncWithGxs(); + } else { + // Remove from local list + for (int i = 0; i < mCalendars.size(); ++i) { + if (mCalendars[i].id == id) { + mCalendars.removeAt(i); + break; + } + } + // Remove associated events and tasks + mEvents.erase(std::remove_if(mEvents.begin(), mEvents.end(), + [&id](const CalendarEvent& ev) { return ev.calendarId == id; }), mEvents.end()); + mTasks.erase(std::remove_if(mTasks.begin(), mTasks.end(), + [&id](const CalendarTask& t) { return t.calendarId == id; }), mTasks.end()); + + saveData(); + emit calendarDataChanged(); + } + return true; +} + +void CalendarData::syncWithGxs() { + if (!rsGxsCalendar) return; + + std::list calendars; + if (rsGxsCalendar->getCalendarsSummaries(calendars)) { + bool changed = false; + for (const auto& meta : calendars) { + bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); + if (isSubscribed) { + QString calId = QString::fromStdString(meta.mGroupId.toStdString()); + + bool found = false; + CalendarInfo localCal; + for (auto& c : mCalendars) { + if (c.id == calId) { + localCal = c; + found = true; + break; + } + } + + if (!found) { + localCal.id = calId; + localCal.name = QString::fromUtf8(meta.mGroupName.c_str()); + localCal.color = QColor("#4a90e2"); + localCal.isPublic = true; + localCal.onNetwork = true; + localCal.showReminders = true; + localCal.owner = "network"; + mCalendars.append(localCal); + changed = true; + } + + std::vector messages; + if (rsGxsCalendar->getCalendarContent(meta.mGroupId, messages)) { + if (!messages.empty()) { + uint32_t latestTime = 0; + size_t latestIdx = 0; + for (size_t i = 0; i < messages.size(); ++i) { + if (messages[i].mMeta.mPublishTs > latestTime) { + latestTime = messages[i].mMeta.mPublishTs; + latestIdx = i; + } + } + QString msgIdStr = QString::fromStdString(messages[latestIdx].mMeta.mMsgId.toStdString()); + if (!mLastMsgIds.contains(calId) || mLastMsgIds[calId] != msgIdStr) { + importCalendarFromIcs(calId, QString::fromStdString(messages[latestIdx].mIcsData)); + mLastMsgIds[calId] = msgIdStr; + changed = true; + } + } + } + } + } + + if (changed) { + saveData(); + emit calendarDataChanged(); + } + } +} + +void CalendarData::handleGxsEvent(std::shared_ptr event) { + const RsGxsCalendarEvent *e = dynamic_cast(event.get()); + if (e) { + switch (e->mCalendarEventCode) { + case RsCalendarEventCode::NEW_CALENDAR: + case RsCalendarEventCode::UPDATED_CALENDAR: + syncWithGxs(); + case RsCalendarEventCode::NEW_EVENT: + case RsCalendarEventCode::UPDATED_EVENT: + case RsCalendarEventCode::SUBSCRIBE_STATUS_CHANGED: + syncWithGxs(); + break; + default: + break; + } + } +} diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/msgs/CalendarData.h index 63109d455..51405728f 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.h +++ b/retroshare-gui/src/gui/msgs/CalendarData.h @@ -27,6 +27,9 @@ #include #include #include +#include +#include +#include struct CalendarInfo { QString id; @@ -73,7 +76,8 @@ struct CalendarTask { bool completed; }; -class CalendarData { +class CalendarData : public QObject { + Q_OBJECT public: static CalendarData* instance(); @@ -99,15 +103,33 @@ public: // Helpers static QMap getContacts(); // map PGP ID -> Name + QString exportCalendarToIcs(const QString& calId) const; + void importCalendarFromIcs(const QString& calId, const QString& icsData); + void migrateCalendarData(const QString& oldId, const QString& newId); + void publishCalendarUpdates(const QString& calId); + bool publishCalendar(const QString& oldId, const QString& email, QString& newIdOut); + bool subscribeToCalendar(const QString& id, bool subscribe, const QString& name = ""); + +signals: + void calendarDataChanged(); + +public slots: + void syncWithGxs(); + +private slots: + void handleGxsEvent(std::shared_ptr event); + private: CalendarData(); - ~CalendarData(); + ~CalendarData() override; QList mCalendars; QList mEvents; QList mTasks; + QMap mLastMsgIds; static CalendarData* mInstance; + uint32_t mEventHandlerId; }; #endif // CALENDARDATA_H diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp index 61bf8c376..0c0a24615 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp @@ -34,6 +34,7 @@ #include #include #include +#include CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget* parent) : QDialog(parent), mCalId(calId), mEditMode(!calId.isEmpty()), mSelectedColor(QColor("#4a90e2")) @@ -249,10 +250,42 @@ void CalendarPropertiesDialog::onAccept() { CalendarInfo info = getCalendarInfo(); - if (mEditMode) { - CalendarData::instance()->updateCalendar(info); + if (info.onNetwork && rsGxsCalendar) { + // Extract GXS ID from email + RsGxsId authorId; + int idx = info.email.lastIndexOf('@'); + int endIdx = info.email.lastIndexOf('>'); + if (idx != -1 && endIdx != -1 && endIdx > idx) { + std::string gxsIdStr = info.email.mid(idx + 1, endIdx - idx - 1).toStdString(); + authorId = RsGxsId(gxsIdStr); + } + + std::string errMsg; + if (mEditMode) { + RsGxsGroupId groupId(info.id.toStdString()); + if (rsGxsCalendar->updateCalendar(groupId, info.name.toStdString(), "RetroShare Calendar", authorId, errMsg)) { + CalendarData::instance()->updateCalendar(info); + } else { + QMessageBox::critical(this, tr("GXS Error"), tr("Failed to update network calendar: %1").arg(QString::fromStdString(errMsg))); + return; + } + } else { + RsGxsGroupId groupId; + if (rsGxsCalendar->createCalendar(info.name.toStdString(), "RetroShare Calendar", authorId, groupId, errMsg)) { + info.id = QString::fromStdString(groupId.toStdString()); + rsGxsCalendar->subscribeToCalendar(groupId, true, errMsg); + CalendarData::instance()->addCalendar(info); + } else { + QMessageBox::critical(this, tr("GXS Error"), tr("Failed to create network calendar: %1").arg(QString::fromStdString(errMsg))); + return; + } + } } else { - CalendarData::instance()->addCalendar(info); + if (mEditMode) { + CalendarData::instance()->updateCalendar(info); + } else { + CalendarData::instance()->addCalendar(info); + } } accept(); diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index f91db0649..2ae14086c 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -21,7 +21,10 @@ #include "gui/msgs/CalendarWidget.h" #include "gui/msgs/EventDialog.h" #include "gui/msgs/CalendarPropertiesDialog.h" +#include +#include #include +#include #include #include #include @@ -44,17 +47,28 @@ #include #include #include +#include #include CalendarWidget::CalendarWidget(QWidget* parent) - : QWidget(parent), mSelectedDate(QDate::currentDate()), mCurrentViewMode(2) // Default to Month View + : QWidget(parent), mSelectedDate(QDate::currentDate()), mCurrentViewMode(2), mCalendarListMode(0), mCalendarViewCombo(nullptr), mInitialLoadDone(false) { buildUi(); refreshData(); + + connect(CalendarData::instance(), SIGNAL(calendarDataChanged()), this, SLOT(refreshData())); } CalendarWidget::~CalendarWidget() {} +void CalendarWidget::showEvent(QShowEvent* event) { + QWidget::showEvent(event); + if (!mInitialLoadDone) { + mInitialLoadDone = true; + CalendarData::instance()->syncWithGxs(); + } +} + void CalendarWidget::buildUi() { ui.setupUi(this); @@ -77,6 +91,19 @@ void CalendarWidget::buildUi() { if (btnIndex == -1) btnIndex = 6; ui.topControlLayout->insertWidget(btnIndex, mCwLabel); + // Hide calendarsLabel + ui.calendarsLabel->hide(); + + // Create and insert calendarViewCombo dynamically + mCalendarViewCombo = new QComboBox(this); + mCalendarViewCombo->setObjectName("calendarViewCombo"); + mCalendarViewCombo->addItems({tr("My Calendars"), tr("Shared Calendars")}); + mCalendarViewCombo->setStyleSheet("font-weight: bold; font-size: 13px; margin-bottom: 4px;"); + + int labelIndex = ui.sidebarLayout->indexOf(ui.calendarsLabel); + if (labelIndex == -1) labelIndex = 2; // Default fallback position + ui.sidebarLayout->insertWidget(labelIndex, mCalendarViewCombo); + // Sidebar Calendar configs mSidebarCalendar->setSelectedDate(mSelectedDate); @@ -131,6 +158,7 @@ void CalendarWidget::buildUi() { connect(mCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCalendarSelectionChanged(QListWidgetItem*))); connect(mCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onCalendarContextMenu(const QPoint&))); connect(ui.newCalBtn, SIGNAL(clicked()), this, SLOT(onNewCalendar())); + connect(mCalendarViewCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onCalendarViewModeChanged(int))); // Connect top control signals connect(ui.prevBtn, SIGNAL(clicked()), this, SLOT(onPrevPeriod())); @@ -154,35 +182,63 @@ void CalendarWidget::buildUi() { } void CalendarWidget::refreshData() { - // Save current check states - QMap checkedStates; - for (int i = 0; i < mCalendarList->count(); ++i) { - QListWidgetItem* item = mCalendarList->item(i); - checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); - } - - // Populate Calendar selection list - mCalendarList->blockSignals(true); - mCalendarList->clear(); - const auto& cals = CalendarData::instance()->getCalendars(); - for (const auto& cal : cals) { - QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); - item->setData(Qt::UserRole, cal.id); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - - // Render colored bullet point icon - QPixmap pix(12, 12); - pix.fill(cal.color); - item->setIcon(QIcon(pix)); - - // Restore checked state - if (checkedStates.contains(cal.id)) { - item->setCheckState(checkedStates[cal.id]); - } else { - item->setCheckState(Qt::Checked); + if (mCalendarListMode == 0) { + // Save current check states + QMap checkedStates; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); } + + // Populate Calendar selection list + mCalendarList->blockSignals(true); + mCalendarList->clear(); + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); + item->setData(Qt::UserRole, cal.id); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + // Render colored bullet point icon + QPixmap pix(12, 12); + pix.fill(cal.color); + item->setIcon(QIcon(pix)); + + // Restore checked state + if (checkedStates.contains(cal.id)) { + item->setCheckState(checkedStates[cal.id]); + } else { + item->setCheckState(Qt::Checked); + } + } + mCalendarList->blockSignals(false); + } else { + // Shared Calendars mode + mCalendarList->blockSignals(true); + mCalendarList->clear(); + if (rsGxsCalendar) { + std::list calendars; + if (rsGxsCalendar->getCalendarsSummaries(calendars)) { + for (const auto& meta : calendars) { + QString calId = QString::fromStdString(meta.mGroupId.toStdString()); + QString calName = QString::fromUtf8(meta.mGroupName.c_str()); + bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); + + QListWidgetItem* item = new QListWidgetItem(calName, mCalendarList); + item->setData(Qt::UserRole, calId); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + // Render blue bullet for subscribed, grey for unsubscribed + QPixmap pix(12, 12); + pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); + item->setIcon(QIcon(pix)); + + item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + } + } + } + mCalendarList->blockSignals(false); } - mCalendarList->blockSignals(false); updateViews(); } @@ -524,8 +580,16 @@ void CalendarWidget::onEventSelected(int row, int col) { } } -void CalendarWidget::onCalendarSelectionChanged(QListWidgetItem* /*item*/) { - updateViews(); +void CalendarWidget::onCalendarSelectionChanged(QListWidgetItem* item) { + if (mCalendarListMode == 1) { + if (item) { + QString calId = item->data(Qt::UserRole).toString(); + bool subscribe = (item->checkState() == Qt::Checked); + CalendarData::instance()->subscribeToCalendar(calId, subscribe, item->text()); + } + } else { + updateViews(); + } } void CalendarWidget::onSearchChanged(const QString& text) { @@ -541,6 +605,16 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { QString calName = item->text(); bool isChecked = item->checkState() == Qt::Checked; + if (mCalendarListMode == 1) { + QMenu menu(this); + QAction* subAct = menu.addAction(isChecked ? tr("Unsubscribe") : tr("Subscribe")); + QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); + if (selectedAct == subAct) { + item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); + } + return; + } + QMenu menu(this); QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); @@ -551,7 +625,6 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); menu.addSeparator(); QAction* exportAct = menu.addAction(tr("Export Calendar...")); - QAction* publishAct = menu.addAction(tr("Publish Calendar...")); menu.addSeparator(); QAction* propertiesAct = menu.addAction(tr("Properties")); @@ -586,8 +659,6 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { } } else if (selectedAct == exportAct) { exportCalendar(calId, calName); - } else if (selectedAct == publishAct) { - QMessageBox::information(this, tr("Publish Calendar"), tr("Calendar '%1' published successfully!").arg(calName)); } else if (selectedAct == propertiesAct) { CalendarPropertiesDialog dlg(calId, this); if (dlg.exec() == QDialog::Accepted) { @@ -941,3 +1012,11 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& painter->restore(); } + +void CalendarWidget::onCalendarViewModeChanged(int index) { + mCalendarListMode = index; + if (index == 0) { + CalendarData::instance()->syncWithGxs(); + } + refreshData(); +} diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h index c80d48d0f..0e5334017 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -31,6 +31,7 @@ class QListWidgetItem; class QLabel; +class QComboBox; class CalendarWidget; class MonthCalendarDelegate : public QStyledItemDelegate { @@ -64,6 +65,7 @@ private slots: void onSearchChanged(const QString& text); void onCalendarContextMenu(const QPoint& pos); void onMonthCellClicked(int row, int col); + void onCalendarViewModeChanged(int index); private: void buildUi(); @@ -78,10 +80,16 @@ private: QDate mSelectedDate; int mCurrentViewMode; // 0=Day, 1=Week, 2=Month QString mSearchText; + int mCalendarListMode; // 0=My Calendars, 1=Shared Calendars + bool mInitialLoadDone; + +protected: + void showEvent(QShowEvent* event) override; // UI elements (now loaded from UI file but kept as pointers for compatibility) QCalendarWidget* mSidebarCalendar; QListWidget* mCalendarList; + QComboBox* mCalendarViewCombo; QLabel* mPeriodLabel; QLabel* mCwLabel; diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/msgs/TasksWidget.cpp index 0335488f9..c215d43b2 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.cpp +++ b/retroshare-gui/src/gui/msgs/TasksWidget.cpp @@ -21,11 +21,18 @@ #include "gui/msgs/TasksWidget.h" #include "gui/msgs/TaskDialog.h" #include "gui/msgs/CalendarPropertiesDialog.h" +#include +#include #include #include +#include #include #include #include +#include +#include +#include +#include #include #include #include @@ -37,17 +44,28 @@ #include #include #include +#include #include TasksWidget::TasksWidget(QWidget* parent) - : QWidget(parent), mCurrentFilterMode(0) + : QWidget(parent), mCurrentFilterMode(0), mCalendarListMode(0), mCalendarViewCombo(nullptr), mInitialLoadDone(false) { buildUi(); refreshData(); + + connect(CalendarData::instance(), SIGNAL(calendarDataChanged()), this, SLOT(refreshData())); } TasksWidget::~TasksWidget() {} +void TasksWidget::showEvent(QShowEvent* event) { + QWidget::showEvent(event); + if (!mInitialLoadDone) { + mInitialLoadDone = true; + CalendarData::instance()->syncWithGxs(); + } +} + void TasksWidget::buildUi() { ui.setupUi(this); @@ -81,6 +99,19 @@ void TasksWidget::buildUi() { mTaskTable->setSelectionMode(QAbstractItemView::SingleSelection); mTaskTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + // Hide calendarsLabel + ui.calendarsLabel->hide(); + + // Create and insert calendarViewCombo dynamically + mCalendarViewCombo = new QComboBox(this); + mCalendarViewCombo->setObjectName("calendarViewCombo"); + mCalendarViewCombo->addItems({tr("My Calendars"), tr("Shared Calendars")}); + mCalendarViewCombo->setStyleSheet("font-weight: bold; font-size: 13px; margin-bottom: 4px;"); + + int labelIndex = ui.sidebarLayout->indexOf(ui.calendarsLabel); + if (labelIndex == -1) labelIndex = 4; // Default fallback position + ui.sidebarLayout->insertWidget(labelIndex, mCalendarViewCombo); + // Splitter configuration ui.splitter->setStretchFactor(0, 0); ui.splitter->setStretchFactor(1, 1); @@ -94,37 +125,66 @@ void TasksWidget::buildUi() { connect(mSearchEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onSearchChanged(const QString&))); connect(mTaskTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onTaskDoubleClicked(int,int))); connect(mTaskTable, SIGNAL(cellClicked(int,int)), this, SLOT(onTaskClicked(int,int))); + connect(mCalendarViewCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onCalendarViewModeChanged(int))); } void TasksWidget::refreshData() { - // Save current check states - QMap checkedStates; - for (int i = 0; i < mCalendarList->count(); ++i) { - QListWidgetItem* item = mCalendarList->item(i); - checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); - } - - // Populate Calendar selection list - mCalendarList->blockSignals(true); - mCalendarList->clear(); - const auto& cals = CalendarData::instance()->getCalendars(); - for (const auto& cal : cals) { - QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); - item->setData(Qt::UserRole, cal.id); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - - QPixmap pix(12, 12); - pix.fill(cal.color); - item->setIcon(QIcon(pix)); - - // Restore checked state - if (checkedStates.contains(cal.id)) { - item->setCheckState(checkedStates[cal.id]); - } else { - item->setCheckState(Qt::Checked); + if (mCalendarListMode == 0) { + // Save current check states + QMap checkedStates; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); } + + // Populate Calendar selection list + mCalendarList->blockSignals(true); + mCalendarList->clear(); + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); + item->setData(Qt::UserRole, cal.id); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + QPixmap pix(12, 12); + pix.fill(cal.color); + item->setIcon(QIcon(pix)); + + // Restore checked state + if (checkedStates.contains(cal.id)) { + item->setCheckState(checkedStates[cal.id]); + } else { + item->setCheckState(Qt::Checked); + } + } + mCalendarList->blockSignals(false); + } else { + // Shared Calendars mode + mCalendarList->blockSignals(true); + mCalendarList->clear(); + if (rsGxsCalendar) { + std::list calendars; + if (rsGxsCalendar->getCalendarsSummaries(calendars)) { + for (const auto& meta : calendars) { + QString calId = QString::fromStdString(meta.mGroupId.toStdString()); + QString calName = QString::fromUtf8(meta.mGroupName.c_str()); + bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); + + QListWidgetItem* item = new QListWidgetItem(calName, mCalendarList); + item->setData(Qt::UserRole, calId); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + // Render blue bullet for subscribed, grey for unsubscribed + QPixmap pix(12, 12); + pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); + item->setIcon(QIcon(pix)); + + item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + } + } + } + mCalendarList->blockSignals(false); } - mCalendarList->blockSignals(false); updateTaskList(); } @@ -284,8 +344,16 @@ void TasksWidget::onFilterSelected(QListWidgetItem* item) { } } -void TasksWidget::onCalendarSelectionChanged(QListWidgetItem* /*item*/) { - updateTaskList(); +void TasksWidget::onCalendarSelectionChanged(QListWidgetItem* item) { + if (mCalendarListMode == 1) { + if (item) { + QString calId = item->data(Qt::UserRole).toString(); + bool subscribe = (item->checkState() == Qt::Checked); + CalendarData::instance()->subscribeToCalendar(calId, subscribe, item->text()); + } + } else { + updateTaskList(); + } } void TasksWidget::onSearchChanged(const QString& text) { @@ -301,6 +369,16 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { QString calName = item->text(); bool isChecked = item->checkState() == Qt::Checked; + if (mCalendarListMode == 1) { + QMenu menu(this); + QAction* subAct = menu.addAction(isChecked ? tr("Unsubscribe") : tr("Subscribe")); + QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); + if (selectedAct == subAct) { + item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); + } + return; + } + QMenu menu(this); QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); @@ -311,7 +389,6 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); menu.addSeparator(); QAction* exportAct = menu.addAction(tr("Export Calendar...")); - QAction* publishAct = menu.addAction(tr("Publish Calendar...")); menu.addSeparator(); QAction* propertiesAct = menu.addAction(tr("Properties")); @@ -348,9 +425,7 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { refreshData(); } } else if (selectedAct == exportAct) { - QMessageBox::information(this, tr("Export Calendar"), tr("Calendar '%1' exported successfully!").arg(calName)); - } else if (selectedAct == publishAct) { - QMessageBox::information(this, tr("Publish Calendar"), tr("Calendar '%1' published successfully!").arg(calName)); + exportCalendar(calId, calName); } else if (selectedAct == propertiesAct) { CalendarPropertiesDialog dlg(calId, this); if (dlg.exec() == QDialog::Accepted) { @@ -358,3 +433,49 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { } } } + +void TasksWidget::exportCalendar(const QString& calId, const QString& calName) { + QString icsContent = CalendarData::instance()->exportCalendarToIcs(calId); + + QString defaultFileName = QString("%1.ics").arg(calName); + defaultFileName.replace(QRegExp("[\\\\/:*?\"<>|]"), "_"); + + QString selectedFilter; + QString filePath = QFileDialog::getSaveFileName( + this, + tr("Export Calendar"), + defaultFileName, + tr("iCalendar files (*.ics);;All Files (*)"), + &selectedFilter + ); + + if (!filePath.isEmpty()) { + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical( + this, + tr("Export Error"), + tr("Could not open file %1 for writing.").arg(filePath) + ); + } else { + QTextStream out(&file); + out.setCodec("UTF-8"); + out << icsContent; + file.close(); + + QMessageBox::information( + this, + tr("Export Calendar"), + tr("Calendar '%1' exported successfully to %2!").arg(calName).arg(QDir::toNativeSeparators(filePath)) + ); + } + } +} + +void TasksWidget::onCalendarViewModeChanged(int index) { + mCalendarListMode = index; + if (index == 0) { + CalendarData::instance()->syncWithGxs(); + } + refreshData(); +} diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.h b/retroshare-gui/src/gui/msgs/TasksWidget.h index 6ae25abbf..1817324ee 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.h +++ b/retroshare-gui/src/gui/msgs/TasksWidget.h @@ -27,6 +27,7 @@ #include "ui_TasksWidget.h" class QListWidgetItem; +class QComboBox; class TasksWidget : public QWidget { Q_OBJECT @@ -45,18 +46,26 @@ private slots: void onCalendarSelectionChanged(QListWidgetItem* item); void onSearchChanged(const QString& text); void onCalendarContextMenu(const QPoint& pos); + void onCalendarViewModeChanged(int index); private: void buildUi(); void updateTaskList(); + void exportCalendar(const QString& calId, const QString& calName); int mCurrentFilterMode; // 0=All, 1=Active, 2=Completed, 3=Overdue QString mSearchText; + int mCalendarListMode; // 0=My Calendars, 1=Shared Calendars + bool mInitialLoadDone; + +protected: + void showEvent(QShowEvent* event) override; // UI elements (loaded from UI file, kept as pointers for compatibility) QCalendarWidget* mSidebarCalendar; QListWidget* mFilterList; QListWidget* mCalendarList; + QComboBox* mCalendarViewCombo; QLineEdit* mQuickTaskEdit; QLineEdit* mSearchEdit; From d36b1deb94a0f8a50974bf29bef32fd13557d549 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:34:11 +0200 Subject: [PATCH 05/26] changed eventType to use the dynamic way --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index 3603ab89d..f3eaf74ab 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -40,11 +40,13 @@ CalendarData::CalendarData() : QObject(), mEventHandlerId(0) { loadData(); if (rsEvents && rsGxsCalendar) { + RsEventType calendarEventType = (RsEventType)rsEvents->getDynamicEventType("GXS_CALENDAR"); + rsEvents->registerEventsHandler( [this](std::shared_ptr event) { RsQThreadUtils::postToObject([=]() { handleGxsEvent(event); }, this); }, - mEventHandlerId, RsEventType::GXS_CALENDAR + mEventHandlerId, calendarEventType ); } } From 9c0e877393190195b4a60e519b5c22e6c41d378b Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:13:43 +0200 Subject: [PATCH 06/26] Added own list for shared calendars *rename function --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 8 +- retroshare-gui/src/gui/msgs/CalendarData.h | 2 +- .../src/gui/msgs/CalendarWidget.cpp | 267 ++++++++++++++---- retroshare-gui/src/gui/msgs/CalendarWidget.h | 6 +- retroshare-gui/src/gui/msgs/CalendarWidget.ui | 35 ++- retroshare-gui/src/gui/msgs/TasksWidget.cpp | 158 +++++++---- retroshare-gui/src/gui/msgs/TasksWidget.h | 5 +- retroshare-gui/src/gui/msgs/TasksWidget.ui | 19 +- 8 files changed, 370 insertions(+), 130 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index f3eaf74ab..6d1b9794f 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -716,7 +716,7 @@ bool CalendarData::subscribeToCalendar(const QString& id, bool subscribe, const } emit calendarDataChanged(); // Trigger sync to fetch contents - syncWithGxs(); + updateCalendars(); } else { // Remove from local list for (int i = 0; i < mCalendars.size(); ++i) { @@ -737,7 +737,7 @@ bool CalendarData::subscribeToCalendar(const QString& id, bool subscribe, const return true; } -void CalendarData::syncWithGxs() { +void CalendarData::updateCalendars() { if (!rsGxsCalendar) return; std::list calendars; @@ -805,11 +805,11 @@ void CalendarData::handleGxsEvent(std::shared_ptr event) { switch (e->mCalendarEventCode) { case RsCalendarEventCode::NEW_CALENDAR: case RsCalendarEventCode::UPDATED_CALENDAR: - syncWithGxs(); + updateCalendars(); case RsCalendarEventCode::NEW_EVENT: case RsCalendarEventCode::UPDATED_EVENT: case RsCalendarEventCode::SUBSCRIBE_STATUS_CHANGED: - syncWithGxs(); + updateCalendars(); break; default: break; diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/msgs/CalendarData.h index 51405728f..dcf48cd65 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.h +++ b/retroshare-gui/src/gui/msgs/CalendarData.h @@ -114,7 +114,7 @@ signals: void calendarDataChanged(); public slots: - void syncWithGxs(); + void updateCalendars(); private slots: void handleGxsEvent(std::shared_ptr event); diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index 2ae14086c..03b7a7d73 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -23,6 +23,7 @@ #include "gui/msgs/CalendarPropertiesDialog.h" #include #include +#include "retroshare/rsgxsflags.h" #include #include #include @@ -51,7 +52,7 @@ #include CalendarWidget::CalendarWidget(QWidget* parent) - : QWidget(parent), mSelectedDate(QDate::currentDate()), mCurrentViewMode(2), mCalendarListMode(0), mCalendarViewCombo(nullptr), mInitialLoadDone(false) + : QWidget(parent), mSelectedDate(QDate::currentDate()), mCurrentViewMode(2), mInitialLoadDone(false) { buildUi(); refreshData(); @@ -65,7 +66,7 @@ void CalendarWidget::showEvent(QShowEvent* event) { QWidget::showEvent(event); if (!mInitialLoadDone) { mInitialLoadDone = true; - CalendarData::instance()->syncWithGxs(); + CalendarData::instance()->updateCalendars(); } } @@ -75,6 +76,7 @@ void CalendarWidget::buildUi() { // Initialize UI pointers mSidebarCalendar = ui.sidebarCalendar; mCalendarList = ui.calendarList; + mSharedCalendarList = ui.sharedCalendarList; mPeriodLabel = ui.periodLabel; mSearchEdit = ui.searchEdit; mEventTable = ui.eventTable; @@ -91,19 +93,6 @@ void CalendarWidget::buildUi() { if (btnIndex == -1) btnIndex = 6; ui.topControlLayout->insertWidget(btnIndex, mCwLabel); - // Hide calendarsLabel - ui.calendarsLabel->hide(); - - // Create and insert calendarViewCombo dynamically - mCalendarViewCombo = new QComboBox(this); - mCalendarViewCombo->setObjectName("calendarViewCombo"); - mCalendarViewCombo->addItems({tr("My Calendars"), tr("Shared Calendars")}); - mCalendarViewCombo->setStyleSheet("font-weight: bold; font-size: 13px; margin-bottom: 4px;"); - - int labelIndex = ui.sidebarLayout->indexOf(ui.calendarsLabel); - if (labelIndex == -1) labelIndex = 2; // Default fallback position - ui.sidebarLayout->insertWidget(labelIndex, mCalendarViewCombo); - // Sidebar Calendar configs mSidebarCalendar->setSelectedDate(mSelectedDate); @@ -116,6 +105,8 @@ void CalendarWidget::buildUi() { mEventTable->setSelectionMode(QAbstractItemView::SingleSelection); mEventTable->setEditTriggers(QAbstractItemView::NoEditTriggers); connect(mEventTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + mEventTable->setContextMenuPolicy(Qt::CustomContextMenu); + connect(mEventTable, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onEventTableContextMenu(const QPoint&))); // Stacked widget pages setup // 1. Day Table @@ -157,8 +148,9 @@ void CalendarWidget::buildUi() { connect(mSidebarCalendar, SIGNAL(clicked(const QDate&)), this, SLOT(onDateSelected(const QDate&))); connect(mCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCalendarSelectionChanged(QListWidgetItem*))); connect(mCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onCalendarContextMenu(const QPoint&))); + connect(mSharedCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onSharedCalendarSelectionChanged(QListWidgetItem*))); + connect(mSharedCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onSharedCalendarContextMenu(const QPoint&))); connect(ui.newCalBtn, SIGNAL(clicked()), this, SLOT(onNewCalendar())); - connect(mCalendarViewCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onCalendarViewModeChanged(int))); // Connect top control signals connect(ui.prevBtn, SIGNAL(clicked()), this, SLOT(onPrevPeriod())); @@ -182,7 +174,10 @@ void CalendarWidget::buildUi() { } void CalendarWidget::refreshData() { - if (mCalendarListMode == 0) { + const auto& cals = CalendarData::instance()->getCalendars(); + + // 1. Populate My Calendars (owned by us, i.e. owner == "local") + { // Save current check states QMap checkedStates; for (int i = 0; i < mCalendarList->count(); ++i) { @@ -190,11 +185,11 @@ void CalendarWidget::refreshData() { checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); } - // Populate Calendar selection list mCalendarList->blockSignals(true); mCalendarList->clear(); - const auto& cals = CalendarData::instance()->getCalendars(); for (const auto& cal : cals) { + if (cal.owner != "local") continue; + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); item->setData(Qt::UserRole, cal.id); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); @@ -212,19 +207,39 @@ void CalendarWidget::refreshData() { } } mCalendarList->blockSignals(false); - } else { - // Shared Calendars mode - mCalendarList->blockSignals(true); - mCalendarList->clear(); + } + + // 2. Populate Shared Calendars (not owned by us) + { + // Save current check states + QMap sharedCheckedStates; + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + sharedCheckedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + } + + mSharedCalendarList->blockSignals(true); + mSharedCalendarList->clear(); if (rsGxsCalendar) { std::list calendars; if (rsGxsCalendar->getCalendarsSummaries(calendars)) { for (const auto& meta : calendars) { QString calId = QString::fromStdString(meta.mGroupId.toStdString()); + + // Filter out calendars owned by us + bool ownedByUs = false; + for (const auto& c : cals) { + if (c.id == calId && c.owner == "local") { + ownedByUs = true; + break; + } + } + if (ownedByUs) continue; + QString calName = QString::fromUtf8(meta.mGroupName.c_str()); bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); - QListWidgetItem* item = new QListWidgetItem(calName, mCalendarList); + QListWidgetItem* item = new QListWidgetItem(calName, mSharedCalendarList); item->setData(Qt::UserRole, calId); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); @@ -233,11 +248,17 @@ void CalendarWidget::refreshData() { pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); item->setIcon(QIcon(pix)); - item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + // Restore checked state if we have a saved state, + // otherwise default to Checked if subscribed, Unchecked if unsubscribed + if (sharedCheckedStates.contains(calId)) { + item->setCheckState(sharedCheckedStates[calId]); + } else { + item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + } } } } - mCalendarList->blockSignals(false); + mSharedCalendarList->blockSignals(false); } updateViews(); @@ -289,6 +310,7 @@ void CalendarWidget::updateViews() { } else { updateMonthView(); } + } void CalendarWidget::updateEventList() { @@ -305,6 +327,12 @@ void CalendarWidget::updateEventList() { enabledCalIds.append(item->data(Qt::UserRole).toString()); } } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + if (item->checkState() == Qt::Checked) { + enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + } int row = 0; for (const auto& ev : events) { @@ -351,6 +379,10 @@ void CalendarWidget::updateDayView() { QListWidgetItem* item = mCalendarList->item(i); if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } for (int hour = 0; hour < 24; ++hour) { QString timeText = QString("%1:00").arg(hour, 2, 10, QChar('0')); @@ -397,6 +429,10 @@ void CalendarWidget::updateWeekView() { QListWidgetItem* item = mCalendarList->item(i); if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } // Populate week cells for (int dayIdx = 0; dayIdx < 7; ++dayIdx) { @@ -437,6 +473,10 @@ void CalendarWidget::updateMonthView() { QListWidgetItem* item = mCalendarList->item(i); if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } for (int row = 0; row < rowsNeeded; ++row) { for (int col = 0; col < 7; ++col) { @@ -555,6 +595,40 @@ void CalendarWidget::onEventSelected(int row, int col) { // If double clicked a cell/row containing an event, edit it. Otherwise create a new one. if (!eventId.isEmpty()) { + // Check if user can edit this event (admin check for shared calendars) + QString calendarId; + const auto& events = CalendarData::instance()->getEvents(); + for (const auto& ev : events) { + if (ev.id == eventId) { + calendarId = ev.calendarId; + break; + } + } + + bool canEdit = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calendarId) { + if (!c.onNetwork) { + canEdit = true; + } else if (rsGxsCalendar) { + std::list summaries; + if (rsGxsCalendar->getCalendarsSummaries(summaries)) { + RsGxsGroupId groupId(calendarId.toStdString()); + for (const auto& meta : summaries) { + if (meta.mGroupId == groupId) { + canEdit = IS_GROUP_ADMIN(meta.mSubscribeFlags); + break; + } + } + } + } + break; + } + } + + if (!canEdit) return; + EventDialog dlg(eventId, QDateTime::currentDateTime(), this); if (dlg.exec() == QDialog::Accepted) { refreshData(); @@ -581,15 +655,11 @@ void CalendarWidget::onEventSelected(int row, int col) { } void CalendarWidget::onCalendarSelectionChanged(QListWidgetItem* item) { - if (mCalendarListMode == 1) { - if (item) { - QString calId = item->data(Qt::UserRole).toString(); - bool subscribe = (item->checkState() == Qt::Checked); - CalendarData::instance()->subscribeToCalendar(calId, subscribe, item->text()); - } - } else { - updateViews(); - } + updateViews(); +} + +void CalendarWidget::onSharedCalendarSelectionChanged(QListWidgetItem* item) { + updateViews(); } void CalendarWidget::onSearchChanged(const QString& text) { @@ -605,24 +675,29 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { QString calName = item->text(); bool isChecked = item->checkState() == Qt::Checked; - if (mCalendarListMode == 1) { - QMenu menu(this); - QAction* subAct = menu.addAction(isChecked ? tr("Unsubscribe") : tr("Subscribe")); - QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); - if (selectedAct == subAct) { - item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); - } - return; - } - QMenu menu(this); QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); QAction* showOnlyAct = menu.addAction(tr("Show Only %1").arg(calName)); QAction* showAllAct = menu.addAction(tr("Show All Calendars")); - menu.addSeparator(); - QAction* newAct = menu.addAction(tr("New Calendar...")); - QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); + + // Check if selected calendar is a shared/network calendar + bool isSharedCal = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calId) { + isSharedCal = (c.owner != "local"); + break; + } + } + + QAction* newAct = nullptr; + QAction* deleteAct = nullptr; + if (!isSharedCal) { + menu.addSeparator(); + newAct = menu.addAction(tr("New Calendar...")); + deleteAct = menu.addAction(tr("Delete Calendar...")); + } menu.addSeparator(); QAction* exportAct = menu.addAction(tr("Export Calendar...")); menu.addSeparator(); @@ -635,11 +710,16 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); } else if (selectedAct == showOnlyAct) { mCalendarList->blockSignals(true); + mSharedCalendarList->blockSignals(true); for (int i = 0; i < mCalendarList->count(); ++i) { QListWidgetItem* it = mCalendarList->item(i); it->setCheckState(it == item ? Qt::Checked : Qt::Unchecked); } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + mSharedCalendarList->item(i)->setCheckState(Qt::Unchecked); + } mCalendarList->blockSignals(false); + mSharedCalendarList->blockSignals(false); updateViews(); } else if (selectedAct == showAllAct) { mCalendarList->blockSignals(true); @@ -667,6 +747,90 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { } } +void CalendarWidget::onSharedCalendarContextMenu(const QPoint& pos) { + QListWidgetItem* item = mSharedCalendarList->itemAt(pos); + if (!item) return; + + QString calId = item->data(Qt::UserRole).toString(); + QString calName = item->text(); + + bool isSubscribed = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calId) { + isSubscribed = true; + break; + } + } + + QMenu menu(this); + QAction* subAct = menu.addAction(isSubscribed ? tr("Unsubscribe") : tr("Subscribe")); + QAction* selectedAct = menu.exec(mSharedCalendarList->mapToGlobal(pos)); + if (selectedAct == subAct) { + CalendarData::instance()->subscribeToCalendar(calId, !isSubscribed, calName); + } +} + +void CalendarWidget::onEventTableContextMenu(const QPoint& pos) { + QTableWidgetItem* titleItem = mEventTable->itemAt(pos); + if (!titleItem) return; + + int row = titleItem->row(); + QTableWidgetItem* firstColItem = mEventTable->item(row, 0); + if (!firstColItem) return; + + QString eventId = firstColItem->data(Qt::UserRole).toString(); + if (eventId.isEmpty()) return; + + // Find the event and its calendar + QString calendarId; + const auto& events = CalendarData::instance()->getEvents(); + for (const auto& ev : events) { + if (ev.id == eventId) { + calendarId = ev.calendarId; + break; + } + } + if (calendarId.isEmpty()) return; + + // Determine if user can edit: local calendars are always editable, + // network calendars require admin (owner) status on the GXS group. + bool canEdit = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calendarId) { + if (!c.onNetwork) { + canEdit = true; // Local calendar — always editable + } else if (rsGxsCalendar) { + // Check GXS admin flag + std::list summaries; + if (rsGxsCalendar->getCalendarsSummaries(summaries)) { + RsGxsGroupId groupId(calendarId.toStdString()); + for (const auto& meta : summaries) { + if (meta.mGroupId == groupId) { + canEdit = IS_GROUP_ADMIN(meta.mSubscribeFlags); + break; + } + } + } + } + break; + } + } + + QMenu menu(this); + QAction* editAct = menu.addAction(tr("Edit Event")); + editAct->setEnabled(canEdit); + + QAction* selectedAct = menu.exec(mEventTable->viewport()->mapToGlobal(pos)); + if (selectedAct == editAct && canEdit) { + EventDialog dlg(eventId, QDateTime::currentDateTime(), this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } +} + void CalendarWidget::exportCalendar(const QString& calId, const QString& calName) { QString icsContent = "BEGIN:VCALENDAR\r\n" "VERSION:2.0\r\n" @@ -1013,10 +1177,3 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& painter->restore(); } -void CalendarWidget::onCalendarViewModeChanged(int index) { - mCalendarListMode = index; - if (index == 0) { - CalendarData::instance()->syncWithGxs(); - } - refreshData(); -} diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h index 0e5334017..326d2ae62 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -62,10 +62,12 @@ private slots: void onDateSelected(const QDate& date); void onEventSelected(int row, int col); void onCalendarSelectionChanged(QListWidgetItem* item); + void onSharedCalendarSelectionChanged(QListWidgetItem* item); void onSearchChanged(const QString& text); void onCalendarContextMenu(const QPoint& pos); + void onSharedCalendarContextMenu(const QPoint& pos); + void onEventTableContextMenu(const QPoint& pos); void onMonthCellClicked(int row, int col); - void onCalendarViewModeChanged(int index); private: void buildUi(); @@ -89,7 +91,7 @@ protected: // UI elements (now loaded from UI file but kept as pointers for compatibility) QCalendarWidget* mSidebarCalendar; QListWidget* mCalendarList; - QComboBox* mCalendarViewCombo; + QListWidget* mSharedCalendarList; QLabel* mPeriodLabel; QLabel* mCwLabel; diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.ui b/retroshare-gui/src/gui/msgs/CalendarWidget.ui index d99c3d9c7..053da1b99 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.ui +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.ui @@ -6,7 +6,7 @@ 0 0 - 800 + 833 600 @@ -80,7 +80,7 @@ font-weight: bold; font-size: 14px; - Calendars + My Calendars @@ -91,6 +91,23 @@ + + + + font-weight: bold; font-size: 14px; margin-top: 10px; + + + Shared Calendars + + + + + + + Qt::CustomContextMenu + + + @@ -199,17 +216,17 @@ - Qt::Orientation::Vertical + Qt::Vertical - QAbstractItemView::EditTrigger::NoEditTriggers + QAbstractItemView::NoEditTriggers - QAbstractItemView::SelectionMode::SingleSelection + QAbstractItemView::NoSelection - QAbstractItemView::SelectionBehavior::SelectRows + QAbstractItemView::SelectItems @@ -233,7 +250,7 @@ - QAbstractItemView::EditTrigger::NoEditTriggers + QAbstractItemView::NoEditTriggers @@ -259,7 +276,7 @@ - QAbstractItemView::EditTrigger::NoEditTriggers + QAbstractItemView::NoEditTriggers @@ -285,7 +302,7 @@ - QAbstractItemView::EditTrigger::NoEditTriggers + QAbstractItemView::NoEditTriggers diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/msgs/TasksWidget.cpp index c215d43b2..82ff328ad 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.cpp +++ b/retroshare-gui/src/gui/msgs/TasksWidget.cpp @@ -48,7 +48,7 @@ #include TasksWidget::TasksWidget(QWidget* parent) - : QWidget(parent), mCurrentFilterMode(0), mCalendarListMode(0), mCalendarViewCombo(nullptr), mInitialLoadDone(false) + : QWidget(parent), mCurrentFilterMode(0), mInitialLoadDone(false) { buildUi(); refreshData(); @@ -62,7 +62,7 @@ void TasksWidget::showEvent(QShowEvent* event) { QWidget::showEvent(event); if (!mInitialLoadDone) { mInitialLoadDone = true; - CalendarData::instance()->syncWithGxs(); + CalendarData::instance()->updateCalendars(); } } @@ -73,6 +73,7 @@ void TasksWidget::buildUi() { mSidebarCalendar = ui.sidebarCalendar; mFilterList = ui.filterList; mCalendarList = ui.calendarList; + mSharedCalendarList = ui.sharedCalendarList; mQuickTaskEdit = ui.quickTaskEdit; mSearchEdit = ui.searchEdit; mTaskTable = ui.taskTable; @@ -99,19 +100,6 @@ void TasksWidget::buildUi() { mTaskTable->setSelectionMode(QAbstractItemView::SingleSelection); mTaskTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - // Hide calendarsLabel - ui.calendarsLabel->hide(); - - // Create and insert calendarViewCombo dynamically - mCalendarViewCombo = new QComboBox(this); - mCalendarViewCombo->setObjectName("calendarViewCombo"); - mCalendarViewCombo->addItems({tr("My Calendars"), tr("Shared Calendars")}); - mCalendarViewCombo->setStyleSheet("font-weight: bold; font-size: 13px; margin-bottom: 4px;"); - - int labelIndex = ui.sidebarLayout->indexOf(ui.calendarsLabel); - if (labelIndex == -1) labelIndex = 4; // Default fallback position - ui.sidebarLayout->insertWidget(labelIndex, mCalendarViewCombo); - // Splitter configuration ui.splitter->setStretchFactor(0, 0); ui.splitter->setStretchFactor(1, 1); @@ -121,15 +109,19 @@ void TasksWidget::buildUi() { connect(mFilterList, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(onFilterSelected(QListWidgetItem*))); connect(mCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCalendarSelectionChanged(QListWidgetItem*))); connect(mCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onCalendarContextMenu(const QPoint&))); + connect(mSharedCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onSharedCalendarSelectionChanged(QListWidgetItem*))); + connect(mSharedCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onSharedCalendarContextMenu(const QPoint&))); connect(mQuickTaskEdit, SIGNAL(returnPressed()), this, SLOT(onQuickTaskAdded())); connect(mSearchEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onSearchChanged(const QString&))); connect(mTaskTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onTaskDoubleClicked(int,int))); connect(mTaskTable, SIGNAL(cellClicked(int,int)), this, SLOT(onTaskClicked(int,int))); - connect(mCalendarViewCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onCalendarViewModeChanged(int))); } void TasksWidget::refreshData() { - if (mCalendarListMode == 0) { + const auto& cals = CalendarData::instance()->getCalendars(); + + // 1. Populate My Calendars (owned by us, i.e. owner == "local") + { // Save current check states QMap checkedStates; for (int i = 0; i < mCalendarList->count(); ++i) { @@ -137,11 +129,11 @@ void TasksWidget::refreshData() { checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); } - // Populate Calendar selection list mCalendarList->blockSignals(true); mCalendarList->clear(); - const auto& cals = CalendarData::instance()->getCalendars(); for (const auto& cal : cals) { + if (cal.owner != "local") continue; + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); item->setData(Qt::UserRole, cal.id); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); @@ -158,19 +150,39 @@ void TasksWidget::refreshData() { } } mCalendarList->blockSignals(false); - } else { - // Shared Calendars mode - mCalendarList->blockSignals(true); - mCalendarList->clear(); + } + + // 2. Populate Shared Calendars (not owned by us) + { + // Save current check states + QMap sharedCheckedStates; + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + sharedCheckedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + } + + mSharedCalendarList->blockSignals(true); + mSharedCalendarList->clear(); if (rsGxsCalendar) { std::list calendars; if (rsGxsCalendar->getCalendarsSummaries(calendars)) { for (const auto& meta : calendars) { QString calId = QString::fromStdString(meta.mGroupId.toStdString()); + + // Filter out calendars owned by us + bool ownedByUs = false; + for (const auto& c : cals) { + if (c.id == calId && c.owner == "local") { + ownedByUs = true; + break; + } + } + if (ownedByUs) continue; + QString calName = QString::fromUtf8(meta.mGroupName.c_str()); bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); - QListWidgetItem* item = new QListWidgetItem(calName, mCalendarList); + QListWidgetItem* item = new QListWidgetItem(calName, mSharedCalendarList); item->setData(Qt::UserRole, calId); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); @@ -179,11 +191,17 @@ void TasksWidget::refreshData() { pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); item->setIcon(QIcon(pix)); - item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + // Restore checked state if we have a saved state, + // otherwise default to Checked if subscribed, Unchecked if unsubscribed + if (sharedCheckedStates.contains(calId)) { + item->setCheckState(sharedCheckedStates[calId]); + } else { + item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + } } } } - mCalendarList->blockSignals(false); + mSharedCalendarList->blockSignals(false); } updateTaskList(); @@ -202,6 +220,12 @@ void TasksWidget::updateTaskList() { enabledCalIds.append(item->data(Qt::UserRole).toString()); } } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + if (item->checkState() == Qt::Checked) { + enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + } int row = 0; QDateTime now = QDateTime::currentDateTime(); @@ -345,15 +369,11 @@ void TasksWidget::onFilterSelected(QListWidgetItem* item) { } void TasksWidget::onCalendarSelectionChanged(QListWidgetItem* item) { - if (mCalendarListMode == 1) { - if (item) { - QString calId = item->data(Qt::UserRole).toString(); - bool subscribe = (item->checkState() == Qt::Checked); - CalendarData::instance()->subscribeToCalendar(calId, subscribe, item->text()); - } - } else { - updateTaskList(); - } + updateTaskList(); +} + +void TasksWidget::onSharedCalendarSelectionChanged(QListWidgetItem* item) { + updateTaskList(); } void TasksWidget::onSearchChanged(const QString& text) { @@ -369,24 +389,29 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { QString calName = item->text(); bool isChecked = item->checkState() == Qt::Checked; - if (mCalendarListMode == 1) { - QMenu menu(this); - QAction* subAct = menu.addAction(isChecked ? tr("Unsubscribe") : tr("Subscribe")); - QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); - if (selectedAct == subAct) { - item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); - } - return; - } - QMenu menu(this); QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); QAction* showOnlyAct = menu.addAction(tr("Show Only %1").arg(calName)); QAction* showAllAct = menu.addAction(tr("Show All Calendars")); - menu.addSeparator(); - QAction* newAct = menu.addAction(tr("New Calendar...")); - QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); + + // Check if selected calendar is a shared/network calendar + bool isSharedCal = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calId) { + isSharedCal = (c.owner != "local"); + break; + } + } + + QAction* newAct = nullptr; + QAction* deleteAct = nullptr; + if (!isSharedCal) { + menu.addSeparator(); + newAct = menu.addAction(tr("New Calendar...")); + deleteAct = menu.addAction(tr("Delete Calendar...")); + } menu.addSeparator(); QAction* exportAct = menu.addAction(tr("Export Calendar...")); menu.addSeparator(); @@ -399,11 +424,16 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); } else if (selectedAct == showOnlyAct) { mCalendarList->blockSignals(true); + mSharedCalendarList->blockSignals(true); for (int i = 0; i < mCalendarList->count(); ++i) { QListWidgetItem* it = mCalendarList->item(i); it->setCheckState(it == item ? Qt::Checked : Qt::Unchecked); } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + mSharedCalendarList->item(i)->setCheckState(Qt::Unchecked); + } mCalendarList->blockSignals(false); + mSharedCalendarList->blockSignals(false); updateTaskList(); } else if (selectedAct == showAllAct) { mCalendarList->blockSignals(true); @@ -434,6 +464,30 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { } } +void TasksWidget::onSharedCalendarContextMenu(const QPoint& pos) { + QListWidgetItem* item = mSharedCalendarList->itemAt(pos); + if (!item) return; + + QString calId = item->data(Qt::UserRole).toString(); + QString calName = item->text(); + + bool isSubscribed = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calId) { + isSubscribed = true; + break; + } + } + + QMenu menu(this); + QAction* subAct = menu.addAction(isSubscribed ? tr("Unsubscribe") : tr("Subscribe")); + QAction* selectedAct = menu.exec(mSharedCalendarList->mapToGlobal(pos)); + if (selectedAct == subAct) { + CalendarData::instance()->subscribeToCalendar(calId, !isSubscribed, calName); + } +} + void TasksWidget::exportCalendar(const QString& calId, const QString& calName) { QString icsContent = CalendarData::instance()->exportCalendarToIcs(calId); @@ -471,11 +525,3 @@ void TasksWidget::exportCalendar(const QString& calId, const QString& calName) { } } } - -void TasksWidget::onCalendarViewModeChanged(int index) { - mCalendarListMode = index; - if (index == 0) { - CalendarData::instance()->syncWithGxs(); - } - refreshData(); -} diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.h b/retroshare-gui/src/gui/msgs/TasksWidget.h index 1817324ee..630dd8660 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.h +++ b/retroshare-gui/src/gui/msgs/TasksWidget.h @@ -44,9 +44,10 @@ private slots: void onTaskClicked(int row, int col); void onFilterSelected(QListWidgetItem* item); void onCalendarSelectionChanged(QListWidgetItem* item); + void onSharedCalendarSelectionChanged(QListWidgetItem* item); void onSearchChanged(const QString& text); void onCalendarContextMenu(const QPoint& pos); - void onCalendarViewModeChanged(int index); + void onSharedCalendarContextMenu(const QPoint& pos); private: void buildUi(); @@ -65,7 +66,7 @@ protected: QCalendarWidget* mSidebarCalendar; QListWidget* mFilterList; QListWidget* mCalendarList; - QComboBox* mCalendarViewCombo; + QListWidget* mSharedCalendarList; QLineEdit* mQuickTaskEdit; QLineEdit* mSearchEdit; diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.ui b/retroshare-gui/src/gui/msgs/TasksWidget.ui index 452043cf7..cd5d0a8bb 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.ui +++ b/retroshare-gui/src/gui/msgs/TasksWidget.ui @@ -93,7 +93,7 @@ font-weight: bold; font-size: 14px; - Calendars + My Calendars @@ -104,6 +104,23 @@ + + + + font-weight: bold; font-size: 14px; margin-top: 10px; + + + Shared Calendars + + + + + + + Qt::CustomContextMenu + + + From 0056e538d29d1e51ed95e8b5c774a4ce1cb6e25e Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 7 Jun 2026 23:45:44 +0200 Subject: [PATCH 07/26] Added color change Improve calendar creator --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 32 ++- retroshare-gui/src/gui/msgs/CalendarData.h | 6 + .../src/gui/msgs/CalendarPropertiesDialog.cpp | 228 ++++++++++++++---- .../src/gui/msgs/CalendarPropertiesDialog.h | 24 +- .../src/gui/msgs/CalendarWidget.cpp | 172 +++++++++++-- 5 files changed, 385 insertions(+), 77 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index 6d1b9794f..4eecadd48 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -81,6 +82,11 @@ void CalendarData::loadData() { cal.showReminders = settings.value("showReminders", true).toBool(); cal.email = settings.value("email", "").toString(); cal.onNetwork = settings.value("onNetwork", false).toBool(); + cal.circleType = settings.value("circleType", 1).toUInt(); + cal.circleId = settings.value("circleId", "").toString(); + cal.internalCircle = settings.value("internalCircle", "").toString(); + cal.groupFlags = settings.value("groupFlags", 4).toUInt(); // Default FLAG_PRIVACY_PUBLIC (4) + cal.description = settings.value("description", "").toString(); mCalendars.append(cal); } settings.endArray(); @@ -175,6 +181,11 @@ void CalendarData::saveData() { settings.setValue("showReminders", mCalendars[i].showReminders); settings.setValue("email", mCalendars[i].email); settings.setValue("onNetwork", mCalendars[i].onNetwork); + settings.setValue("circleType", mCalendars[i].circleType); + settings.setValue("circleId", mCalendars[i].circleId); + settings.setValue("internalCircle", mCalendars[i].internalCircle); + settings.setValue("groupFlags", mCalendars[i].groupFlags); + settings.setValue("description", mCalendars[i].description); } settings.endArray(); @@ -657,7 +668,7 @@ bool CalendarData::publishCalendar(const QString& oldId, const QString& email, Q RsGxsGroupId groupId; std::string errMsg; - if (!rsGxsCalendar->createCalendar(cal.name.toStdString(), "RetroShare Calendar", authorId, groupId, errMsg)) { + if (!rsGxsCalendar->createCalendar(cal.name.toStdString(), "RetroShare Calendar", authorId, cal.circleType, RsGxsCircleId(cal.circleId.toStdString()), RsGxsCircleId(cal.internalCircle.toStdString()), cal.groupFlags, groupId, errMsg)) { return false; } @@ -711,6 +722,11 @@ bool CalendarData::subscribeToCalendar(const QString& id, bool subscribe, const localCal.onNetwork = true; localCal.showReminders = true; localCal.owner = "network"; + localCal.circleType = 1; + localCal.circleId = ""; + localCal.internalCircle = ""; + localCal.groupFlags = 4; + localCal.description = ""; mCalendars.append(localCal); saveData(); } @@ -766,8 +782,21 @@ void CalendarData::updateCalendars() { localCal.onNetwork = true; localCal.showReminders = true; localCal.owner = "network"; + localCal.circleType = meta.mCircleType; + localCal.circleId = QString::fromStdString(meta.mCircleId.toStdString()); + localCal.internalCircle = QString::fromStdString(meta.mInternalCircle.toStdString()); + localCal.groupFlags = meta.mGroupFlags; + localCal.description = ""; mCalendars.append(localCal); changed = true; + } else { + QString remoteName = QString::fromUtf8(meta.mGroupName.c_str()); + for (auto& c : mCalendars) { + if (c.id == calId && c.name != remoteName) { + c.name = remoteName; + changed = true; + } + } } std::vector messages; @@ -805,7 +834,6 @@ void CalendarData::handleGxsEvent(std::shared_ptr event) { switch (e->mCalendarEventCode) { case RsCalendarEventCode::NEW_CALENDAR: case RsCalendarEventCode::UPDATED_CALENDAR: - updateCalendars(); case RsCalendarEventCode::NEW_EVENT: case RsCalendarEventCode::UPDATED_EVENT: case RsCalendarEventCode::SUBSCRIBE_STATUS_CHANGED: diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/msgs/CalendarData.h index dcf48cd65..d76e5637a 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.h +++ b/retroshare-gui/src/gui/msgs/CalendarData.h @@ -40,6 +40,12 @@ struct CalendarInfo { bool showReminders; QString email; bool onNetwork; + + uint32_t circleType; + QString circleId; + QString internalCircle; + uint32_t groupFlags; + QString description; }; struct CalendarEvent { diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp index 0c0a24615..62eb3f158 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp @@ -35,12 +35,24 @@ #include #include #include +#include "gui/gxs/GxsIdChooser.h" +#include "gui/gxs/GxsCircleChooser.h" +#include "gui/common/GroupChooser.h" +#include +#include +#include CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget* parent) : QDialog(parent), mCalId(calId), mEditMode(!calId.isEmpty()), mSelectedColor(QColor("#4a90e2")) { setupUi(); - loadIdentities(); + + // Default initialization for GXS choosers + mIdChooser->loadIds(0, RsGxsId()); + mCircleCombo->loadCircles(RsGxsCircleId()); + mLocalCombo->loadGroups(0, RsNodeGroupId()); + mRadioPublic->setChecked(true); + updateCircleOptions(); if (mEditMode) { setWindowTitle(tr("Calendar Properties")); @@ -58,21 +70,42 @@ CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget if (found) { mNameEdit->setText(existingCal.name); mSelectedColor = existingCal.color; - mRemindersCheckBox->setChecked(existingCal.showReminders); mRadioNetwork->setChecked(existingCal.onNetwork); mRadioComputer->setChecked(!existingCal.onNetwork); - - // Try to find the email in the combo box - int idx = mEmailCombo->findText(existingCal.email); - if (idx != -1) { - mEmailCombo->setCurrentIndex(idx); - } else if (!existingCal.email.isEmpty()) { - mEmailCombo->addItem(existingCal.email); - mEmailCombo->setCurrentIndex(mEmailCombo->count() - 1); + + // GXS fields loading if calendar is on network + RsGxsId authorId; + int idxGxs = existingCal.email.lastIndexOf('@'); + int endIdxGxs = existingCal.email.lastIndexOf('>'); + if (idxGxs != -1 && endIdxGxs != -1 && endIdxGxs > idxGxs) { + std::string gxsIdStr = existingCal.email.mid(idxGxs + 1, endIdxGxs - idxGxs - 1).toStdString(); + authorId = RsGxsId(gxsIdStr); } + mIdChooser->loadIds(0, authorId); + + mDescEdit->setPlainText(existingCal.description); + + if (existingCal.circleType == GXS_CIRCLE_TYPE_PUBLIC) { + mRadioPublic->setChecked(true); + } else if (existingCal.circleType == GXS_CIRCLE_TYPE_EXTERNAL) { + mRadioCircle->setChecked(true); + } else if (existingCal.circleType == GXS_CIRCLE_TYPE_YOUR_FRIENDS_ONLY) { + mRadioNodeGroup->setChecked(true); + } else { + mRadioPublic->setChecked(true); + } + + RsGxsCircleId cid(existingCal.circleId.toStdString()); + mCircleCombo->loadCircles(cid); + + RsNodeGroupId ngi(existingCal.internalCircle.toStdString()); + mLocalCombo->loadGroups(0, ngi); + + updateCircleOptions(); } updateColorButton(); mStackedWidget->setCurrentWidget(mPage2); + updatePage2Layout(); } else { setWindowTitle(tr("Create New Calendar")); mStackedWidget->setCurrentWidget(mPage1); @@ -99,7 +132,7 @@ void CalendarPropertiesDialog::setupUi() { page1Layout->setSpacing(15); QLabel* descLabel = new QLabel( - tr("Your calendar can be stored on your computer or be stored on a server in order to access it remotely or share it with your friends or co-workers."), + tr("Your calendar can be stored on your computer or share it with your friends or co-workers."), mPage1 ); descLabel->setWordWrap(true); @@ -128,30 +161,61 @@ void CalendarPropertiesDialog::setupUi() { page2Layout->setContentsMargins(5, 5, 5, 5); page2Layout->setSpacing(15); - QFormLayout* formLayout = new QFormLayout(); - formLayout->setSpacing(12); - formLayout->setLabelAlignment(Qt::AlignRight); + mFormLayout = new QFormLayout(); + mFormLayout->setSpacing(12); + mFormLayout->setLabelAlignment(Qt::AlignRight); mNameEdit = new QLineEdit(mPage2); mNameEdit->setMinimumHeight(26); - formLayout->addRow(tr("Calendar Name:"), mNameEdit); + mFormLayout->addRow(tr("Calendar Name:"), mNameEdit); mColorBtn = new QPushButton(mPage2); mColorBtn->setFixedWidth(80); mColorBtn->setCursor(Qt::PointingHandCursor); updateColorButton(); connect(mColorBtn, SIGNAL(clicked()), this, SLOT(onSelectColor())); - formLayout->addRow(tr("Colour:"), mColorBtn); + mFormLayout->addRow(tr("Colour:"), mColorBtn); - mRemindersCheckBox = new QCheckBox(tr("Show Reminders"), mPage2); - mRemindersCheckBox->setChecked(true); - formLayout->addRow(QString(), mRemindersCheckBox); + mIdChooser = new GxsIdChooser(mPage2); + mFormLayout->addRow(tr("Owner:"), mIdChooser); - mEmailCombo = new QComboBox(mPage2); - mEmailCombo->setMinimumHeight(26); - formLayout->addRow(tr("Email:"), mEmailCombo); + page2Layout->addLayout(mFormLayout); + + // Message Distribution group box + mDistribGroupBox = new QGroupBox(tr("Message Distribution"), mPage2); + QVBoxLayout* distribLayout = new QVBoxLayout(mDistribGroupBox); + distribLayout->setContentsMargins(10, 10, 10, 10); + distribLayout->setSpacing(8); + + QHBoxLayout* radioLayout = new QHBoxLayout(); + mRadioPublic = new QRadioButton(tr("Public"), mDistribGroupBox); + mRadioCircle = new QRadioButton(tr("Restricted to Circle"), mDistribGroupBox); + mRadioNodeGroup = new QRadioButton(tr("Restricted node group"), mDistribGroupBox); + + mRadioPublic->setChecked(true); + radioLayout->addWidget(mRadioPublic); + radioLayout->addWidget(mRadioCircle); + radioLayout->addWidget(mRadioNodeGroup); + distribLayout->addLayout(radioLayout); + + mCircleCombo = new GxsCircleChooser(mDistribGroupBox); + mCircleCombo->setMinimumHeight(26); + mLocalCombo = new GroupChooser(mDistribGroupBox); + mLocalCombo->setMinimumHeight(26); + + distribLayout->addWidget(mCircleCombo); + distribLayout->addWidget(mLocalCombo); + + page2Layout->addWidget(mDistribGroupBox); + + mDescLabel = new QLabel(tr("Description"), mPage2); + mDescEdit = new QPlainTextEdit(mPage2); + mDescEdit->setPlaceholderText(tr("Set a descriptive description here")); + mDescEdit->setMaximumHeight(80); + + page2Layout->addWidget(mDescLabel); + page2Layout->addWidget(mDescEdit); - page2Layout->addLayout(formLayout); page2Layout->addStretch(); mStackedWidget->addWidget(mPage2); @@ -164,6 +228,10 @@ void CalendarPropertiesDialog::setupUi() { mCreateOrSaveBtn = new QPushButton(mEditMode ? tr("OK") : tr("Create Calendar"), this); mCancelBtn = new QPushButton(tr("Cancel"), this); + connect(mRadioPublic, SIGNAL(clicked()), this, SLOT(updateCircleOptions())); + connect(mRadioCircle, SIGNAL(clicked()), this, SLOT(updateCircleOptions())); + connect(mRadioNodeGroup, SIGNAL(clicked()), this, SLOT(updateCircleOptions())); + connect(mBackBtn, SIGNAL(clicked()), this, SLOT(onBack())); connect(mNextBtn, SIGNAL(clicked()), this, SLOT(onNext())); connect(mCreateOrSaveBtn, SIGNAL(clicked()), this, SLOT(onAccept())); @@ -193,28 +261,6 @@ void CalendarPropertiesDialog::updateColorButton() { ).arg(mSelectedColor.name())); } -void CalendarPropertiesDialog::loadIdentities() { - mEmailCombo->clear(); - QStringList emails; - - if (rsIdentity) { - std::list own_identities; - rsIdentity->getOwnIds(own_identities); - for (const auto& id : own_identities) { - RsIdentityDetails details; - if (rsIdentity->getIdDetails(id, details)) { - QString nickname = QString::fromUtf8(details.mNickname.c_str()).trimmed(); - QString gxsId = QString::fromStdString(id.toStdString()); - if (!nickname.isEmpty()) { - emails.append(QString("%1 <%1@%2>").arg(nickname).arg(gxsId)); - } - } - } - } - - mEmailCombo->addItems(emails); -} - void CalendarPropertiesDialog::onNext() { if (mRadioImport && mRadioImport->isChecked()) { accept(); @@ -250,7 +296,7 @@ void CalendarPropertiesDialog::onAccept() { CalendarInfo info = getCalendarInfo(); - if (info.onNetwork && rsGxsCalendar) { + if (info.onNetwork && rsGxsCalendar && info.owner == "local") { // Extract GXS ID from email RsGxsId authorId; int idx = info.email.lastIndexOf('@'); @@ -261,9 +307,13 @@ void CalendarPropertiesDialog::onAccept() { } std::string errMsg; + std::string descStr = info.description.toStdString(); + RsGxsCircleId circleId(info.circleId.toStdString()); + RsGxsCircleId internalCircle(info.internalCircle.toStdString()); + if (mEditMode) { RsGxsGroupId groupId(info.id.toStdString()); - if (rsGxsCalendar->updateCalendar(groupId, info.name.toStdString(), "RetroShare Calendar", authorId, errMsg)) { + if (rsGxsCalendar->updateCalendar(groupId, info.name.toStdString(), descStr, authorId, info.circleType, circleId, internalCircle, info.groupFlags, errMsg)) { CalendarData::instance()->updateCalendar(info); } else { QMessageBox::critical(this, tr("GXS Error"), tr("Failed to update network calendar: %1").arg(QString::fromStdString(errMsg))); @@ -271,7 +321,7 @@ void CalendarPropertiesDialog::onAccept() { } } else { RsGxsGroupId groupId; - if (rsGxsCalendar->createCalendar(info.name.toStdString(), "RetroShare Calendar", authorId, groupId, errMsg)) { + if (rsGxsCalendar->createCalendar(info.name.toStdString(), descStr, authorId, info.circleType, circleId, internalCircle, info.groupFlags, groupId, errMsg)) { info.id = QString::fromStdString(groupId.toStdString()); rsGxsCalendar->subscribeToCalendar(groupId, true, errMsg); CalendarData::instance()->addCalendar(info); @@ -298,12 +348,90 @@ CalendarInfo CalendarPropertiesDialog::getCalendarInfo() const { info.color = mSelectedColor; info.onNetwork = mRadioNetwork->isChecked(); info.isPublic = info.onNetwork; - info.showReminders = mRemindersCheckBox->isChecked(); - info.email = mEmailCombo->currentText(); + + // Preserve owner and defaults if in edit mode info.owner = "local"; + info.showReminders = true; + info.email = ""; + if (mEditMode) { + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == mCalId) { + info.owner = c.owner; + info.showReminders = c.showReminders; + info.email = c.email; + break; + } + } + } + + info.circleType = GXS_CIRCLE_TYPE_PUBLIC; + info.circleId = ""; + info.internalCircle = ""; + info.groupFlags = GXS_SERV::FLAG_PRIVACY_PUBLIC; + info.description = ""; + + if (info.onNetwork) { + RsGxsId authorId; + if (mIdChooser->getChosenId(authorId) == GxsIdChooser::KnowId) { + std::string nickname = ""; + if (rsIdentity) { + RsIdentityDetails details; + if (rsIdentity->getIdDetails(authorId, details)) { + nickname = details.mNickname; + } + } + if (!nickname.empty()) { + info.email = QString("%1 <%1@%2>").arg(QString::fromStdString(nickname)).arg(QString::fromStdString(authorId.toStdString())); + } else { + info.email = QString("<%1@%1>").arg(QString::fromStdString(authorId.toStdString())); + } + } else { + info.email = ""; + } + + if (mRadioPublic->isChecked()) { + info.circleType = GXS_CIRCLE_TYPE_PUBLIC; + info.groupFlags = GXS_SERV::FLAG_PRIVACY_PUBLIC; + } else if (mRadioCircle->isChecked()) { + info.circleType = GXS_CIRCLE_TYPE_EXTERNAL; + RsGxsCircleId cid; + mCircleCombo->getChosenCircle(cid); + info.circleId = QString::fromStdString(cid.toStdString()); + info.groupFlags = GXS_SERV::FLAG_PRIVACY_RESTRICTED; + } else if (mRadioNodeGroup->isChecked()) { + info.circleType = GXS_CIRCLE_TYPE_YOUR_FRIENDS_ONLY; + RsNodeGroupId ngi; + mLocalCombo->getChosenGroup(ngi); + info.internalCircle = QString::fromStdString(ngi.toStdString()); + info.groupFlags = GXS_SERV::FLAG_PRIVACY_PRIVATE; + } + info.description = mDescEdit->toPlainText(); + } + return info; } bool CalendarPropertiesDialog::isImportMode() const { return mRadioImport && mRadioImport->isChecked(); } + +void CalendarPropertiesDialog::updateCircleOptions() { + mCircleCombo->setVisible(mRadioCircle->isChecked()); + mLocalCombo->setVisible(mRadioNodeGroup->isChecked()); +} + +void CalendarPropertiesDialog::updatePage2Layout() { + bool onNetwork = mRadioNetwork->isChecked(); + + mIdChooser->setVisible(onNetwork); + if (QWidget* lbl = mFormLayout->labelForField(mIdChooser)) { + lbl->setVisible(onNetwork); + } + + mDistribGroupBox->setVisible(onNetwork); + mDescLabel->setVisible(onNetwork); + mDescEdit->setVisible(onNetwork); + + updateCircleOptions(); +} diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h index 35c0a6990..5d3933445 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h @@ -31,6 +31,13 @@ class QLineEdit; class QPushButton; class QCheckBox; class QComboBox; +class GxsIdChooser; +class GxsCircleChooser; +class GroupChooser; +class QGroupBox; +class QPlainTextEdit; +class QLabel; +class QFormLayout; class CalendarPropertiesDialog : public QDialog { Q_OBJECT @@ -46,11 +53,12 @@ private slots: void onBack(); void onSelectColor(); void onAccept(); + void updateCircleOptions(); private: void setupUi(); - void loadIdentities(); void updateColorButton(); + void updatePage2Layout(); QString mCalId; bool mEditMode; @@ -66,10 +74,20 @@ private: QRadioButton* mRadioImport; // Page 2 widgets + QFormLayout* mFormLayout; QLineEdit* mNameEdit; QPushButton* mColorBtn; - QCheckBox* mRemindersCheckBox; - QComboBox* mEmailCombo; + + // GXS network calendar widgets + GxsIdChooser* mIdChooser; + QGroupBox* mDistribGroupBox; + QRadioButton* mRadioPublic; + QRadioButton* mRadioCircle; + QRadioButton* mRadioNodeGroup; + GxsCircleChooser* mCircleCombo; + GroupChooser* mLocalCombo; + QLabel* mDescLabel; + QPlainTextEdit* mDescEdit; // Buttons QPushButton* mNextBtn; diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index 03b7a7d73..10fb91bac 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -237,15 +237,25 @@ void CalendarWidget::refreshData() { if (ownedByUs) continue; QString calName = QString::fromUtf8(meta.mGroupName.c_str()); - bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); QListWidgetItem* item = new QListWidgetItem(calName, mSharedCalendarList); item->setData(Qt::UserRole, calId); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - // Render blue bullet for subscribed, grey for unsubscribed - QPixmap pix(12, 12); - pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); + // Find if it has a saved local color and check subscription status locally + bool isSubscribedLocal = false; + QColor calColor = QColor("#4a90e2"); + for (const auto& c : cals) { + if (c.id == calId) { + isSubscribedLocal = true; + calColor = c.color; + break; + } + } + + // Render custom color bullet for subscribed, grey for unsubscribed + QPixmap pix(16, 16); + pix.fill(isSubscribedLocal ? calColor : Qt::gray); item->setIcon(QIcon(pix)); // Restore checked state if we have a saved state, @@ -253,7 +263,7 @@ void CalendarWidget::refreshData() { if (sharedCheckedStates.contains(calId)) { item->setCheckState(sharedCheckedStates[calId]); } else { - item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + item->setCheckState(isSubscribedLocal ? Qt::Checked : Qt::Unchecked); } } } @@ -367,12 +377,43 @@ void CalendarWidget::updateEventList() { } } +static QColor blendColors(const QColor& color1, const QColor& color2, qreal ratio) { + int r = color1.red() * ratio + color2.red() * (1.0 - ratio); + int g = color1.green() * ratio + color2.green() * (1.0 - ratio); + int b = color1.blue() * ratio + color2.blue() * (1.0 - ratio); + return QColor(r, g, b); +} + +static void styleEventItem(QTableWidgetItem* item, const QColor& eventColor, const QColor& baseBg) { + bool isDark = (baseBg.value() < 128); + QColor bgCol; + QColor fgCol; + if (isDark) { + bgCol = blendColors(eventColor, baseBg, 0.25); + fgCol = eventColor.lighter(130); + } else { + bgCol = blendColors(eventColor, baseBg, 0.15); + fgCol = eventColor.darker(140); + } + item->setBackground(QBrush(bgCol)); + item->setForeground(QBrush(fgCol)); + QFont font = item->font(); + font.setBold(true); + item->setFont(font); +} + void CalendarWidget::updateDayView() { mDayTable->setRowCount(0); mDayTable->setRowCount(24); // List of events for the selected day const auto& events = CalendarData::instance()->getEvents(); + const auto& cals = CalendarData::instance()->getCalendars(); + + QMap calColors; + for (const auto& cal : cals) { + calColors[cal.id] = cal.color; + } QStringList enabledCalIds; for (int i = 0; i < mCalendarList->count(); ++i) { @@ -384,6 +425,8 @@ void CalendarWidget::updateDayView() { if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } + QColor baseBg = mDayTable->palette().color(QPalette::Base); + for (int hour = 0; hour < 24; ++hour) { QString timeText = QString("%1:00").arg(hour, 2, 10, QChar('0')); mDayTable->setItem(hour, 0, new QTableWidgetItem(timeText)); @@ -402,7 +445,17 @@ void CalendarWidget::updateDayView() { QTableWidgetItem* evCell = new QTableWidgetItem(matchedEvents.join(", ")); if (!lastEventId.isEmpty()) { mCellEventMap[QString("0_%1_%2").arg(hour).arg(1)] = lastEventId; - evCell->setBackground(QBrush(QColor("#eef5fc"))); + + QColor eventColor("#4a90e2"); // default fallback + for (const auto& ev : events) { + if (ev.id == lastEventId) { + if (calColors.contains(ev.calendarId)) { + eventColor = calColors[ev.calendarId]; + } + break; + } + } + styleEventItem(evCell, eventColor, baseBg); } mDayTable->setItem(hour, 1, evCell); } @@ -423,6 +476,12 @@ void CalendarWidget::updateWeekView() { mWeekTable->setHorizontalHeaderLabels(headers); const auto& events = CalendarData::instance()->getEvents(); + const auto& cals = CalendarData::instance()->getCalendars(); + + QMap calColors; + for (const auto& cal : cals) { + calColors[cal.id] = cal.color; + } QStringList enabledCalIds; for (int i = 0; i < mCalendarList->count(); ++i) { @@ -434,6 +493,8 @@ void CalendarWidget::updateWeekView() { if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } + QColor baseBg = mWeekTable->palette().color(QPalette::Base); + // Populate week cells for (int dayIdx = 0; dayIdx < 7; ++dayIdx) { QDate date = monday.addDays(dayIdx); @@ -443,7 +504,13 @@ void CalendarWidget::updateWeekView() { if (ev.start.date() == date) { if (rowIdx >= mWeekTable->rowCount()) mWeekTable->insertRow(rowIdx); QTableWidgetItem* cellItem = new QTableWidgetItem(ev.title); - cellItem->setBackground(QBrush(QColor("#eef5fc"))); + + QColor eventColor("#4a90e2"); // default fallback + if (calColors.contains(ev.calendarId)) { + eventColor = calColors[ev.calendarId]; + } + styleEventItem(cellItem, eventColor, baseBg); + mWeekTable->setItem(rowIdx, dayIdx, cellItem); mCellEventMap[QString("1_%1_%2").arg(rowIdx).arg(dayIdx)] = ev.id; rowIdx++; @@ -490,17 +557,20 @@ void CalendarWidget::updateMonthView() { cellLines << QString::number(date.day()); } + QStringList eventCalIds; QString matchedEventId = ""; for (const auto& ev : events) { if (!enabledCalIds.contains(ev.calendarId)) continue; if (ev.start.date() == date) { cellLines << ev.title; + eventCalIds << ev.calendarId; matchedEventId = ev.id; } } QTableWidgetItem* cellItem = new QTableWidgetItem(cellLines.join("\n")); cellItem->setData(Qt::UserRole + 1, date); // Store the QDate + cellItem->setData(Qt::UserRole + 2, eventCalIds); // Store list of calendar IDs if (date.month() != mSelectedDate.month()) { cellItem->setForeground(QBrush(Qt::gray)); @@ -765,9 +835,22 @@ void CalendarWidget::onSharedCalendarContextMenu(const QPoint& pos) { QMenu menu(this); QAction* subAct = menu.addAction(isSubscribed ? tr("Unsubscribe") : tr("Subscribe")); + QAction* propertiesAct = nullptr; + if (isSubscribed) { + menu.addSeparator(); + propertiesAct = menu.addAction(tr("Properties")); + } + QAction* selectedAct = menu.exec(mSharedCalendarList->mapToGlobal(pos)); + if (!selectedAct) return; + if (selectedAct == subAct) { CalendarData::instance()->subscribeToCalendar(calId, !isSubscribed, calName); + } else if (propertiesAct && selectedAct == propertiesAct) { + CalendarPropertiesDialog dlg(calId, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } } } @@ -1047,6 +1130,11 @@ void CalendarWidget::importCalendar() { cal.showReminders = true; cal.email = ""; cal.onNetwork = false; + cal.circleType = 1; + cal.circleId = ""; + cal.internalCircle = ""; + cal.groupFlags = 4; + cal.description = ""; CalendarData::instance()->addCalendar(cal); @@ -1089,16 +1177,31 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& QDate cellDate = index.data(Qt::UserRole + 1).toDate(); bool isSelected = (cellDate.isValid() && cellDate == mCalendarWidget->selectedDate()); + QColor baseBg = mCalendarWidget->palette().color(QPalette::Base); + bool isDark = (baseBg.value() < 128); + // Draw background QColor bgColor; - if (isSelected) { - bgColor = QColor("#eff6ff"); // Light blue highlight for selected day - } else if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { - bgColor = QColor("#f8fafc"); // Slate-50 for days outside the current month - } else if (index.column() == 5 || index.column() == 6) { - bgColor = QColor("#f1f5f9"); // Slate-100 for weekends + if (isDark) { + if (isSelected) { + bgColor = QColor("#1e3a8a"); // Dark blue highlight + } else if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { + bgColor = QColor("#0f172a"); // Very dark slate for days outside month + } else if (index.column() == 5 || index.column() == 6) { + bgColor = QColor("#1e293b"); // Dark slate for weekends + } else { + bgColor = QColor("#111827"); // Dark background for weekdays + } } else { - bgColor = QColor("#ffffff"); // White for standard weekdays + if (isSelected) { + bgColor = QColor("#eff6ff"); // Light blue highlight for selected day + } else if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { + bgColor = QColor("#f8fafc"); // Slate-50 for days outside the current month + } else if (index.column() == 5 || index.column() == 6) { + bgColor = QColor("#f1f5f9"); // Slate-100 for weekends + } else { + bgColor = QColor("#ffffff"); // White for standard weekdays + } } painter->fillRect(option.rect, bgColor); @@ -1107,7 +1210,7 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& painter->setPen(QPen(QColor("#3b82f6"), 2)); painter->drawRect(option.rect.adjusted(1, 1, -1, -1)); } else { - painter->setPen(QPen(QColor("#e2e8f0"), 1)); + painter->setPen(QPen(isDark ? QColor("#334155") : QColor("#e2e8f0"), 1)); painter->drawRect(option.rect); } @@ -1123,11 +1226,11 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& painter->setFont(dayFont); if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { - painter->setPen(QColor("#94a3b8")); // Muted grey for other month days + painter->setPen(isDark ? QColor("#475569") : QColor("#94a3b8")); // Muted grey for other month days } else if (isSelected) { - painter->setPen(QColor("#2563eb")); // Darker blue for selected day number + painter->setPen(isDark ? QColor("#60a5fa") : QColor("#2563eb")); // Blue for selected day number } else { - painter->setPen(QColor("#1e293b")); // Slate-800 for standard days + painter->setPen(isDark ? QColor("#f1f5f9") : QColor("#1e293b")); // Light/slate-800 for standard days } QRect dayRect = option.rect.adjusted(5, 5, -8, -5); @@ -1140,14 +1243,14 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& QRect badgeRect(option.rect.left() + 6, option.rect.top() + 5, 38, 16); painter->setPen(Qt::NoPen); - painter->setBrush(QColor("#e2e8f0")); // Slate-200 + painter->setBrush(isDark ? QColor("#334155") : QColor("#e2e8f0")); // Slate badge background painter->drawRoundedRect(badgeRect, 8, 8); QFont badgeFont = option.font; badgeFont.setPointSize(badgeFont.pointSize() - 2); badgeFont.setBold(true); painter->setFont(badgeFont); - painter->setPen(QColor("#475569")); // Slate-600 + painter->setPen(isDark ? QColor("#cbd5e1") : QColor("#475569")); // Badge text painter->drawText(badgeRect, Qt::AlignCenter, weekStr); } @@ -1157,17 +1260,42 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& eventFont.setPointSize(eventFont.pointSize() - 1); painter->setFont(eventFont); + QStringList eventCalIds = index.data(Qt::UserRole + 2).toStringList(); + for (int i = 1; i < lines.size(); ++i) { if (yOffset + 18 > option.rect.bottom()) break; // Out of bounds QString eventTitle = lines[i]; QRect eventRect(option.rect.left() + 6, yOffset, option.rect.width() - 12, 16); + // Find event calendar color + QColor eventColor("#4a90e2"); // default fallback + if (i - 1 < eventCalIds.size()) { + QString calId = eventCalIds[i - 1]; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calId) { + eventColor = c.color; + break; + } + } + } + + QColor bgCol; + QColor fgCol; + if (isDark) { + bgCol = blendColors(eventColor, bgColor, 0.25); + fgCol = eventColor.lighter(130); + } else { + bgCol = blendColors(eventColor, bgColor, 0.15); + fgCol = eventColor.darker(140); + } + painter->setPen(Qt::NoPen); - painter->setBrush(QColor("#e0f2fe")); // Light blue event background + painter->setBrush(bgCol); painter->drawRoundedRect(eventRect, 3, 3); - painter->setPen(QColor("#0369a1")); // Blue text for events + painter->setPen(fgCol); painter->drawText(eventRect.adjusted(4, 0, -4, 0), Qt::AlignVCenter | Qt::AlignLeft, eventTitle); yOffset += 19; From e1015c20cfae4b97bcb165b8416d36dca46d1034 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:55:16 +0200 Subject: [PATCH 08/26] fix calendardata refresh --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index 4eecadd48..b516f8634 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -823,8 +823,11 @@ void CalendarData::updateCalendars() { if (changed) { saveData(); - emit calendarDataChanged(); } + // Always emit so the UI refreshes the shared calendar list + // from GXS group metadata (getCalendarsSummaries), even when + // local calendar data didn't change. + emit calendarDataChanged(); } } From 9a34d328160b4ca2619a56a70ea7eb0d0da81b37 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:18:17 +0200 Subject: [PATCH 09/26] Fixed update of the shared calendars & events update Fixed to enable some widgets only for the admins of the calendar --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 19 ++++------- .../src/gui/msgs/CalendarPropertiesDialog.cpp | 15 ++++++++- .../src/gui/msgs/CalendarWidget.cpp | 21 ++++++++---- retroshare-gui/src/gui/msgs/CalendarWidget.h | 4 ++- retroshare-gui/src/gui/msgs/TasksWidget.cpp | 33 ++++++++++++++----- retroshare-gui/src/gui/msgs/TasksWidget.h | 1 + 6 files changed, 65 insertions(+), 28 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index b516f8634..a2972a516 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -95,7 +95,7 @@ void CalendarData::loadData() { if (mCalendars.isEmpty()) { CalendarInfo defaultCal; defaultCal.id = "personal"; - defaultCal.name = "Privat"; + defaultCal.name = "Private"; defaultCal.color = QColor("#4a90e2"); defaultCal.isPublic = false; defaultCal.owner = "local"; @@ -103,17 +103,6 @@ void CalendarData::loadData() { defaultCal.email = "retroshare "; defaultCal.onNetwork = false; mCalendars.append(defaultCal); - - CalendarInfo testCal; - testCal.id = "test"; - testCal.name = "test"; - testCal.color = QColor("#50e3c2"); - testCal.isPublic = true; - testCal.owner = "local"; - testCal.showReminders = true; - testCal.email = "retroshare "; - testCal.onNetwork = true; - mCalendars.append(testCal); } // Load Events @@ -237,6 +226,7 @@ void CalendarData::saveData() { void CalendarData::addCalendar(const CalendarInfo& cal) { mCalendars.append(cal); saveData(); + emit calendarDataChanged(); } void CalendarData::updateCalendar(const CalendarInfo& cal) { @@ -247,6 +237,7 @@ void CalendarData::updateCalendar(const CalendarInfo& cal) { } } saveData(); + emit calendarDataChanged(); } void CalendarData::removeCalendar(const QString& id) { @@ -269,6 +260,7 @@ void CalendarData::removeCalendar(const QString& id) { [&id](const CalendarTask& t) { return t.calendarId == id; }), mTasks.end()); saveData(); + emit calendarDataChanged(); } void CalendarData::addEvent(const CalendarEvent& ev) { @@ -823,6 +815,7 @@ void CalendarData::updateCalendars() { if (changed) { saveData(); + emit calendarDataChanged(); } // Always emit so the UI refreshes the shared calendar list // from GXS group metadata (getCalendarsSummaries), even when @@ -837,6 +830,8 @@ void CalendarData::handleGxsEvent(std::shared_ptr event) { switch (e->mCalendarEventCode) { case RsCalendarEventCode::NEW_CALENDAR: case RsCalendarEventCode::UPDATED_CALENDAR: + updateCalendars(); + break; case RsCalendarEventCode::NEW_EVENT: case RsCalendarEventCode::UPDATED_EVENT: case RsCalendarEventCode::SUBSCRIBE_STATUS_CHANGED: diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp index 62eb3f158..e5821b209 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp @@ -102,6 +102,19 @@ CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget mLocalCombo->loadGroups(0, ngi); updateCircleOptions(); + + // Disable distribution and description controls for non-admin users + // owner == "local" means the current user created (and administers) this calendar + bool isAdmin = (existingCal.owner == "local"); + if (existingCal.onNetwork && !isAdmin) { + mIdChooser->setEnabled(false); + mRadioPublic->setEnabled(false); + mRadioCircle->setEnabled(false); + mRadioNodeGroup->setEnabled(false); + mDescEdit->setReadOnly(true); + mDescEdit->setEnabled(false); + mNameEdit->setEnabled(false); + } } updateColorButton(); mStackedWidget->setCurrentWidget(mPage2); @@ -182,7 +195,7 @@ void CalendarPropertiesDialog::setupUi() { page2Layout->addLayout(mFormLayout); // Message Distribution group box - mDistribGroupBox = new QGroupBox(tr("Message Distribution"), mPage2); + mDistribGroupBox = new QGroupBox(tr("Calendar Distribution"), mPage2); QVBoxLayout* distribLayout = new QVBoxLayout(mDistribGroupBox); distribLayout->setContentsMargins(10, 10, 10, 10); distribLayout->setSpacing(8); diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index 10fb91bac..5d302561e 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -195,7 +195,7 @@ void CalendarWidget::refreshData() { item->setFlags(item->flags() | Qt::ItemIsUserCheckable); // Render colored bullet point icon - QPixmap pix(12, 12); + QPixmap pix(16, 16); pix.fill(cal.color); item->setIcon(QIcon(pix)); @@ -211,11 +211,14 @@ void CalendarWidget::refreshData() { // 2. Populate Shared Calendars (not owned by us) { - // Save current check states + // Save current check states and subscription states QMap sharedCheckedStates; + QMap sharedSubscribedStates; for (int i = 0; i < mSharedCalendarList->count(); ++i) { QListWidgetItem* item = mSharedCalendarList->item(i); - sharedCheckedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + QString calId = item->data(Qt::UserRole).toString(); + sharedCheckedStates[calId] = item->checkState(); + sharedSubscribedStates[calId] = item->data(Qt::UserRole + 1).toBool(); } mSharedCalendarList->blockSignals(true); @@ -257,11 +260,17 @@ void CalendarWidget::refreshData() { QPixmap pix(16, 16); pix.fill(isSubscribedLocal ? calColor : Qt::gray); item->setIcon(QIcon(pix)); + item->setData(Qt::UserRole + 1, isSubscribedLocal); - // Restore checked state if we have a saved state, - // otherwise default to Checked if subscribed, Unchecked if unsubscribed + // Restore checked state if we have a saved state and subscription status did not change. + // If subscription state changed, set checked state based on new subscription status. if (sharedCheckedStates.contains(calId)) { - item->setCheckState(sharedCheckedStates[calId]); + bool wasSubscribed = sharedSubscribedStates.value(calId, false); + if (wasSubscribed != isSubscribedLocal) { + item->setCheckState(isSubscribedLocal ? Qt::Checked : Qt::Unchecked); + } else { + item->setCheckState(sharedCheckedStates[calId]); + } } else { item->setCheckState(isSubscribedLocal ? Qt::Checked : Qt::Unchecked); } diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h index 326d2ae62..70beaecfe 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -49,9 +49,11 @@ public: CalendarWidget(QWidget* parent = nullptr); ~CalendarWidget(); - void refreshData(); QDate selectedDate() const { return mSelectedDate; } +public slots: + void refreshData(); + private slots: void onNewEvent(); void onNewCalendar(); diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/msgs/TasksWidget.cpp index 82ff328ad..7265bf3c1 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.cpp +++ b/retroshare-gui/src/gui/msgs/TasksWidget.cpp @@ -154,11 +154,14 @@ void TasksWidget::refreshData() { // 2. Populate Shared Calendars (not owned by us) { - // Save current check states + // Save current check states and subscription states QMap sharedCheckedStates; + QMap sharedSubscribedStates; for (int i = 0; i < mSharedCalendarList->count(); ++i) { QListWidgetItem* item = mSharedCalendarList->item(i); - sharedCheckedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + QString calId = item->data(Qt::UserRole).toString(); + sharedCheckedStates[calId] = item->checkState(); + sharedSubscribedStates[calId] = item->data(Qt::UserRole + 1).toBool(); } mSharedCalendarList->blockSignals(true); @@ -180,7 +183,15 @@ void TasksWidget::refreshData() { if (ownedByUs) continue; QString calName = QString::fromUtf8(meta.mGroupName.c_str()); - bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); + + // Check subscription status locally (immediate) + bool isSubscribedLocal = false; + for (const auto& c : cals) { + if (c.id == calId) { + isSubscribedLocal = true; + break; + } + } QListWidgetItem* item = new QListWidgetItem(calName, mSharedCalendarList); item->setData(Qt::UserRole, calId); @@ -188,15 +199,21 @@ void TasksWidget::refreshData() { // Render blue bullet for subscribed, grey for unsubscribed QPixmap pix(12, 12); - pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); + pix.fill(isSubscribedLocal ? QColor("#4a90e2") : Qt::gray); item->setIcon(QIcon(pix)); + item->setData(Qt::UserRole + 1, isSubscribedLocal); - // Restore checked state if we have a saved state, - // otherwise default to Checked if subscribed, Unchecked if unsubscribed + // Restore checked state if we have a saved state and subscription status did not change. + // If subscription state changed, set checked state based on new subscription status. if (sharedCheckedStates.contains(calId)) { - item->setCheckState(sharedCheckedStates[calId]); + bool wasSubscribed = sharedSubscribedStates.value(calId, false); + if (wasSubscribed != isSubscribedLocal) { + item->setCheckState(isSubscribedLocal ? Qt::Checked : Qt::Unchecked); + } else { + item->setCheckState(sharedCheckedStates[calId]); + } } else { - item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + item->setCheckState(isSubscribedLocal ? Qt::Checked : Qt::Unchecked); } } } diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.h b/retroshare-gui/src/gui/msgs/TasksWidget.h index 630dd8660..a3154c122 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.h +++ b/retroshare-gui/src/gui/msgs/TasksWidget.h @@ -35,6 +35,7 @@ public: TasksWidget(QWidget* parent = nullptr); ~TasksWidget(); +public slots: void refreshData(); private slots: From 7f3dc2a6d97f93b1197f431f7dac2e5999f42838 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:33:32 +0200 Subject: [PATCH 10/26] Fix to get work attachments Enabled sorting for the event table Added view Mode for Events Trying fix Attendees invite --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 16 + retroshare-gui/src/gui/msgs/CalendarData.h | 2 + .../src/gui/msgs/CalendarWidget.cpp | 17 +- retroshare-gui/src/gui/msgs/EventDialog.cpp | 365 ++++++++++++++++-- retroshare-gui/src/gui/msgs/EventDialog.h | 16 +- retroshare-gui/src/gui/msgs/TaskDialog.cpp | 131 ++++++- retroshare-gui/src/gui/msgs/TaskDialog.h | 2 + 7 files changed, 495 insertions(+), 54 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index a2972a516..45c9effc2 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -123,6 +123,7 @@ void CalendarData::loadData() { ev.description = settings.value("description").toString(); ev.attendees = settings.value("attendees").toStringList(); ev.isPublic = settings.value("isPublic").toBool(); + ev.attachments = settings.value("attachments").toStringList(); mEvents.append(ev); } settings.endArray(); @@ -147,6 +148,7 @@ void CalendarData::loadData() { task.reminder = settings.value("reminder").toString(); task.description = settings.value("description").toString(); task.completed = settings.value("completed").toBool(); + task.attachments = settings.value("attachments").toStringList(); mTasks.append(task); } settings.endArray(); @@ -195,6 +197,7 @@ void CalendarData::saveData() { settings.setValue("description", mEvents[i].description); settings.setValue("attendees", mEvents[i].attendees); settings.setValue("isPublic", mEvents[i].isPublic); + settings.setValue("attachments", mEvents[i].attachments); } settings.endArray(); @@ -217,6 +220,7 @@ void CalendarData::saveData() { settings.setValue("reminder", mTasks[i].reminder); settings.setValue("description", mTasks[i].description); settings.setValue("completed", mTasks[i].completed); + settings.setValue("attachments", mTasks[i].attachments); } settings.endArray(); @@ -398,6 +402,10 @@ QString CalendarData::exportCalendarToIcs(const QString& calId) const { icsContent += QString("DTEND:%1\r\n").arg(ev.end.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); } + for (const auto& att : ev.attachments) { + icsContent += QString("ATTACH:%1\r\n").arg(att); + } + icsContent += "END:VEVENT\r\n"; } @@ -437,6 +445,10 @@ QString CalendarData::exportCalendarToIcs(const QString& calId) const { icsContent += QString("PERCENT-COMPLETE:%1\r\n").arg(t.percentComplete); icsContent += QString("COMPLETED:%1\r\n").arg(t.completed ? "TRUE" : "FALSE"); + for (const auto& att : t.attachments) { + icsContent += QString("ATTACH:%1\r\n").arg(att); + } + icsContent += "END:VTODO\r\n"; } @@ -561,6 +573,8 @@ void CalendarData::importCalendarFromIcs(const QString& calId, const QString& ic } else { currentEvent.end = parseIcsDateTime(val); } + } else if (key.compare("ATTACH", Qt::CaseInsensitive) == 0) { + currentEvent.attachments.append(val); } } } else if (inTask) { @@ -598,6 +612,8 @@ void CalendarData::importCalendarFromIcs(const QString& calId, const QString& ic currentTask.percentComplete = val.toInt(); } else if (key.compare("COMPLETED", Qt::CaseInsensitive) == 0) { currentTask.completed = (val.compare("TRUE", Qt::CaseInsensitive) == 0); + } else if (key.compare("ATTACH", Qt::CaseInsensitive) == 0) { + currentTask.attachments.append(val); } } } diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/msgs/CalendarData.h index d76e5637a..3e111c14c 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.h +++ b/retroshare-gui/src/gui/msgs/CalendarData.h @@ -62,6 +62,7 @@ struct CalendarEvent { QString description; QStringList attendees; // PGP IDs of contacts bool isPublic; + QStringList attachments; }; struct CalendarTask { @@ -80,6 +81,7 @@ struct CalendarTask { QString reminder; QString description; bool completed; + QStringList attachments; }; class CalendarData : public QObject { diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index 5d302561e..97a107ed1 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -104,6 +104,7 @@ void CalendarWidget::buildUi() { mEventTable->setSelectionBehavior(QAbstractItemView::SelectRows); mEventTable->setSelectionMode(QAbstractItemView::SingleSelection); mEventTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + mEventTable->setSortingEnabled(true); connect(mEventTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); mEventTable->setContextMenuPolicy(Qt::CustomContextMenu); connect(mEventTable, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onEventTableContextMenu(const QPoint&))); @@ -333,6 +334,7 @@ void CalendarWidget::updateViews() { } void CalendarWidget::updateEventList() { + mEventTable->setSortingEnabled(false); mEventTable->setRowCount(0); const auto& events = CalendarData::instance()->getEvents(); @@ -384,6 +386,7 @@ void CalendarWidget::updateEventList() { mEventTable->setItem(row, 4, new QTableWidgetItem(calName)); row++; } + mEventTable->setSortingEnabled(true); } static QColor blendColors(const QColor& color1, const QColor& color2, qreal ratio) { @@ -706,9 +709,7 @@ void CalendarWidget::onEventSelected(int row, int col) { } } - if (!canEdit) return; - - EventDialog dlg(eventId, QDateTime::currentDateTime(), this); + EventDialog dlg(eventId, QDateTime::currentDateTime(), this, !canEdit); if (dlg.exec() == QDialog::Accepted) { refreshData(); } @@ -911,12 +912,18 @@ void CalendarWidget::onEventTableContextMenu(const QPoint& pos) { } QMenu menu(this); + QAction* viewAct = menu.addAction(tr("View Event")); QAction* editAct = menu.addAction(tr("Edit Event")); editAct->setEnabled(canEdit); QAction* selectedAct = menu.exec(mEventTable->viewport()->mapToGlobal(pos)); - if (selectedAct == editAct && canEdit) { - EventDialog dlg(eventId, QDateTime::currentDateTime(), this); + if (selectedAct == viewAct) { + EventDialog dlg(eventId, QDateTime::currentDateTime(), this, !canEdit); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } else if (selectedAct == editAct && canEdit) { + EventDialog dlg(eventId, QDateTime::currentDateTime(), this, false); if (dlg.exec() == QDialog::Accepted) { refreshData(); } diff --git a/retroshare-gui/src/gui/msgs/EventDialog.cpp b/retroshare-gui/src/gui/msgs/EventDialog.cpp index 922ad18c5..bd93ff0e5 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.cpp +++ b/retroshare-gui/src/gui/msgs/EventDialog.cpp @@ -1,4 +1,6 @@ #include "gui/msgs/EventDialog.h" +#include +#include "retroshare/rsgxsflags.h" #include #include #include @@ -14,15 +16,23 @@ #include #include #include +#include +#include +#include +#include +#include +#include "gui/RetroShareLink.h" +#include "gui/common/FriendSelectionWidget.h" +#include -EventDialog::EventDialog(const QString& eventId, const QDateTime& startInfo, QWidget* parent) - : QDialog(parent), mEventId(eventId), mDefaultStart(startInfo) +EventDialog::EventDialog(const QString& eventId, const QDateTime& startInfo, QWidget* parent, bool readOnly) + : QDialog(parent), mEventId(eventId), mDefaultStart(startInfo), mReadOnly(readOnly) { - setWindowTitle(mEventId.isEmpty() ? tr("New Event") : tr("Edit Event")); setMinimumSize(500, 600); buildUi(); loadEvent(); + updateModeUi(); } EventDialog::~EventDialog() {} @@ -32,28 +42,31 @@ void EventDialog::buildUi() { mainLayout->setContentsMargins(15, 15, 15, 15); mainLayout->setSpacing(10); - // Top action bar (Save, Close, Delete) - QHBoxLayout* actionLayout = new QHBoxLayout(); - QPushButton* saveBtn = new QPushButton(tr("Save and Close"), this); - saveBtn->setIcon(QIcon(":/icons/mail/compose.png")); - connect(saveBtn, SIGNAL(clicked()), this, SLOT(onSaveAndClose())); - actionLayout->addWidget(saveBtn); + // Top action bar (Save, Invite, Delete) + mActionWidget = new QWidget(this); + QHBoxLayout* actionLayout = new QHBoxLayout(mActionWidget); + actionLayout->setContentsMargins(0, 0, 0, 0); - QPushButton* inviteBtn = new QPushButton(tr("Invite Attendees"), this); - connect(inviteBtn, SIGNAL(clicked()), this, SLOT(onInviteAttendees())); - actionLayout->addWidget(inviteBtn); + mSaveBtn = new QPushButton(tr("Save and Close"), this); + mSaveBtn->setIcon(QIcon(":/icons/mail/compose.png")); + connect(mSaveBtn, SIGNAL(clicked()), this, SLOT(onSaveAndClose())); + actionLayout->addWidget(mSaveBtn); - QPushButton* deleteBtn = new QPushButton(tr("Delete"), this); - deleteBtn->setIcon(QIcon(":/icons/mail/delete.png")); - connect(deleteBtn, SIGNAL(clicked()), this, SLOT(onDelete())); - actionLayout->addWidget(deleteBtn); + mInviteBtn = new QPushButton(tr("Invite Attendees"), this); + connect(mInviteBtn, SIGNAL(clicked()), this, SLOT(onInviteAttendees())); + actionLayout->addWidget(mInviteBtn); + + mDeleteBtn = new QPushButton(tr("Delete"), this); + mDeleteBtn->setIcon(QIcon(":/icons/mail/delete.png")); + connect(mDeleteBtn, SIGNAL(clicked()), this, SLOT(onDelete())); + actionLayout->addWidget(mDeleteBtn); if (mEventId.isEmpty()) { - deleteBtn->setEnabled(false); + mDeleteBtn->setEnabled(false); } actionLayout->addStretch(); - mainLayout->addLayout(actionLayout); + mainLayout->addWidget(mActionWidget); // Form inputs layout QFormLayout* formLayout = new QFormLayout(); @@ -110,28 +123,125 @@ void EventDialog::buildUi() { // Attendees Tab mAttendeesList = new QListWidget(this); - QMap contacts = CalendarData::getContacts(); - for (auto it = contacts.begin(); it != contacts.end(); ++it) { - QListWidgetItem* item = new QListWidgetItem(it.value(), mAttendeesList); - item->setData(Qt::UserRole, it.key()); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(Qt::Unchecked); - } tabWidget->addTab(mAttendeesList, tr("Attendees")); // Attachments Tab QWidget* attachTab = new QWidget(this); QVBoxLayout* attachLayout = new QVBoxLayout(attachTab); mAttachmentsList = new QListWidget(this); - attachLayout->addWidget(mAttachmentsList); - QPushButton* addAttachBtn = new QPushButton(tr("Attach File..."), this); - connect(addAttachBtn, &QPushButton::clicked, [this]() { - QString file = QFileDialog::getOpenFileName(this, tr("Select File")); - if (!file.isEmpty()) { - mAttachmentsList->addItem(QFileInfo(file).fileName()); + mAttachmentsList->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(mAttachmentsList, &QListWidget::customContextMenuRequested, [this](const QPoint& pos) { + QListWidgetItem* item = mAttachmentsList->itemAt(pos); + if (!item) return; + + QMenu menu(this); + QAction* downloadAction = menu.addAction(QIcon(":/icons/png/download.png"), tr("Download")); + QAction* downloadAllAction = menu.addAction(QIcon(":/icons/mail/downloadall.png"), tr("Download all")); + QAction* removeAction = nullptr; + + if (!mReadOnly) { + menu.addSeparator(); + removeAction = menu.addAction(QIcon(":/icons/mail/delete.png"), tr("Remove Attachment")); + } + + QAction* selectedAction = menu.exec(mAttachmentsList->mapToGlobal(pos)); + if (selectedAction == downloadAction) { + QString att = item->data(Qt::UserRole).toString(); + if (!att.isEmpty()) { + RetroShareLink link(att); + if (link.valid() && (link.type() == RetroShareLink::TYPE_FILE || link.type() == RetroShareLink::TYPE_FILE_TREE)) { + QList links; + links.append(link); + RetroShareLink::process(links); + } else if (QFileInfo::exists(att)) { + QString targetPath = QFileDialog::getSaveFileName(this, tr("Save Attachment As"), QFileInfo(att).fileName()); + if (!targetPath.isEmpty()) { + if (QFile::exists(targetPath)) { + QFile::remove(targetPath); + } + if (!QFile::copy(att, targetPath)) { + QMessageBox::warning(this, tr("Download Failed"), tr("Could not save the file to %1").arg(targetPath)); + } + } + } else { + QUrl url(att); + if (!url.scheme().isEmpty()) { + QDesktopServices::openUrl(url); + } + } + } + } else if (selectedAction == downloadAllAction) { + QList rsLinks; + QStringList localFiles; + for (int i = 0; i < mAttachmentsList->count(); ++i) { + QString att = mAttachmentsList->item(i)->data(Qt::UserRole).toString(); + if (att.isEmpty()) continue; + RetroShareLink link(att); + if (link.valid() && (link.type() == RetroShareLink::TYPE_FILE || link.type() == RetroShareLink::TYPE_FILE_TREE)) { + rsLinks.append(link); + } else if (QFileInfo::exists(att)) { + localFiles.append(att); + } else { + QUrl url(att); + if (!url.scheme().isEmpty()) { + QDesktopServices::openUrl(url); + } + } + } + if (!rsLinks.isEmpty()) { + RetroShareLink::process(rsLinks); + } + if (!localFiles.isEmpty()) { + QString targetDir = QFileDialog::getExistingDirectory(this, tr("Select Directory to Save Attachments")); + if (!targetDir.isEmpty()) { + bool success = true; + QStringList failedFiles; + for (const QString& file : localFiles) { + QFileInfo fi(file); + QString targetPath = QDir(targetDir).filePath(fi.fileName()); + if (QFile::exists(targetPath)) { + QFile::remove(targetPath); + } + if (!QFile::copy(file, targetPath)) { + success = false; + failedFiles.append(fi.fileName()); + } + } + if (!success) { + QMessageBox::warning(this, tr("Download Failed"), tr("Could not save the following files: %1").arg(failedFiles.join(", "))); + } + } + } + } else if (removeAction && selectedAction == removeAction) { + delete mAttachmentsList->takeItem(mAttachmentsList->row(item)); } }); - attachLayout->addWidget(addAttachBtn); + + connect(mAttachmentsList, &QListWidget::itemDoubleClicked, [](QListWidgetItem* item) { + QString pathOrUrl = item->data(Qt::UserRole).toString(); + if (!pathOrUrl.isEmpty()) { + QUrl url(pathOrUrl); + if (url.scheme().isEmpty()) { + url = QUrl::fromLocalFile(pathOrUrl); + } + QDesktopServices::openUrl(url); + } + }); + + attachLayout->addWidget(mAttachmentsList); + mAddAttachBtn = new QPushButton(tr("Attach File..."), this); + connect(mAddAttachBtn, &QPushButton::clicked, [this]() { + QStringList files = QFileDialog::getOpenFileNames(this, tr("Select File(s)")); + for (const QString& file : files) { + if (!file.isEmpty()) { + QListWidgetItem* item = new QListWidgetItem(QFileInfo(file).fileName(), mAttachmentsList); + item->setData(Qt::UserRole, file); + item->setToolTip(file); + } + } + }); + attachLayout->addWidget(mAddAttachBtn); tabWidget->addTab(attachTab, tr("Attachments")); mainLayout->addWidget(tabWidget); @@ -149,6 +259,23 @@ void EventDialog::buildUi() { bottomCheckLayout->addWidget(mDisallowCheck); mainLayout->addLayout(bottomCheckLayout); + + // Bottom Buttons (Close & Edit for read-only view mode) + mBottomButtonsWidget = new QWidget(this); + QHBoxLayout* bottomBtnLayout = new QHBoxLayout(mBottomButtonsWidget); + bottomBtnLayout->setContentsMargins(0, 0, 0, 0); + bottomBtnLayout->addStretch(); + + mEditBtn = new QPushButton(tr("Edit"), this); + mEditBtn->setStyleSheet("background-color: #0078d4; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold;"); + connect(mEditBtn, SIGNAL(clicked()), this, SLOT(onEditClicked())); + bottomBtnLayout->addWidget(mEditBtn); + + mCloseBtn = new QPushButton(tr("Close"), this); + connect(mCloseBtn, SIGNAL(clicked()), this, SLOT(reject())); + bottomBtnLayout->addWidget(mCloseBtn); + + mainLayout->addWidget(mBottomButtonsWidget); } void EventDialog::loadEvent() { @@ -174,14 +301,22 @@ void EventDialog::loadEvent() { mDescriptionEdit->setPlainText(ev.description); // Set attendees - for (int i = 0; i < mAttendeesList->count(); ++i) { - QListWidgetItem* item = mAttendeesList->item(i); - QString contactId = item->data(Qt::UserRole).toString(); - if (ev.attendees.contains(contactId)) { - item->setCheckState(Qt::Checked); - } else { - item->setCheckState(Qt::Unchecked); - } + mAttendeesList->clear(); + QMap contacts = CalendarData::getContacts(); + for (const auto& contactId : ev.attendees) { + QString name = contacts.value(contactId, contactId); + QListWidgetItem* item = new QListWidgetItem(name, mAttendeesList); + item->setData(Qt::UserRole, contactId); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Checked); + } + + // Load attachments + mAttachmentsList->clear(); + for (const auto& att : ev.attachments) { + QListWidgetItem* item = new QListWidgetItem(QFileInfo(att).fileName(), mAttachmentsList); + item->setData(Qt::UserRole, att); + item->setToolTip(att); } break; } @@ -199,7 +334,76 @@ void EventDialog::onAllDayToggled(bool checked) { } void EventDialog::onInviteAttendees() { - // Just switches to attendees tab + QDialog dialog(this); + dialog.setWindowTitle(tr("Invite Attendees")); + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QComboBox* filterCombo = new QComboBox(&dialog); + filterCombo->addItem(tr("All people")); + filterCombo->addItem(tr("My contacts")); +#ifdef RS_DIRECT_CHAT + filterCombo->addItem(tr("Friend Nodes")); +#endif + filterCombo->setCurrentIndex(0); + + FriendSelectionWidget* friendsWidget = new FriendSelectionWidget(&dialog); + friendsWidget->setHeaderText(tr("Select contacts to invite:")); + friendsWidget->setModus(FriendSelectionWidget::MODUS_MULTI); + friendsWidget->setShowType(FriendSelectionWidget::SHOW_GXS); + friendsWidget->start(); + + connect(filterCombo, static_cast(&QComboBox::currentIndexChanged), [friendsWidget](int index) { + switch (index) { + default: + case 0: + friendsWidget->setShowType(FriendSelectionWidget::SHOW_GXS); + break; + case 1: + friendsWidget->setShowType(FriendSelectionWidget::SHOW_CONTACTS); + break; +#ifdef RS_DIRECT_CHAT + case 2: + friendsWidget->setShowType(FriendSelectionWidget::SHOW_SSL); + break; +#endif + } + }); + + // Pre-select current attendees + std::set psids; + for (int i = 0; i < mAttendeesList->count(); ++i) { + QListWidgetItem* item = mAttendeesList->item(i); + if (item->checkState() == Qt::Checked) { + psids.insert(item->data(Qt::UserRole).toString().toStdString()); + } + } + friendsWidget->setSelectedIdsFromString(FriendSelectionWidget::IDTYPE_GPG, psids, false); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + layout->addWidget(filterCombo); + layout->addWidget(friendsWidget); + layout->addWidget(buttonBox); + + if (dialog.exec() == QDialog::Accepted) { + std::set selected; + friendsWidget->selectedIds(selected, false); + + mAttendeesList->clear(); + QMap contacts = CalendarData::getContacts(); + for (const auto& pgpId : selected) { + QString pgpIdStr = QString::fromStdString(pgpId.toStdString()); + QString name = contacts.value(pgpIdStr, pgpIdStr); + QListWidgetItem* item = new QListWidgetItem(name, mAttendeesList); + item->setData(Qt::UserRole, pgpIdStr); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Checked); + } + } + + // Switch to attendees tab QTabWidget* tabWidget = findChild(); if (tabWidget) { tabWidget->setCurrentIndex(1); // Attendees is index 1 @@ -236,6 +440,11 @@ void EventDialog::onSaveAndClose() { } } + // Get attachments + for (int i = 0; i < mAttachmentsList->count(); ++i) { + ev.attachments.append(mAttachmentsList->item(i)->data(Qt::UserRole).toString()); + } + if (mEventId.isEmpty()) { CalendarData::instance()->addEvent(ev); } else { @@ -259,3 +468,77 @@ void EventDialog::onDelete() { accept(); } } + +void EventDialog::onEditClicked() { + mReadOnly = false; + updateModeUi(); +} + +void EventDialog::updateModeUi() { + bool canEdit = false; + if (mEventId.isEmpty()) { + canEdit = true; + } else { + // Determine if user can edit this event (admin check for shared calendars) + QString calendarId; + const auto& events = CalendarData::instance()->getEvents(); + for (const auto& ev : events) { + if (ev.id == mEventId) { + calendarId = ev.calendarId; + break; + } + } + if (!calendarId.isEmpty()) { + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calendarId) { + if (!c.onNetwork) { + canEdit = true; + } else if (rsGxsCalendar) { + std::list summaries; + if (rsGxsCalendar->getCalendarsSummaries(summaries)) { + RsGxsGroupId groupId(calendarId.toStdString()); + for (const auto& meta : summaries) { + if (meta.mGroupId == groupId) { + canEdit = IS_GROUP_ADMIN(meta.mSubscribeFlags); + break; + } + } + } + } + break; + } + } + } else { + canEdit = true; + } + } + + setWindowTitle(mReadOnly ? tr("View Event") : (mEventId.isEmpty() ? tr("New Event") : tr("Edit Event"))); + + // Top action bar is visible only in edit mode + mActionWidget->setVisible(!mReadOnly); + + // Bottom buttons are visible only in read-only mode + mBottomButtonsWidget->setVisible(mReadOnly); + mEditBtn->setVisible(canEdit); + + // Set read-only / enabled state of all fields + mCalendarCombo->setEnabled(!mReadOnly); + mTitleEdit->setReadOnly(mReadOnly); + mLocationEdit->setReadOnly(mReadOnly); + mCategoryCombo->setEnabled(!mReadOnly); + mAllDayCheck->setEnabled(!mReadOnly); + mStartEdit->setReadOnly(mReadOnly); + mEndEdit->setReadOnly(mReadOnly); + mRepeatCombo->setEnabled(!mReadOnly); + mReminderCombo->setEnabled(!mReadOnly); + mDescriptionEdit->setReadOnly(mReadOnly); + + mAttendeesList->setEnabled(!mReadOnly); + mAddAttachBtn->setVisible(!mReadOnly); + + mNotifyCheck->setEnabled(!mReadOnly); + mSeparateCheck->setEnabled(!mReadOnly); + mDisallowCheck->setEnabled(!mReadOnly); +} diff --git a/retroshare-gui/src/gui/msgs/EventDialog.h b/retroshare-gui/src/gui/msgs/EventDialog.h index 46c85adfa..0b29c79dd 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.h +++ b/retroshare-gui/src/gui/msgs/EventDialog.h @@ -17,7 +17,7 @@ class EventDialog : public QDialog { public: // Pass eventId to edit existing event, or empty string to create a new one. // If creating a new one, startInfo can specify the default start time. - EventDialog(const QString& eventId = "", const QDateTime& startInfo = QDateTime::currentDateTime(), QWidget* parent = nullptr); + EventDialog(const QString& eventId = "", const QDateTime& startInfo = QDateTime::currentDateTime(), QWidget* parent = nullptr, bool readOnly = false); ~EventDialog(); private slots: @@ -25,13 +25,21 @@ private slots: void onDelete(); void onAllDayToggled(bool checked); void onInviteAttendees(); + void onEditClicked(); private: void loadEvent(); void buildUi(); + void updateModeUi(); QString mEventId; QDateTime mDefaultStart; + bool mReadOnly; + + QWidget* mActionWidget; + QPushButton* mSaveBtn; + QPushButton* mInviteBtn; + QPushButton* mDeleteBtn; QComboBox* mCalendarCombo; QLineEdit* mTitleEdit; @@ -44,7 +52,13 @@ private: QComboBox* mReminderCombo; QTextEdit* mDescriptionEdit; QListWidget* mAttendeesList; + QListWidget* mAttachmentsList; + QPushButton* mAddAttachBtn; + + QWidget* mBottomButtonsWidget; + QPushButton* mEditBtn; + QPushButton* mCloseBtn; QCheckBox* mNotifyCheck; QCheckBox* mSeparateCheck; diff --git a/retroshare-gui/src/gui/msgs/TaskDialog.cpp b/retroshare-gui/src/gui/msgs/TaskDialog.cpp index fe795d88a..d8cfb4ae4 100644 --- a/retroshare-gui/src/gui/msgs/TaskDialog.cpp +++ b/retroshare-gui/src/gui/msgs/TaskDialog.cpp @@ -16,6 +16,11 @@ #include #include #include +#include +#include +#include +#include +#include "gui/RetroShareLink.h" TaskDialog::TaskDialog(const QString& taskId, QWidget* parent) : QDialog(parent), mTaskId(taskId) @@ -129,15 +134,114 @@ void TaskDialog::buildUi() { QWidget* attachTab = new QWidget(this); QVBoxLayout* attachLayout = new QVBoxLayout(attachTab); mAttachmentsList = new QListWidget(this); - attachLayout->addWidget(mAttachmentsList); - QPushButton* addAttachBtn = new QPushButton(tr("Attach File..."), this); - connect(addAttachBtn, &QPushButton::clicked, [this]() { - QString file = QFileDialog::getOpenFileName(this, tr("Select File")); - if (!file.isEmpty()) { - mAttachmentsList->addItem(QFileInfo(file).fileName()); + mAttachmentsList->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(mAttachmentsList, &QListWidget::customContextMenuRequested, [this](const QPoint& pos) { + QListWidgetItem* item = mAttachmentsList->itemAt(pos); + if (!item) return; + + QMenu menu(this); + QAction* downloadAction = menu.addAction(QIcon(":/icons/png/download.png"), tr("Download")); + QAction* downloadAllAction = menu.addAction(QIcon(":/icons/mail/downloadall.png"), tr("Download all")); + QAction* removeAction = menu.addAction(QIcon(":/icons/mail/delete.png"), tr("Remove Attachment")); + + QAction* selectedAction = menu.exec(mAttachmentsList->mapToGlobal(pos)); + if (selectedAction == downloadAction) { + QString att = item->data(Qt::UserRole).toString(); + if (!att.isEmpty()) { + RetroShareLink link(att); + if (link.valid() && (link.type() == RetroShareLink::TYPE_FILE || link.type() == RetroShareLink::TYPE_FILE_TREE)) { + QList links; + links.append(link); + RetroShareLink::process(links); + } else if (QFileInfo::exists(att)) { + QString targetPath = QFileDialog::getSaveFileName(this, tr("Save Attachment As"), QFileInfo(att).fileName()); + if (!targetPath.isEmpty()) { + if (QFile::exists(targetPath)) { + QFile::remove(targetPath); + } + if (!QFile::copy(att, targetPath)) { + QMessageBox::warning(this, tr("Download Failed"), tr("Could not save the file to %1").arg(targetPath)); + } + } + } else { + QUrl url(att); + if (!url.scheme().isEmpty()) { + QDesktopServices::openUrl(url); + } + } + } + } else if (selectedAction == downloadAllAction) { + QList rsLinks; + QStringList localFiles; + for (int i = 0; i < mAttachmentsList->count(); ++i) { + QString att = mAttachmentsList->item(i)->data(Qt::UserRole).toString(); + if (att.isEmpty()) continue; + RetroShareLink link(att); + if (link.valid() && (link.type() == RetroShareLink::TYPE_FILE || link.type() == RetroShareLink::TYPE_FILE_TREE)) { + rsLinks.append(link); + } else if (QFileInfo::exists(att)) { + localFiles.append(att); + } else { + QUrl url(att); + if (!url.scheme().isEmpty()) { + QDesktopServices::openUrl(url); + } + } + } + if (!rsLinks.isEmpty()) { + RetroShareLink::process(rsLinks); + } + if (!localFiles.isEmpty()) { + QString targetDir = QFileDialog::getExistingDirectory(this, tr("Select Directory to Save Attachments")); + if (!targetDir.isEmpty()) { + bool success = true; + QStringList failedFiles; + for (const QString& file : localFiles) { + QFileInfo fi(file); + QString targetPath = QDir(targetDir).filePath(fi.fileName()); + if (QFile::exists(targetPath)) { + QFile::remove(targetPath); + } + if (!QFile::copy(file, targetPath)) { + success = false; + failedFiles.append(fi.fileName()); + } + } + if (!success) { + QMessageBox::warning(this, tr("Download Failed"), tr("Could not save the following files: %1").arg(failedFiles.join(", "))); + } + } + } + } else if (selectedAction == removeAction) { + delete mAttachmentsList->takeItem(mAttachmentsList->row(item)); } }); - attachLayout->addWidget(addAttachBtn); + + connect(mAttachmentsList, &QListWidget::itemDoubleClicked, [](QListWidgetItem* item) { + QString pathOrUrl = item->data(Qt::UserRole).toString(); + if (!pathOrUrl.isEmpty()) { + QUrl url(pathOrUrl); + if (url.scheme().isEmpty()) { + url = QUrl::fromLocalFile(pathOrUrl); + } + QDesktopServices::openUrl(url); + } + }); + + attachLayout->addWidget(mAttachmentsList); + mAddAttachBtn = new QPushButton(tr("Attach File..."), this); + connect(mAddAttachBtn, &QPushButton::clicked, [this]() { + QStringList files = QFileDialog::getOpenFileNames(this, tr("Select File(s)")); + for (const QString& file : files) { + if (!file.isEmpty()) { + QListWidgetItem* item = new QListWidgetItem(QFileInfo(file).fileName(), mAttachmentsList); + item->setData(Qt::UserRole, file); + item->setToolTip(file); + } + } + }); + attachLayout->addWidget(mAddAttachBtn); tabWidget->addTab(attachTab, tr("Attachments")); mainLayout->addWidget(tabWidget); @@ -169,6 +273,14 @@ void TaskDialog::loadTask() { mRepeatCombo->setCurrentText(t.repeat); mReminderCombo->setCurrentText(t.reminder); mDescriptionEdit->setPlainText(t.description); + + // Load attachments + mAttachmentsList->clear(); + for (const auto& att : t.attachments) { + QListWidgetItem* item = new QListWidgetItem(QFileInfo(att).fileName(), mAttachmentsList); + item->setData(Qt::UserRole, att); + item->setToolTip(att); + } break; } } @@ -209,6 +321,11 @@ void TaskDialog::onSaveAndClose() { t.percentComplete = 100; } + // Get attachments + for (int i = 0; i < mAttachmentsList->count(); ++i) { + t.attachments.append(mAttachmentsList->item(i)->data(Qt::UserRole).toString()); + } + if (mTaskId.isEmpty()) { CalendarData::instance()->addTask(t); } else { diff --git a/retroshare-gui/src/gui/msgs/TaskDialog.h b/retroshare-gui/src/gui/msgs/TaskDialog.h index 774724479..7cae6074b 100644 --- a/retroshare-gui/src/gui/msgs/TaskDialog.h +++ b/retroshare-gui/src/gui/msgs/TaskDialog.h @@ -11,6 +11,7 @@ class QDateTimeEdit; class QTextEdit; class QSpinBox; class QListWidget; +class QPushButton; class TaskDialog : public QDialog { Q_OBJECT @@ -44,6 +45,7 @@ private: QComboBox* mReminderCombo; QTextEdit* mDescriptionEdit; QListWidget* mAttachmentsList; + QPushButton* mAddAttachBtn; }; #endif // TASKDIALOG_H From 9b9041c6ae50c6f08a2f34b8e9ce5942832c5eef Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:17:12 +0200 Subject: [PATCH 11/26] Added invite feature --- retroshare-gui/src/gui/msgs/EventDialog.cpp | 311 ++++++++++++++++++-- retroshare-gui/src/gui/msgs/EventDialog.h | 4 +- 2 files changed, 283 insertions(+), 32 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/EventDialog.cpp b/retroshare-gui/src/gui/msgs/EventDialog.cpp index bd93ff0e5..df9582de4 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.cpp +++ b/retroshare-gui/src/gui/msgs/EventDialog.cpp @@ -11,6 +11,10 @@ #include #include #include +#include +#include +#include "gui/gxs/GxsIdTreeWidgetItem.h" +#include "gui/gxs/GxsIdDetails.h" #include #include #include @@ -24,6 +28,65 @@ #include "gui/RetroShareLink.h" #include "gui/common/FriendSelectionWidget.h" #include +#include +#include +#include +#include "gui/common/PeerDefs.h" +#include +#include "gui/common/AvatarDefs.h" +#include +#include + +namespace { +QString getContactName(const QString& idStr) { + std::string str = idStr.toStdString(); + if (str.length() == 16) { + RsPgpId pgpId(str); + QString name; + PeerDefs::rsidFromId(pgpId, &name); + return name; + } else if (str.length() == 32) { + RsGxsId gxsId(str); + RsIdentityDetails details; + if (rsIdentity && rsIdentity->getIdDetails(gxsId, details)) { + return QString::fromUtf8(details.mNickname.c_str()); + } + RsPeerId peerId(str); + std::string peerName = rsPeers ? rsPeers->getPeerName(peerId) : ""; + if (!peerName.empty()) { + return QString::fromUtf8(peerName.c_str()); + } + QString name; + PeerDefs::rsidFromId(peerId, &name); + if (name != QCoreApplication::translate("PeerDefs", "Unknown")) { + return name; + } + PeerDefs::rsidFromId(gxsId, &name); + return name; + } + return idStr; +} + +QIcon getContactAvatar(const QString& idStr) { + std::string str = idStr.toStdString(); + QPixmap pixmap; + if (str.length() == 16) { + AvatarDefs::getAvatarFromGpgId(RsPgpId(str), pixmap); + } else if (str.length() == 32) { + RsGxsId gxsId(str); + RsIdentityDetails details; + if (rsIdentity && rsIdentity->getIdDetails(gxsId, details)) { + AvatarDefs::getAvatarFromGxsId(gxsId, pixmap); + } else { + AvatarDefs::getAvatarFromSslId(RsPeerId(str), pixmap); + } + } + if (pixmap.isNull()) { + pixmap = QPixmap(AVATAR_DEFAULT_IMAGE_SQUARE); + } + return QIcon(pixmap); +} +} EventDialog::EventDialog(const QString& eventId, const QDateTime& startInfo, QWidget* parent, bool readOnly) : QDialog(parent), mEventId(eventId), mDefaultStart(startInfo), mReadOnly(readOnly) @@ -122,7 +185,10 @@ void EventDialog::buildUi() { tabWidget->addTab(mDescriptionEdit, tr("Description")); // Attendees Tab - mAttendeesList = new QListWidget(this); + mAttendeesList = new QTreeWidget(this); + mAttendeesList->setHeaderHidden(true); + mAttendeesList->setIconSize(QSize(32, 32)); + mAttendeesList->setRootIsDecorated(false); tabWidget->addTab(mAttendeesList, tr("Attendees")); // Attachments Tab @@ -302,13 +368,42 @@ void EventDialog::loadEvent() { // Set attendees mAttendeesList->clear(); - QMap contacts = CalendarData::getContacts(); for (const auto& contactId : ev.attendees) { - QString name = contacts.value(contactId, contactId); - QListWidgetItem* item = new QListWidgetItem(name, mAttendeesList); - item->setData(Qt::UserRole, contactId); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(Qt::Checked); + if (contactId.length() == 16) { + QString name = getContactName(contactId); + QTreeWidgetItem* item = new QTreeWidgetItem(mAttendeesList); + item->setIcon(0, getContactAvatar(contactId)); + item->setText(0, name); + item->setData(0, Qt::UserRole, contactId); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Checked); + } else if (contactId.length() == 32) { + RsGxsId gxsId(contactId.toStdString()); + RsPeerId peerId(contactId.toStdString()); + bool isSsl = false; + if (rsPeers) { + std::string peerName = rsPeers->getPeerName(peerId); + if (!peerName.empty() || rsPeers->isFriend(peerId)) { + isSsl = true; + } + } + if (isSsl) { + QString name = getContactName(contactId); + QTreeWidgetItem* item = new QTreeWidgetItem(mAttendeesList); + item->setIcon(0, getContactAvatar(contactId)); + item->setText(0, name); + item->setData(0, Qt::UserRole, contactId); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Checked); + } else { + // Treat as GXS ID + GxsIdRSTreeWidgetItem* item = new GxsIdRSTreeWidgetItem(nullptr, GxsIdDetails::ICON_TYPE_AVATAR, true, mAttendeesList); + item->setData(0, Qt::UserRole, contactId); + item->setId(gxsId, 0, true); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Checked); + } + } } // Load attachments @@ -336,6 +431,7 @@ void EventDialog::onAllDayToggled(bool checked) { void EventDialog::onInviteAttendees() { QDialog dialog(this); dialog.setWindowTitle(tr("Invite Attendees")); + dialog.resize(this->width(), 500); QVBoxLayout* layout = new QVBoxLayout(&dialog); QComboBox* filterCombo = new QComboBox(&dialog); @@ -348,7 +444,7 @@ void EventDialog::onInviteAttendees() { FriendSelectionWidget* friendsWidget = new FriendSelectionWidget(&dialog); friendsWidget->setHeaderText(tr("Select contacts to invite:")); - friendsWidget->setModus(FriendSelectionWidget::MODUS_MULTI); + friendsWidget->setModus(FriendSelectionWidget::MODUS_CHECK); friendsWidget->setShowType(FriendSelectionWidget::SHOW_GXS); friendsWidget->start(); @@ -370,14 +466,35 @@ void EventDialog::onInviteAttendees() { }); // Pre-select current attendees - std::set psids; - for (int i = 0; i < mAttendeesList->count(); ++i) { - QListWidgetItem* item = mAttendeesList->item(i); - if (item->checkState() == Qt::Checked) { - psids.insert(item->data(Qt::UserRole).toString().toStdString()); + std::set psidsGpg; + std::set psidsGxs; + std::set psidsSsl; + for (int i = 0; i < mAttendeesList->topLevelItemCount(); ++i) { + QTreeWidgetItem* item = mAttendeesList->topLevelItem(i); + if (item->checkState(0) == Qt::Checked) { + std::string idStr = item->data(0, Qt::UserRole).toString().toStdString(); + if (idStr.length() == 16) { + psidsGpg.insert(idStr); + } else if (idStr.length() == 32) { + RsPeerId peerId(idStr); + bool isSsl = false; + if (rsPeers) { + std::string peerName = rsPeers->getPeerName(peerId); + if (!peerName.empty() || rsPeers->isFriend(peerId)) { + isSsl = true; + } + } + if (isSsl) { + psidsSsl.insert(idStr); + } else { + psidsGxs.insert(idStr); + } + } } } - friendsWidget->setSelectedIdsFromString(FriendSelectionWidget::IDTYPE_GPG, psids, false); + friendsWidget->setSelectedIdsFromString(FriendSelectionWidget::IDTYPE_GPG, psidsGpg, false); + friendsWidget->setSelectedIdsFromString(FriendSelectionWidget::IDTYPE_GXS, psidsGxs, false); + friendsWidget->setSelectedIdsFromString(FriendSelectionWidget::IDTYPE_SSL, psidsSsl, false); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); @@ -387,20 +504,68 @@ void EventDialog::onInviteAttendees() { layout->addWidget(friendsWidget); layout->addWidget(buttonBox); - if (dialog.exec() == QDialog::Accepted) { - std::set selected; - friendsWidget->selectedIds(selected, false); + while (dialog.exec() == QDialog::Accepted) { + std::set selectedGpg; + friendsWidget->selectedIds(selectedGpg, false); + + std::set selectedGxs; + friendsWidget->selectedIds(selectedGxs, false); + + std::set selectedSsl; + friendsWidget->selectedIds(selectedSsl, false); + + int totalCount = 0; + for (const auto& id : selectedGpg) { + if (QString::fromStdString(id.toStdString()) != "0000000000000000") totalCount++; + } + for (const auto& id : selectedGxs) { + if (QString::fromStdString(id.toStdString()) != "00000000000000000000000000000000") totalCount++; + } + for (const auto& id : selectedSsl) { + if (QString::fromStdString(id.toStdString()) != "00000000000000000000000000000000") totalCount++; + } + + if (totalCount > 20) { + QMessageBox::warning(this, tr("Limit Exceeded"), tr("You can select a maximum of 20 attendees. Currently selected: %1").arg(totalCount)); + continue; + } mAttendeesList->clear(); - QMap contacts = CalendarData::getContacts(); - for (const auto& pgpId : selected) { + + for (const auto& pgpId : selectedGpg) { QString pgpIdStr = QString::fromStdString(pgpId.toStdString()); - QString name = contacts.value(pgpIdStr, pgpIdStr); - QListWidgetItem* item = new QListWidgetItem(name, mAttendeesList); - item->setData(Qt::UserRole, pgpIdStr); + if (pgpIdStr == "0000000000000000") continue; + QString name = getContactName(pgpIdStr); + QTreeWidgetItem* item = new QTreeWidgetItem(mAttendeesList); + item->setIcon(0, getContactAvatar(pgpIdStr)); + item->setText(0, name); + item->setData(0, Qt::UserRole, pgpIdStr); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(Qt::Checked); + item->setCheckState(0, Qt::Checked); } + + for (const auto& gxsId : selectedGxs) { + QString gxsIdStr = QString::fromStdString(gxsId.toStdString()); + if (gxsIdStr == "00000000000000000000000000000000") continue; + GxsIdRSTreeWidgetItem* item = new GxsIdRSTreeWidgetItem(nullptr, GxsIdDetails::ICON_TYPE_AVATAR, true, mAttendeesList); + item->setData(0, Qt::UserRole, gxsIdStr); + item->setId(gxsId, 0, true); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Checked); + } + + for (const auto& sslId : selectedSsl) { + QString sslIdStr = QString::fromStdString(sslId.toStdString()); + if (sslIdStr == "00000000000000000000000000000000") continue; + QString name = getContactName(sslIdStr); + QTreeWidgetItem* item = new QTreeWidgetItem(mAttendeesList); + item->setIcon(0, getContactAvatar(sslIdStr)); + item->setText(0, name); + item->setData(0, Qt::UserRole, sslIdStr); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Checked); + } + break; } // Switch to attendees tab @@ -432,14 +597,19 @@ void EventDialog::onSaveAndClose() { // Get checked attendees QStringList invitedNames; - for (int i = 0; i < mAttendeesList->count(); ++i) { - QListWidgetItem* item = mAttendeesList->item(i); - if (item->checkState() == Qt::Checked) { - ev.attendees.append(item->data(Qt::UserRole).toString()); - invitedNames.append(item->text()); + for (int i = 0; i < mAttendeesList->topLevelItemCount(); ++i) { + QTreeWidgetItem* item = mAttendeesList->topLevelItem(i); + if (item->checkState(0) == Qt::Checked) { + ev.attendees.append(item->data(0, Qt::UserRole).toString()); + invitedNames.append(item->text(0)); } } + if (ev.attendees.size() > 20) { + QMessageBox::warning(this, tr("Limit Exceeded"), tr("You can select a maximum of 20 attendees. Please uncheck some.")); + return; + } + // Get attachments for (int i = 0; i < mAttachmentsList->count(); ++i) { ev.attachments.append(mAttachmentsList->item(i)->data(Qt::UserRole).toString()); @@ -451,10 +621,9 @@ void EventDialog::onSaveAndClose() { CalendarData::instance()->updateEvent(ev); } - // Mock invitation mailing + // Send actual invitations if (mNotifyCheck->isChecked() && !invitedNames.isEmpty()) { - QMessageBox::information(this, tr("Invitations Sent"), - tr("Invitations successfully sent to: %1").arg(invitedNames.join(", "))); + sendInvite(ev, invitedNames); } accept(); @@ -542,3 +711,83 @@ void EventDialog::updateModeUi() { mSeparateCheck->setEnabled(!mReadOnly); mDisallowCheck->setEnabled(!mReadOnly); } + +void EventDialog::sendInvite(const CalendarEvent& ev, const QStringList& invitedNames) { + bool at_least_one_gxsid = false; + std::set destinations; + + for (const auto& contactId : ev.attendees) { + std::string idStr = contactId.toStdString(); + if (idStr.length() == 16) { + RsPgpId pgpId(idStr); + std::list sslIds; + if (rsPeers) { + rsPeers->getAssociatedSSLIds(pgpId, sslIds); + for (const auto& sslId : sslIds) { + destinations.insert(Rs::Mail::MsgAddress(sslId, Rs::Mail::MsgAddress::AddressMode::MSG_ADDRESS_MODE_TO)); + } + } + } else if (idStr.length() == 32) { + RsPeerId peerId(idStr); + bool isSsl = false; + if (rsPeers) { + std::string peerName = rsPeers->getPeerName(peerId); + if (!peerName.empty() || rsPeers->isFriend(peerId)) { + isSsl = true; + } + } + if (isSsl) { + destinations.insert(Rs::Mail::MsgAddress(peerId, Rs::Mail::MsgAddress::AddressMode::MSG_ADDRESS_MODE_TO)); + } else { + destinations.insert(Rs::Mail::MsgAddress(RsGxsId(idStr), Rs::Mail::MsgAddress::AddressMode::MSG_ADDRESS_MODE_TO)); + at_least_one_gxsid = true; + } + } + } + + if (destinations.empty()) { + return; + } + + Rs::Mail::MessageInfo mi; + mi.destinations = destinations; + mi.title = (tr("Invitation: %1").arg(ev.title)).toUtf8().constData(); + + // Construct invitation HTML message body + QString body; + body += "

" + tr("You are invited to a calendar event:") + "

"; + body += ""; + body += ""; + if (!ev.location.isEmpty()) { + body += ""; + } + body += ""; + if (!ev.description.isEmpty()) { + body += ""; + } + body += "
" + tr("Title:") + "" + ev.title + "
" + tr("Location:") + "" + ev.location + "
" + tr("Time:") + "" + ev.start.toString("yyyy-MM-dd hh:mm") + " - " + ev.end.toString("yyyy-MM-dd hh:mm") + "
" + tr("Description:") + "" + QString(ev.description).replace("\n", "
") + "
"; + mi.msg = body.toUtf8().constData(); + + if (!at_least_one_gxsid) { + if (rsPeers) { + mi.from = Rs::Mail::MsgAddress(rsPeers->getOwnId(), Rs::Mail::MsgAddress::AddressMode::MSG_ADDRESS_MODE_TO); + } + } else { + std::list own_ids; + if (rsIdentity) { + rsIdentity->getOwnIds(own_ids); + } + if (own_ids.empty()) { + QMessageBox::warning(this, tr("RetroShare"), tr("Please create an identity to sign distant messages, or remove GXS contacts from the attendee list."), QMessageBox::Ok); + return; + } + mi.from = Rs::Mail::MsgAddress(own_ids.front(), Rs::Mail::MsgAddress::AddressMode::MSG_ADDRESS_MODE_TO); + } + + if (rsMail && rsMail->MessageSend(mi)) { + QMessageBox::information(this, tr("Invitations Sent"), + tr("Invitations successfully sent to: %1").arg(invitedNames.join(", "))); + } else { + QMessageBox::warning(this, tr("Sending Failed"), tr("Failed to send invitations.")); + } +} diff --git a/retroshare-gui/src/gui/msgs/EventDialog.h b/retroshare-gui/src/gui/msgs/EventDialog.h index 0b29c79dd..4e7064bba 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.h +++ b/retroshare-gui/src/gui/msgs/EventDialog.h @@ -10,6 +10,7 @@ class QCheckBox; class QDateTimeEdit; class QTextEdit; class QListWidget; +class QTreeWidget; class QTabWidget; class EventDialog : public QDialog { @@ -31,6 +32,7 @@ private: void loadEvent(); void buildUi(); void updateModeUi(); + void sendInvite(const CalendarEvent& ev, const QStringList& invitedNames); QString mEventId; QDateTime mDefaultStart; @@ -51,7 +53,7 @@ private: QComboBox* mRepeatCombo; QComboBox* mReminderCombo; QTextEdit* mDescriptionEdit; - QListWidget* mAttendeesList; + QTreeWidget* mAttendeesList; QListWidget* mAttachmentsList; QPushButton* mAddAttachBtn; From eb735536b1e2104fddd5d6d4cc5c636348f86ea9 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:46:12 +0200 Subject: [PATCH 12/26] Removed not needed checkboxes --- retroshare-gui/src/gui/msgs/EventDialog.cpp | 28 +++++++++++++++------ retroshare-gui/src/gui/msgs/EventDialog.h | 22 ++++++++++++++-- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/EventDialog.cpp b/retroshare-gui/src/gui/msgs/EventDialog.cpp index df9582de4..ec13c5653 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.cpp +++ b/retroshare-gui/src/gui/msgs/EventDialog.cpp @@ -1,3 +1,23 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/EventDialog.cpp * + * * + * Copyright (C) 2026 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + #include "gui/msgs/EventDialog.h" #include #include "retroshare/rsgxsflags.h" @@ -318,12 +338,6 @@ void EventDialog::buildUi() { mNotifyCheck->setChecked(true); bottomCheckLayout->addWidget(mNotifyCheck); - mSeparateCheck = new QCheckBox(tr("Separate invitation per attendee"), this); - bottomCheckLayout->addWidget(mSeparateCheck); - - mDisallowCheck = new QCheckBox(tr("Disallow counter"), this); - bottomCheckLayout->addWidget(mDisallowCheck); - mainLayout->addLayout(bottomCheckLayout); // Bottom Buttons (Close & Edit for read-only view mode) @@ -708,8 +722,6 @@ void EventDialog::updateModeUi() { mAddAttachBtn->setVisible(!mReadOnly); mNotifyCheck->setEnabled(!mReadOnly); - mSeparateCheck->setEnabled(!mReadOnly); - mDisallowCheck->setEnabled(!mReadOnly); } void EventDialog::sendInvite(const CalendarEvent& ev, const QStringList& invitedNames) { diff --git a/retroshare-gui/src/gui/msgs/EventDialog.h b/retroshare-gui/src/gui/msgs/EventDialog.h index 4e7064bba..081f00bd0 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.h +++ b/retroshare-gui/src/gui/msgs/EventDialog.h @@ -1,3 +1,23 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/EventDialog.h * + * * + * Copyright (C) 2026 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + #ifndef EVENTDIALOG_H #define EVENTDIALOG_H @@ -63,8 +83,6 @@ private: QPushButton* mCloseBtn; QCheckBox* mNotifyCheck; - QCheckBox* mSeparateCheck; - QCheckBox* mDisallowCheck; }; #endif // EVENTDIALOG_H From e7686e063523c8e5c939f39f8d758e82a47d6166 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:33:59 +0200 Subject: [PATCH 13/26] calendar & tasks --- retroshare-gui/src/CMakeLists.txt | 14 + retroshare-gui/src/gui/msgs/CalendarData.cpp | 317 ++++++++++ retroshare-gui/src/gui/msgs/CalendarData.h | 113 ++++ .../src/gui/msgs/CalendarPropertiesDialog.cpp | 264 +++++++++ .../src/gui/msgs/CalendarPropertiesDialog.h | 79 +++ .../src/gui/msgs/CalendarWidget.cpp | 549 ++++++++++++++++++ retroshare-gui/src/gui/msgs/CalendarWidget.h | 90 +++ retroshare-gui/src/gui/msgs/CalendarWidget.ui | 329 +++++++++++ retroshare-gui/src/gui/msgs/EventDialog.cpp | 261 +++++++++ retroshare-gui/src/gui/msgs/EventDialog.h | 54 ++ .../src/gui/msgs/MessagesDialog.cpp | 66 ++- retroshare-gui/src/gui/msgs/MessagesDialog.h | 6 + retroshare-gui/src/gui/msgs/TaskDialog.cpp | 228 ++++++++ retroshare-gui/src/gui/msgs/TaskDialog.h | 49 ++ retroshare-gui/src/gui/msgs/TasksWidget.cpp | 361 ++++++++++++ retroshare-gui/src/gui/msgs/TasksWidget.h | 68 +++ retroshare-gui/src/gui/msgs/TasksWidget.ui | 177 ++++++ retroshare-gui/src/retroshare-gui.pro | 14 + 18 files changed, 3036 insertions(+), 3 deletions(-) create mode 100644 retroshare-gui/src/gui/msgs/CalendarData.cpp create mode 100644 retroshare-gui/src/gui/msgs/CalendarData.h create mode 100644 retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp create mode 100644 retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h create mode 100644 retroshare-gui/src/gui/msgs/CalendarWidget.cpp create mode 100644 retroshare-gui/src/gui/msgs/CalendarWidget.h create mode 100644 retroshare-gui/src/gui/msgs/CalendarWidget.ui create mode 100644 retroshare-gui/src/gui/msgs/EventDialog.cpp create mode 100644 retroshare-gui/src/gui/msgs/EventDialog.h create mode 100644 retroshare-gui/src/gui/msgs/TaskDialog.cpp create mode 100644 retroshare-gui/src/gui/msgs/TaskDialog.h create mode 100644 retroshare-gui/src/gui/msgs/TasksWidget.cpp create mode 100644 retroshare-gui/src/gui/msgs/TasksWidget.h create mode 100644 retroshare-gui/src/gui/msgs/TasksWidget.ui diff --git a/retroshare-gui/src/CMakeLists.txt b/retroshare-gui/src/CMakeLists.txt index d2025ae9c..907e21f2c 100644 --- a/retroshare-gui/src/CMakeLists.txt +++ b/retroshare-gui/src/CMakeLists.txt @@ -115,6 +115,12 @@ list( src/gui/connect/FriendRecommendDialog.cpp src/gui/msgs/MessagesDialog.cpp + src/gui/msgs/CalendarData.cpp + src/gui/msgs/CalendarWidget.cpp + src/gui/msgs/TasksWidget.cpp + src/gui/msgs/CalendarPropertiesDialog.cpp + src/gui/msgs/EventDialog.cpp + src/gui/msgs/TaskDialog.cpp src/gui/msgs/MessageComposer.cpp src/gui/msgs/MessageWidget.cpp src/gui/msgs/MessageWindow.cpp @@ -330,6 +336,8 @@ list( src/gui/msgs/MessageComposer.ui src/gui/msgs/MessageWindow.ui src/gui/msgs/MessageWidget.ui + src/gui/msgs/CalendarWidget.ui + src/gui/msgs/TasksWidget.ui src/gui/settings/settingsw.ui src/gui/settings/GeneralPage.ui @@ -544,6 +552,12 @@ list( src/gui/connect/FriendRecommendDialog.h src/gui/msgs/MessagesDialog.h + src/gui/msgs/CalendarData.h + src/gui/msgs/CalendarWidget.h + src/gui/msgs/TasksWidget.h + src/gui/msgs/CalendarPropertiesDialog.h + src/gui/msgs/EventDialog.h + src/gui/msgs/TaskDialog.h src/gui/msgs/MessageInterface.h src/gui/msgs/MessageComposer.h src/gui/msgs/MessageWindow.h diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp new file mode 100644 index 000000000..fe475a9ca --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -0,0 +1,317 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarData.cpp * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "gui/msgs/CalendarData.h" +#include +#include +#include +#include +#include + +CalendarData* CalendarData::mInstance = nullptr; + +CalendarData* CalendarData::instance() { + if (!mInstance) { + mInstance = new CalendarData(); + } + return mInstance; +} + +CalendarData::CalendarData() { + loadData(); +} + +CalendarData::~CalendarData() { + saveData(); +} + +void CalendarData::loadData() { + mCalendars.clear(); + mEvents.clear(); + mTasks.clear(); + + QString accountDir = QString::fromStdString(RsAccounts::AccountDirectory()); + QString path = accountDir + "/calendar.conf"; + + QSettings settings(path, QSettings::IniFormat); + + // Load Calendars + int calSize = settings.beginReadArray("calendars"); + for (int i = 0; i < calSize; ++i) { + settings.setArrayIndex(i); + CalendarInfo cal; + cal.id = settings.value("id").toString(); + cal.name = settings.value("name").toString(); + cal.color = QColor(settings.value("color").toString()); + cal.isPublic = settings.value("isPublic").toBool(); + cal.owner = settings.value("owner").toString(); + cal.showReminders = settings.value("showReminders", true).toBool(); + cal.email = settings.value("email", "").toString(); + cal.onNetwork = settings.value("onNetwork", false).toBool(); + mCalendars.append(cal); + } + settings.endArray(); + + // Ensure we have at least one default calendar + if (mCalendars.isEmpty()) { + CalendarInfo defaultCal; + defaultCal.id = "personal"; + defaultCal.name = "Privat"; + defaultCal.color = QColor("#4a90e2"); + defaultCal.isPublic = false; + defaultCal.owner = "local"; + defaultCal.showReminders = true; + defaultCal.email = "defnator "; + defaultCal.onNetwork = false; + mCalendars.append(defaultCal); + + CalendarInfo testCal; + testCal.id = "test"; + testCal.name = "test"; + testCal.color = QColor("#50e3c2"); + testCal.isPublic = true; + testCal.owner = "local"; + testCal.showReminders = true; + testCal.email = "defnator "; + testCal.onNetwork = true; + mCalendars.append(testCal); + } + + // Load Events + int eventSize = settings.beginReadArray("events"); + for (int i = 0; i < eventSize; ++i) { + settings.setArrayIndex(i); + CalendarEvent ev; + ev.id = settings.value("id").toString(); + ev.calendarId = settings.value("calendarId").toString(); + ev.title = settings.value("title").toString(); + ev.location = settings.value("location").toString(); + ev.category = settings.value("category").toString(); + ev.allDay = settings.value("allDay").toBool(); + ev.start = settings.value("start").toDateTime(); + ev.end = settings.value("end").toDateTime(); + ev.repeat = settings.value("repeat").toString(); + ev.reminder = settings.value("reminder").toString(); + ev.description = settings.value("description").toString(); + ev.attendees = settings.value("attendees").toStringList(); + ev.isPublic = settings.value("isPublic").toBool(); + mEvents.append(ev); + } + settings.endArray(); + + // Load Tasks + int taskSize = settings.beginReadArray("tasks"); + for (int i = 0; i < taskSize; ++i) { + settings.setArrayIndex(i); + CalendarTask task; + task.id = settings.value("id").toString(); + task.calendarId = settings.value("calendarId").toString(); + task.title = settings.value("title").toString(); + task.location = settings.value("location").toString(); + task.category = settings.value("category").toString(); + task.hasStart = settings.value("hasStart").toBool(); + task.start = settings.value("start").toDateTime(); + task.hasDue = settings.value("hasDue").toBool(); + task.due = settings.value("due").toDateTime(); + task.status = settings.value("status").toString(); + task.percentComplete = settings.value("percentComplete").toInt(); + task.repeat = settings.value("repeat").toString(); + task.reminder = settings.value("reminder").toString(); + task.description = settings.value("description").toString(); + task.completed = settings.value("completed").toBool(); + mTasks.append(task); + } + settings.endArray(); +} + +void CalendarData::saveData() { + QString accountDir = QString::fromStdString(RsAccounts::AccountDirectory()); + QString path = accountDir + "/calendar.conf"; + + QSettings settings(path, QSettings::IniFormat); + + // Save Calendars + settings.beginWriteArray("calendars"); + for (int i = 0; i < mCalendars.size(); ++i) { + settings.setArrayIndex(i); + settings.setValue("id", mCalendars[i].id); + settings.setValue("name", mCalendars[i].name); + settings.setValue("color", mCalendars[i].color.name()); + settings.setValue("isPublic", mCalendars[i].isPublic); + settings.setValue("owner", mCalendars[i].owner); + settings.setValue("showReminders", mCalendars[i].showReminders); + settings.setValue("email", mCalendars[i].email); + settings.setValue("onNetwork", mCalendars[i].onNetwork); + } + settings.endArray(); + + // Save Events + settings.beginWriteArray("events"); + for (int i = 0; i < mEvents.size(); ++i) { + settings.setArrayIndex(i); + settings.setValue("id", mEvents[i].id); + settings.setValue("calendarId", mEvents[i].calendarId); + settings.setValue("title", mEvents[i].title); + settings.setValue("location", mEvents[i].location); + settings.setValue("category", mEvents[i].category); + settings.setValue("allDay", mEvents[i].allDay); + settings.setValue("start", mEvents[i].start); + settings.setValue("end", mEvents[i].end); + settings.setValue("repeat", mEvents[i].repeat); + settings.setValue("reminder", mEvents[i].reminder); + settings.setValue("description", mEvents[i].description); + settings.setValue("attendees", mEvents[i].attendees); + settings.setValue("isPublic", mEvents[i].isPublic); + } + settings.endArray(); + + // Save Tasks + settings.beginWriteArray("tasks"); + for (int i = 0; i < mTasks.size(); ++i) { + settings.setArrayIndex(i); + settings.setValue("id", mTasks[i].id); + settings.setValue("calendarId", mTasks[i].calendarId); + settings.setValue("title", mTasks[i].title); + settings.setValue("location", mTasks[i].location); + settings.setValue("category", mTasks[i].category); + settings.setValue("hasStart", mTasks[i].hasStart); + settings.setValue("start", mTasks[i].start); + settings.setValue("hasDue", mTasks[i].hasDue); + settings.setValue("due", mTasks[i].due); + settings.setValue("status", mTasks[i].status); + settings.setValue("percentComplete", mTasks[i].percentComplete); + settings.setValue("repeat", mTasks[i].repeat); + settings.setValue("reminder", mTasks[i].reminder); + settings.setValue("description", mTasks[i].description); + settings.setValue("completed", mTasks[i].completed); + } + settings.endArray(); + + settings.sync(); +} + +void CalendarData::addCalendar(const CalendarInfo& cal) { + mCalendars.append(cal); + saveData(); +} + +void CalendarData::updateCalendar(const CalendarInfo& cal) { + for (int i = 0; i < mCalendars.size(); ++i) { + if (mCalendars[i].id == cal.id) { + mCalendars[i] = cal; + break; + } + } + saveData(); +} + +void CalendarData::removeCalendar(const QString& id) { + for (int i = 0; i < mCalendars.size(); ++i) { + if (mCalendars[i].id == id) { + mCalendars.removeAt(i); + break; + } + } + + // Remove associated events and tasks + mEvents.erase(std::remove_if(mEvents.begin(), mEvents.end(), + [&id](const CalendarEvent& ev) { return ev.calendarId == id; }), mEvents.end()); + mTasks.erase(std::remove_if(mTasks.begin(), mTasks.end(), + [&id](const CalendarTask& t) { return t.calendarId == id; }), mTasks.end()); + + saveData(); +} + +void CalendarData::addEvent(const CalendarEvent& ev) { + mEvents.append(ev); + saveData(); +} + +void CalendarData::updateEvent(const CalendarEvent& ev) { + for (int i = 0; i < mEvents.size(); ++i) { + if (mEvents[i].id == ev.id) { + mEvents[i] = ev; + break; + } + } + saveData(); +} + +void CalendarData::removeEvent(const QString& id) { + for (int i = 0; i < mEvents.size(); ++i) { + if (mEvents[i].id == id) { + mEvents.removeAt(i); + break; + } + } + saveData(); +} + +void CalendarData::addTask(const CalendarTask& task) { + mTasks.append(task); + saveData(); +} + +void CalendarData::updateTask(const CalendarTask& task) { + for (int i = 0; i < mTasks.size(); ++i) { + if (mTasks[i].id == task.id) { + mTasks[i] = task; + break; + } + } + saveData(); +} + +void CalendarData::removeTask(const QString& id) { + for (int i = 0; i < mTasks.size(); ++i) { + if (mTasks[i].id == id) { + mTasks.removeAt(i); + break; + } + } + saveData(); +} + +QMap CalendarData::getContacts() { + QMap contacts; + + if (!rsPeers) { + return contacts; + } + + std::list pgpIds; + rsPeers->getGPGAcceptedList(pgpIds); + + for (const auto& pgpId : pgpIds) { + RsPeerDetails details; + if (rsPeers->getGPGDetails(pgpId, details)) { + contacts.insert(QString::fromStdString(pgpId.toStdString()), QString::fromUtf8(details.name.c_str())); + } + } + + // Fallbacks/Mocks if empty (to make sure it lists some developers/coworkers as requested in the screenshots) + if (contacts.isEmpty()) { + contacts.insert("friend1", "Alice (Developer)"); + contacts.insert("friend2", "Bob (Coworker)"); + contacts.insert("friend3", "Charlie (Friend)"); + } + + return contacts; +} diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/msgs/CalendarData.h new file mode 100644 index 000000000..63109d455 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarData.h @@ -0,0 +1,113 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarData.h * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef CALENDARDATA_H +#define CALENDARDATA_H + +#include +#include +#include +#include +#include +#include + +struct CalendarInfo { + QString id; + QString name; + QColor color; + bool isPublic; + QString owner; // contact PGP ID or "local" + bool showReminders; + QString email; + bool onNetwork; +}; + +struct CalendarEvent { + QString id; + QString calendarId; + QString title; + QString location; + QString category; + bool allDay; + QDateTime start; + QDateTime end; + QString repeat; + QString reminder; + QString description; + QStringList attendees; // PGP IDs of contacts + bool isPublic; +}; + +struct CalendarTask { + QString id; + QString calendarId; + QString title; + QString location; + QString category; + bool hasStart; + QDateTime start; + bool hasDue; + QDateTime due; + QString status; + int percentComplete; + QString repeat; + QString reminder; + QString description; + bool completed; +}; + +class CalendarData { +public: + static CalendarData* instance(); + + void loadData(); + void saveData(); + + const QList& getCalendars() const { return mCalendars; } + const QList& getEvents() const { return mEvents; } + const QList& getTasks() const { return mTasks; } + + void addCalendar(const CalendarInfo& cal); + void updateCalendar(const CalendarInfo& cal); + void removeCalendar(const QString& id); + + void addEvent(const CalendarEvent& ev); + void updateEvent(const CalendarEvent& ev); + void removeEvent(const QString& id); + + void addTask(const CalendarTask& task); + void updateTask(const CalendarTask& task); + void removeTask(const QString& id); + + // Helpers + static QMap getContacts(); // map PGP ID -> Name + +private: + CalendarData(); + ~CalendarData(); + + QList mCalendars; + QList mEvents; + QList mTasks; + + static CalendarData* mInstance; +}; + +#endif // CALENDARDATA_H diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp new file mode 100644 index 000000000..1cebf2a8f --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp @@ -0,0 +1,264 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "gui/msgs/CalendarPropertiesDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget* parent) + : QDialog(parent), mCalId(calId), mEditMode(!calId.isEmpty()), mSelectedColor(QColor("#4a90e2")) +{ + setupUi(); + loadIdentities(); + + if (mEditMode) { + setWindowTitle(tr("Calendar Properties")); + // Load existing calendar info + const auto& cals = CalendarData::instance()->getCalendars(); + CalendarInfo existingCal; + bool found = false; + for (const auto& c : cals) { + if (c.id == mCalId) { + existingCal = c; + found = true; + break; + } + } + if (found) { + mNameEdit->setText(existingCal.name); + mSelectedColor = existingCal.color; + mRemindersCheckBox->setChecked(existingCal.showReminders); + mRadioNetwork->setChecked(existingCal.onNetwork); + mRadioComputer->setChecked(!existingCal.onNetwork); + + // Try to find the email in the combo box + int idx = mEmailCombo->findText(existingCal.email); + if (idx != -1) { + mEmailCombo->setCurrentIndex(idx); + } else if (!existingCal.email.isEmpty()) { + mEmailCombo->addItem(existingCal.email); + mEmailCombo->setCurrentIndex(mEmailCombo->count() - 1); + } + } + updateColorButton(); + mStackedWidget->setCurrentWidget(mPage2); + } else { + setWindowTitle(tr("Create New Calendar")); + mStackedWidget->setCurrentWidget(mPage1); + } +} + +CalendarPropertiesDialog::~CalendarPropertiesDialog() {} + +void CalendarPropertiesDialog::setupUi() { + // Set fixed size matching the mockup aspect ratio + setMinimumSize(480, 360); + resize(500, 380); + + QVBoxLayout* dialogLayout = new QVBoxLayout(this); + dialogLayout->setContentsMargins(15, 15, 15, 15); + dialogLayout->setSpacing(15); + + mStackedWidget = new QStackedWidget(this); + + // ================= PAGE 1 (Wizard Step 1) ================= + mPage1 = new QWidget(this); + QVBoxLayout* page1Layout = new QVBoxLayout(mPage1); + page1Layout->setContentsMargins(5, 5, 5, 5); + page1Layout->setSpacing(15); + + QLabel* descLabel = new QLabel( + tr("Your calendar can be stored on your computer or be stored on a server in order to access it remotely or share it with your friends or co-workers."), + mPage1 + ); + descLabel->setWordWrap(true); + descLabel->setStyleSheet("font-size: 13px; line-height: 1.4;"); + page1Layout->addWidget(descLabel); + + mRadioComputer = new QRadioButton(tr("On My Computer"), mPage1); + mRadioComputer->setChecked(true); + mRadioComputer->setStyleSheet("font-size: 13px; font-weight: bold; padding: 5px;"); + page1Layout->addWidget(mRadioComputer); + + mRadioNetwork = new QRadioButton(tr("On the Network"), mPage1); + mRadioNetwork->setStyleSheet("font-size: 13px; font-weight: bold; padding: 5px;"); + page1Layout->addWidget(mRadioNetwork); + + page1Layout->addStretch(); + mStackedWidget->addWidget(mPage1); + + // ================= PAGE 2 (Wizard Step 2) ================= + mPage2 = new QWidget(this); + QVBoxLayout* page2Layout = new QVBoxLayout(mPage2); + page2Layout->setContentsMargins(5, 5, 5, 5); + page2Layout->setSpacing(15); + + QFormLayout* formLayout = new QFormLayout(); + formLayout->setSpacing(12); + formLayout->setLabelAlignment(Qt::AlignRight); + + mNameEdit = new QLineEdit(mPage2); + mNameEdit->setMinimumHeight(26); + formLayout->addRow(tr("Calendar Name:"), mNameEdit); + + mColorBtn = new QPushButton(mPage2); + mColorBtn->setFixedWidth(80); + mColorBtn->setCursor(Qt::PointingHandCursor); + updateColorButton(); + connect(mColorBtn, SIGNAL(clicked()), this, SLOT(onSelectColor())); + formLayout->addRow(tr("Colour:"), mColorBtn); + + mRemindersCheckBox = new QCheckBox(tr("Show Reminders"), mPage2); + mRemindersCheckBox->setChecked(true); + formLayout->addRow(QString(), mRemindersCheckBox); + + mEmailCombo = new QComboBox(mPage2); + mEmailCombo->setMinimumHeight(26); + formLayout->addRow(tr("Email:"), mEmailCombo); + + page2Layout->addLayout(formLayout); + page2Layout->addStretch(); + mStackedWidget->addWidget(mPage2); + + dialogLayout->addWidget(mStackedWidget); + + // ================= BUTTONS ROW ================= + QHBoxLayout* buttonLayout = new QHBoxLayout(); + mBackBtn = new QPushButton(tr("Back"), this); + mNextBtn = new QPushButton(tr("Next"), this); + mCreateOrSaveBtn = new QPushButton(mEditMode ? tr("OK") : tr("Create Calendar"), this); + mCancelBtn = new QPushButton(tr("Cancel"), this); + + connect(mBackBtn, SIGNAL(clicked()), this, SLOT(onBack())); + connect(mNextBtn, SIGNAL(clicked()), this, SLOT(onNext())); + connect(mCreateOrSaveBtn, SIGNAL(clicked()), this, SLOT(onAccept())); + connect(mCancelBtn, SIGNAL(clicked()), this, SLOT(reject())); + + // Layout buttons correctly + buttonLayout->addStretch(); + buttonLayout->addWidget(mBackBtn); + buttonLayout->addWidget(mNextBtn); + buttonLayout->addWidget(mCreateOrSaveBtn); + buttonLayout->addWidget(mCancelBtn); + dialogLayout->addLayout(buttonLayout); + + // Update buttons visibility depending on state + if (mEditMode) { + mBackBtn->hide(); + mNextBtn->hide(); + } else { + mBackBtn->hide(); + mCreateOrSaveBtn->hide(); + } +} + +void CalendarPropertiesDialog::updateColorButton() { + mColorBtn->setStyleSheet(QString( + "background-color: %1; border: 1px solid #ababab; border-radius: 3px; min-height: 20px;" + ).arg(mSelectedColor.name())); +} + +void CalendarPropertiesDialog::loadIdentities() { + mEmailCombo->clear(); + QStringList emails; + + if (rsIdentity) { + std::list own_identities; + rsIdentity->getOwnIds(own_identities); + for (const auto& id : own_identities) { + RsIdentityDetails details; + if (rsIdentity->getIdDetails(id, details)) { + QString nickname = QString::fromUtf8(details.mNickname.c_str()).trimmed(); + QString gxsId = QString::fromStdString(id.toStdString()); + if (!nickname.isEmpty()) { + emails.append(QString("%1 <%1@%2>").arg(nickname).arg(gxsId)); + } + } + } + } + + mEmailCombo->addItems(emails); +} + +void CalendarPropertiesDialog::onNext() { + mStackedWidget->setCurrentWidget(mPage2); + mBackBtn->show(); + mCreateOrSaveBtn->show(); + mNextBtn->hide(); +} + +void CalendarPropertiesDialog::onBack() { + mStackedWidget->setCurrentWidget(mPage1); + mBackBtn->hide(); + mCreateOrSaveBtn->hide(); + mNextBtn->show(); +} + +void CalendarPropertiesDialog::onSelectColor() { + QColor color = QColorDialog::getColor(mSelectedColor, this, tr("Select Calendar Color")); + if (color.isValid()) { + mSelectedColor = color; + updateColorButton(); + } +} + +void CalendarPropertiesDialog::onAccept() { + QString name = mNameEdit->text().trimmed(); + if (name.isEmpty()) { + QMessageBox::warning(this, tr("Invalid Name"), tr("Please enter a name for the calendar.")); + return; + } + + CalendarInfo info = getCalendarInfo(); + + if (mEditMode) { + CalendarData::instance()->updateCalendar(info); + } else { + CalendarData::instance()->addCalendar(info); + } + + accept(); +} + +CalendarInfo CalendarPropertiesDialog::getCalendarInfo() const { + CalendarInfo info; + info.id = mCalId.isEmpty() ? QUuid::createUuid().toString(QUuid::WithoutBraces) : mCalId; + info.name = mNameEdit->text().trimmed(); + info.color = mSelectedColor; + info.onNetwork = mRadioNetwork->isChecked(); + info.isPublic = info.onNetwork; + info.showReminders = mRemindersCheckBox->isChecked(); + info.email = mEmailCombo->currentText(); + info.owner = "local"; + return info; +} diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h new file mode 100644 index 000000000..158d8ee68 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h @@ -0,0 +1,79 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef CALENDARPROPERTIESDIALOG_H +#define CALENDARPROPERTIESDIALOG_H + +#include +#include +#include "gui/msgs/CalendarData.h" + +class QStackedWidget; +class QRadioButton; +class QLineEdit; +class QPushButton; +class QCheckBox; +class QComboBox; + +class CalendarPropertiesDialog : public QDialog { + Q_OBJECT +public: + CalendarPropertiesDialog(const QString& calId = "", QWidget* parent = nullptr); + ~CalendarPropertiesDialog(); + + CalendarInfo getCalendarInfo() const; + +private slots: + void onNext(); + void onBack(); + void onSelectColor(); + void onAccept(); + +private: + void setupUi(); + void loadIdentities(); + void updateColorButton(); + + QString mCalId; + bool mEditMode; + QColor mSelectedColor; + + QStackedWidget* mStackedWidget; + QWidget* mPage1; + QWidget* mPage2; + + // Page 1 widgets + QRadioButton* mRadioComputer; + QRadioButton* mRadioNetwork; + + // Page 2 widgets + QLineEdit* mNameEdit; + QPushButton* mColorBtn; + QCheckBox* mRemindersCheckBox; + QComboBox* mEmailCombo; + + // Buttons + QPushButton* mNextBtn; + QPushButton* mBackBtn; + QPushButton* mCreateOrSaveBtn; + QPushButton* mCancelBtn; +}; + +#endif // CALENDARPROPERTIESDIALOG_H diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp new file mode 100644 index 000000000..2467c0530 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -0,0 +1,549 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarWidget.cpp * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "gui/msgs/CalendarWidget.h" +#include "gui/msgs/EventDialog.h" +#include "gui/msgs/CalendarPropertiesDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CalendarWidget::CalendarWidget(QWidget* parent) + : QWidget(parent), mSelectedDate(QDate::currentDate()), mCurrentViewMode(2) // Default to Month View +{ + buildUi(); + refreshData(); +} + +CalendarWidget::~CalendarWidget() {} + +void CalendarWidget::buildUi() { + ui.setupUi(this); + + // Initialize UI pointers + mSidebarCalendar = ui.sidebarCalendar; + mCalendarList = ui.calendarList; + mPeriodLabel = ui.periodLabel; + mSearchEdit = ui.searchEdit; + mEventTable = ui.eventTable; + mViewStack = ui.viewStack; + mDayTable = ui.dayTable; + mWeekTable = ui.weekTable; + mMonthTable = ui.monthTable; + + // Sidebar Calendar configs + mSidebarCalendar->setSelectedDate(mSelectedDate); + + // Event table configs + mEventTable->setColumnCount(5); + mEventTable->setHorizontalHeaderLabels({tr("Title"), tr("Start"), tr("End"), tr("Category"), tr("Calendar")}); + mEventTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + mEventTable->verticalHeader()->setVisible(false); + mEventTable->setSelectionBehavior(QAbstractItemView::SelectRows); + mEventTable->setSelectionMode(QAbstractItemView::SingleSelection); + mEventTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + mEventTable->setMaximumHeight(120); + connect(mEventTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + + // Stacked widget pages setup + // 1. Day Table + mDayTable->setColumnCount(2); + mDayTable->setHorizontalHeaderLabels({tr("Time"), tr("Events")}); + mDayTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed); + mDayTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + mDayTable->horizontalHeader()->resizeSection(0, 80); + mDayTable->verticalHeader()->setVisible(false); + mDayTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + connect(mDayTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + + // 2. Week Table + mWeekTable->setColumnCount(7); + mWeekTable->setHorizontalHeaderLabels({tr("Mon"), tr("Tue"), tr("Wed"), tr("Thu"), tr("Fri"), tr("Sat"), tr("Sun")}); + mWeekTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + mWeekTable->verticalHeader()->setVisible(false); + mWeekTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + connect(mWeekTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + + // 3. Month Table + mMonthTable->setColumnCount(7); + mMonthTable->setHorizontalHeaderLabels({tr("Mon"), tr("Tue"), tr("Wed"), tr("Thu"), tr("Fri"), tr("Sat"), tr("Sun")}); + mMonthTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + mMonthTable->verticalHeader()->setVisible(false); + mMonthTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + connect(mMonthTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + + mViewStack->setCurrentIndex(mCurrentViewMode); + + // Set splitter sizes or stretch factors + ui.splitter->setStretchFactor(0, 0); + ui.splitter->setStretchFactor(1, 1); + + // Connect sidebar signals + connect(ui.newEventBtn, SIGNAL(clicked()), this, SLOT(onNewEvent())); + connect(mSidebarCalendar, SIGNAL(clicked(const QDate&)), this, SLOT(onDateSelected(const QDate&))); + connect(mCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCalendarSelectionChanged(QListWidgetItem*))); + connect(mCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onCalendarContextMenu(const QPoint&))); + connect(ui.newCalBtn, SIGNAL(clicked()), this, SLOT(onNewCalendar())); + + // Connect top control signals + connect(ui.prevBtn, SIGNAL(clicked()), this, SLOT(onPrevPeriod())); + connect(ui.todayBtn, SIGNAL(clicked()), this, SLOT(onToday())); + connect(ui.nextBtn, SIGNAL(clicked()), this, SLOT(onNextPeriod())); + connect(mSearchEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onSearchChanged(const QString&))); + + // View selector buttons + connect(ui.dayViewBtn, &QPushButton::clicked, [this]() { + ui.dayViewBtn->setChecked(true); ui.weekViewBtn->setChecked(false); ui.monthViewBtn->setChecked(false); + onViewChanged(0); + }); + connect(ui.weekViewBtn, &QPushButton::clicked, [this]() { + ui.dayViewBtn->setChecked(false); ui.weekViewBtn->setChecked(true); ui.monthViewBtn->setChecked(false); + onViewChanged(1); + }); + connect(ui.monthViewBtn, &QPushButton::clicked, [this]() { + ui.dayViewBtn->setChecked(false); ui.weekViewBtn->setChecked(false); ui.monthViewBtn->setChecked(true); + onViewChanged(2); + }); +} + +void CalendarWidget::refreshData() { + // Save current check states + QMap checkedStates; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + } + + // Populate Calendar selection list + mCalendarList->blockSignals(true); + mCalendarList->clear(); + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); + item->setData(Qt::UserRole, cal.id); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + // Render colored bullet point icon + QPixmap pix(12, 12); + pix.fill(cal.color); + item->setIcon(QIcon(pix)); + + // Restore checked state + if (checkedStates.contains(cal.id)) { + item->setCheckState(checkedStates[cal.id]); + } else { + item->setCheckState(Qt::Checked); + } + } + mCalendarList->blockSignals(false); + + updateViews(); +} + +void CalendarWidget::updateViews() { + mCellEventMap.clear(); + + // 1. Update the Period Label + if (mCurrentViewMode == 0) { // Day View + mPeriodLabel->setText(mSelectedDate.toString("dd MMMM yyyy")); + } else if (mCurrentViewMode == 1) { // Week View + QDate monday = mSelectedDate.addDays(-(mSelectedDate.dayOfWeek() - 1)); + QDate sunday = monday.addDays(6); + if (monday.month() == sunday.month()) { + mPeriodLabel->setText(monday.toString("dd") + " - " + sunday.toString("dd") + " " + monday.toString("MMMM yyyy")); + } else { + mPeriodLabel->setText(monday.toString("dd MMM") + " - " + sunday.toString("dd MMM") + " " + sunday.toString("yyyy")); + } + } else { // Month View + mPeriodLabel->setText(mSelectedDate.toString("MMMM yyyy")); + } + + // 2. Load and Filter Active Events + updateEventList(); + + // 3. Render Stacked Calendar Views + if (mCurrentViewMode == 0) { + updateDayView(); + } else if (mCurrentViewMode == 1) { + updateWeekView(); + } else { + updateMonthView(); + } +} + +void CalendarWidget::updateEventList() { + mEventTable->setRowCount(0); + + const auto& events = CalendarData::instance()->getEvents(); + const auto& cals = CalendarData::instance()->getCalendars(); + + // Get enabled calendar IDs + QStringList enabledCalIds; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) { + enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + } + + int row = 0; + for (const auto& ev : events) { + if (!enabledCalIds.contains(ev.calendarId)) continue; + + // Search text filter + if (!mSearchText.isEmpty() && !ev.title.contains(mSearchText, Qt::CaseInsensitive) && + !ev.description.contains(mSearchText, Qt::CaseInsensitive)) { + continue; + } + + mEventTable->insertRow(row); + + QTableWidgetItem* titleItem = new QTableWidgetItem(ev.title); + titleItem->setData(Qt::UserRole, ev.id); + mEventTable->setItem(row, 0, titleItem); + + mEventTable->setItem(row, 1, new QTableWidgetItem(ev.start.toString("yyyy-MM-dd hh:mm"))); + mEventTable->setItem(row, 2, new QTableWidgetItem(ev.end.toString("yyyy-MM-dd hh:mm"))); + mEventTable->setItem(row, 3, new QTableWidgetItem(ev.category)); + + // Get calendar name + QString calName = ""; + for (const auto& c : cals) { + if (c.id == ev.calendarId) { + calName = c.name; + break; + } + } + mEventTable->setItem(row, 4, new QTableWidgetItem(calName)); + row++; + } +} + +void CalendarWidget::updateDayView() { + mDayTable->setRowCount(0); + mDayTable->setRowCount(24); + + // List of events for the selected day + const auto& events = CalendarData::instance()->getEvents(); + + QStringList enabledCalIds; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + + for (int hour = 0; hour < 24; ++hour) { + QString timeText = QString("%1:00").arg(hour, 2, 10, QChar('0')); + mDayTable->setItem(hour, 0, new QTableWidgetItem(timeText)); + + // Match events starting or active during this hour + QStringList matchedEvents; + QString lastEventId = ""; + for (const auto& ev : events) { + if (!enabledCalIds.contains(ev.calendarId)) continue; + if (ev.start.date() == mSelectedDate && ev.start.time().hour() == hour) { + matchedEvents.append(ev.title); + lastEventId = ev.id; + } + } + + QTableWidgetItem* evCell = new QTableWidgetItem(matchedEvents.join(", ")); + if (!lastEventId.isEmpty()) { + mCellEventMap[QString("0_%1_%2").arg(hour).arg(1)] = lastEventId; + evCell->setBackground(QBrush(QColor("#eef5fc"))); + } + mDayTable->setItem(hour, 1, evCell); + } +} + +void CalendarWidget::updateWeekView() { + mWeekTable->setRowCount(0); + mWeekTable->setRowCount(8); // Max events rows per week + + // Get current Monday + QDate monday = mSelectedDate.addDays(-(mSelectedDate.dayOfWeek() - 1)); + + // Update column headers with dates + QStringList headers; + for (int i = 0; i < 7; ++i) { + headers << monday.addDays(i).toString("ddd dd/MM"); + } + mWeekTable->setHorizontalHeaderLabels(headers); + + const auto& events = CalendarData::instance()->getEvents(); + + QStringList enabledCalIds; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + + // Populate week cells + for (int dayIdx = 0; dayIdx < 7; ++dayIdx) { + QDate date = monday.addDays(dayIdx); + int rowIdx = 0; + for (const auto& ev : events) { + if (!enabledCalIds.contains(ev.calendarId)) continue; + if (ev.start.date() == date) { + if (rowIdx >= mWeekTable->rowCount()) mWeekTable->insertRow(rowIdx); + QTableWidgetItem* cellItem = new QTableWidgetItem(ev.title); + cellItem->setBackground(QBrush(QColor("#eef5fc"))); + mWeekTable->setItem(rowIdx, dayIdx, cellItem); + mCellEventMap[QString("1_%1_%2").arg(rowIdx).arg(dayIdx)] = ev.id; + rowIdx++; + } + } + } +} + +void CalendarWidget::updateMonthView() { + mMonthTable->setRowCount(6); // A month calendar grid needs up to 6 rows + + // Find first day of the month + QDate firstOfMonth(mSelectedDate.year(), mSelectedDate.month(), 1); + int startDayOfWeek = firstOfMonth.dayOfWeek(); // 1=Mon, 7=Sun + QDate startDate = firstOfMonth.addDays(-(startDayOfWeek - 1)); + + const auto& events = CalendarData::instance()->getEvents(); + + QStringList enabledCalIds; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + + for (int row = 0; row < 6; ++row) { + for (int col = 0; col < 7; ++col) { + QDate date = startDate.addDays(row * 7 + col); + + // Build cell contents: "Date \n Event1 \n Event2..." + QStringList cellLines; + cellLines << QString::number(date.day()); + + QString matchedEventId = ""; + for (const auto& ev : events) { + if (!enabledCalIds.contains(ev.calendarId)) continue; + if (ev.start.date() == date) { + cellLines << ev.title; + matchedEventId = ev.id; + } + } + + QTableWidgetItem* cellItem = new QTableWidgetItem(cellLines.join("\n")); + if (date.month() != mSelectedDate.month()) { + cellItem->setForeground(QBrush(Qt::gray)); + } + if (!matchedEventId.isEmpty()) { + cellItem->setBackground(QBrush(QColor("#eef5fc"))); + mCellEventMap[QString("2_%1_%2").arg(row).arg(col)] = matchedEventId; + } + mMonthTable->setItem(row, col, cellItem); + } + } + + // Set row heights to expand nicely in the month grid + for (int row = 0; row < 6; ++row) { + mMonthTable->setRowHeight(row, 60); + } +} + +void CalendarWidget::onDateSelected(const QDate& date) { + mSelectedDate = date; + updateViews(); +} + +void CalendarWidget::onPrevPeriod() { + if (mCurrentViewMode == 0) { // Day + mSelectedDate = mSelectedDate.addDays(-1); + } else if (mCurrentViewMode == 1) { // Week + mSelectedDate = mSelectedDate.addDays(-7); + } else { // Month + mSelectedDate = mSelectedDate.addMonths(-1); + } + mSidebarCalendar->setSelectedDate(mSelectedDate); + updateViews(); +} + +void CalendarWidget::onNextPeriod() { + if (mCurrentViewMode == 0) { // Day + mSelectedDate = mSelectedDate.addDays(1); + } else if (mCurrentViewMode == 1) { // Week + mSelectedDate = mSelectedDate.addDays(7); + } else { // Month + mSelectedDate = mSelectedDate.addMonths(1); + } + mSidebarCalendar->setSelectedDate(mSelectedDate); + updateViews(); +} + +void CalendarWidget::onToday() { + mSelectedDate = QDate::currentDate(); + mSidebarCalendar->setSelectedDate(mSelectedDate); + updateViews(); +} + +void CalendarWidget::onViewChanged(int index) { + mCurrentViewMode = index; + mViewStack->setCurrentIndex(mCurrentViewMode); + updateViews(); +} + +void CalendarWidget::onNewEvent() { + // Open Dialog + QDateTime defaultStart(mSelectedDate, QTime(QTime::currentTime().hour(), 0)); + EventDialog dlg("", defaultStart, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } +} + +void CalendarWidget::onNewCalendar() { + CalendarPropertiesDialog dlg("", this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } +} + +void CalendarWidget::onEventSelected(int row, int col) { + QObject* senderObj = sender(); + QString eventId = ""; + + if (senderObj == mEventTable) { + QTableWidgetItem* titleItem = mEventTable->item(row, 0); + if (titleItem) eventId = titleItem->data(Qt::UserRole).toString(); + } else { + QString key = QString("%1_%2_%3").arg(mCurrentViewMode).arg(row).arg(col); + if (mCellEventMap.contains(key)) { + eventId = mCellEventMap[key]; + } + } + + // If double clicked a cell/row containing an event, edit it. Otherwise create a new one. + if (!eventId.isEmpty()) { + EventDialog dlg(eventId, QDateTime::currentDateTime(), this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } else { + // Create new event on the clicked cell's date + QDateTime startDateTime = QDateTime::currentDateTime(); + if (mCurrentViewMode == 1) { // Week View + QDate monday = mSelectedDate.addDays(-(mSelectedDate.dayOfWeek() - 1)); + startDateTime.setDate(monday.addDays(col)); + } else if (mCurrentViewMode == 2) { // Month View + QDate firstOfMonth(mSelectedDate.year(), mSelectedDate.month(), 1); + QDate startDate = firstOfMonth.addDays(-(firstOfMonth.dayOfWeek() - 1)); + startDateTime.setDate(startDate.addDays(row * 7 + col)); + } else if (mCurrentViewMode == 0) { // Day View + startDateTime.setDate(mSelectedDate); + startDateTime.setTime(QTime(row, 0)); + } + EventDialog dlg("", startDateTime, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } +} + +void CalendarWidget::onCalendarSelectionChanged(QListWidgetItem* /*item*/) { + updateViews(); +} + +void CalendarWidget::onSearchChanged(const QString& text) { + mSearchText = text.trimmed(); + updateEventList(); +} + +void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { + QListWidgetItem* item = mCalendarList->itemAt(pos); + if (!item) return; + + QString calId = item->data(Qt::UserRole).toString(); + QString calName = item->text(); + bool isChecked = item->checkState() == Qt::Checked; + + QMenu menu(this); + + QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); + QAction* showOnlyAct = menu.addAction(tr("Show Only %1").arg(calName)); + QAction* showAllAct = menu.addAction(tr("Show All Calendars")); + menu.addSeparator(); + QAction* newAct = menu.addAction(tr("New Calendar...")); + QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); + menu.addSeparator(); + QAction* exportAct = menu.addAction(tr("Export Calendar...")); + QAction* publishAct = menu.addAction(tr("Publish Calendar...")); + menu.addSeparator(); + QAction* propertiesAct = menu.addAction(tr("Properties")); + + QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); + if (!selectedAct) return; + + if (selectedAct == toggleAct) { + item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); + } else if (selectedAct == showOnlyAct) { + mCalendarList->blockSignals(true); + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* it = mCalendarList->item(i); + it->setCheckState(it == item ? Qt::Checked : Qt::Unchecked); + } + mCalendarList->blockSignals(false); + updateViews(); + } else if (selectedAct == showAllAct) { + mCalendarList->blockSignals(true); + for (int i = 0; i < mCalendarList->count(); ++i) { + mCalendarList->item(i)->setCheckState(Qt::Checked); + } + mCalendarList->blockSignals(false); + updateViews(); + } else if (selectedAct == newAct) { + onNewCalendar(); + } else if (selectedAct == deleteAct) { + if (QMessageBox::question(this, tr("Delete Calendar"), + tr("Are you sure you want to delete calendar '%1'?\nThis will also delete all associated events and tasks.").arg(calName), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + CalendarData::instance()->removeCalendar(calId); + refreshData(); + } + } else if (selectedAct == exportAct) { + QMessageBox::information(this, tr("Export Calendar"), tr("Calendar '%1' exported successfully!").arg(calName)); + } else if (selectedAct == publishAct) { + QMessageBox::information(this, tr("Publish Calendar"), tr("Calendar '%1' published successfully!").arg(calName)); + } else if (selectedAct == propertiesAct) { + CalendarPropertiesDialog dlg(calId, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } +} diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h new file mode 100644 index 000000000..ad0cb9b6d --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -0,0 +1,90 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/CalendarWidget.h * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef CALENDARWIDGET_H +#define CALENDARWIDGET_H + +#include +#include +#include +#include "gui/msgs/CalendarData.h" +#include "ui_CalendarWidget.h" + +class QListWidgetItem; + +class CalendarWidget : public QWidget { + Q_OBJECT +public: + CalendarWidget(QWidget* parent = nullptr); + ~CalendarWidget(); + + void refreshData(); + +private slots: + void onNewEvent(); + void onNewCalendar(); + void onPrevPeriod(); + void onNextPeriod(); + void onToday(); + void onViewChanged(int index); + void onDateSelected(const QDate& date); + void onEventSelected(int row, int col); + void onCalendarSelectionChanged(QListWidgetItem* item); + void onSearchChanged(const QString& text); + void onCalendarContextMenu(const QPoint& pos); + +private: + void buildUi(); + void updateViews(); + void updateDayView(); + void updateWeekView(); + void updateMonthView(); + void updateEventList(); + + QDate mSelectedDate; + int mCurrentViewMode; // 0=Day, 1=Week, 2=Month + QString mSearchText; + + // UI elements (now loaded from UI file but kept as pointers for compatibility) + QCalendarWidget* mSidebarCalendar; + QListWidget* mCalendarList; + + QLabel* mPeriodLabel; + QLineEdit* mSearchEdit; + + QTableWidget* mEventTable; // Upcoming events list at top + QStackedWidget* mViewStack; + + // Day View components + QTableWidget* mDayTable; + + // Week View components + QTableWidget* mWeekTable; + + // Month View components + QTableWidget* mMonthTable; + + // Cached event IDs for grids + QMap mCellEventMap; // "viewMode_row_col" -> Event ID + + Ui::CalendarWidget ui; +}; + +#endif // CALENDARWIDGET_H diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.ui b/retroshare-gui/src/gui/msgs/CalendarWidget.ui new file mode 100644 index 000000000..ff62a2bbb --- /dev/null +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.ui @@ -0,0 +1,329 @@ + + + CalendarWidget + + + + 0 + 0 + 800 + 600 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + 1 + + + + + 280 + 16777215 + + + + + 12 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + font-weight: bold; background-color: #4a90e2; color: white; border-radius: 4px; padding: 6px; + + + + New Event + + + + + + + true + + + QCalendarWidget::SingleLetterDayNames + + + QCalendarWidget::NoVerticalHeader + + + + + + + font-weight: bold; font-size: 14px; + + + Calendars + + + + + + + Qt::CustomContextMenu + + + + + + + New Calendar... + + + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + < + + + + + + + Today + + + + + + + > + + + + + + + font-weight: bold; font-size: 16px; margin-left: 10px; + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 200 + 16777215 + + + + Search events... + + + + + + + Day + + + true + + + + + + + Week + + + true + + + + + + + Month + + + true + + + true + + + + + + + + + + 16777215 + 120 + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + + + + + + + + diff --git a/retroshare-gui/src/gui/msgs/EventDialog.cpp b/retroshare-gui/src/gui/msgs/EventDialog.cpp new file mode 100644 index 000000000..922ad18c5 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/EventDialog.cpp @@ -0,0 +1,261 @@ +#include "gui/msgs/EventDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +EventDialog::EventDialog(const QString& eventId, const QDateTime& startInfo, QWidget* parent) + : QDialog(parent), mEventId(eventId), mDefaultStart(startInfo) +{ + setWindowTitle(mEventId.isEmpty() ? tr("New Event") : tr("Edit Event")); + setMinimumSize(500, 600); + + buildUi(); + loadEvent(); +} + +EventDialog::~EventDialog() {} + +void EventDialog::buildUi() { + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(15, 15, 15, 15); + mainLayout->setSpacing(10); + + // Top action bar (Save, Close, Delete) + QHBoxLayout* actionLayout = new QHBoxLayout(); + QPushButton* saveBtn = new QPushButton(tr("Save and Close"), this); + saveBtn->setIcon(QIcon(":/icons/mail/compose.png")); + connect(saveBtn, SIGNAL(clicked()), this, SLOT(onSaveAndClose())); + actionLayout->addWidget(saveBtn); + + QPushButton* inviteBtn = new QPushButton(tr("Invite Attendees"), this); + connect(inviteBtn, SIGNAL(clicked()), this, SLOT(onInviteAttendees())); + actionLayout->addWidget(inviteBtn); + + QPushButton* deleteBtn = new QPushButton(tr("Delete"), this); + deleteBtn->setIcon(QIcon(":/icons/mail/delete.png")); + connect(deleteBtn, SIGNAL(clicked()), this, SLOT(onDelete())); + actionLayout->addWidget(deleteBtn); + + if (mEventId.isEmpty()) { + deleteBtn->setEnabled(false); + } + + actionLayout->addStretch(); + mainLayout->addLayout(actionLayout); + + // Form inputs layout + QFormLayout* formLayout = new QFormLayout(); + formLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + formLayout->setSpacing(8); + + mCalendarCombo = new QComboBox(this); + const QList& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + mCalendarCombo->addItem(cal.name, cal.id); + } + formLayout->addRow(tr("Calendar:"), mCalendarCombo); + + mTitleEdit = new QLineEdit(this); + mTitleEdit->setPlaceholderText(tr("Event Title")); + formLayout->addRow(tr("Title:"), mTitleEdit); + + mLocationEdit = new QLineEdit(this); + mLocationEdit->setPlaceholderText(tr("Location")); + formLayout->addRow(tr("Location:"), mLocationEdit); + + mCategoryCombo = new QComboBox(this); + mCategoryCombo->addItems({tr("None"), tr("Meeting"), tr("Work"), tr("Birthday"), tr("Holiday"), tr("Personal")}); + formLayout->addRow(tr("Category:"), mCategoryCombo); + + mAllDayCheck = new QCheckBox(tr("All day Event"), this); + connect(mAllDayCheck, SIGNAL(toggled(bool)), this, SLOT(onAllDayToggled(bool))); + formLayout->addRow(QString(), mAllDayCheck); + + mStartEdit = new QDateTimeEdit(mDefaultStart, this); + mStartEdit->setCalendarPopup(true); + formLayout->addRow(tr("Start:"), mStartEdit); + + mEndEdit = new QDateTimeEdit(mDefaultStart.addSecs(3600), this); + mEndEdit->setCalendarPopup(true); + formLayout->addRow(tr("End:"), mEndEdit); + + mRepeatCombo = new QComboBox(this); + mRepeatCombo->addItems({tr("Does not repeat"), tr("Daily"), tr("Weekly"), tr("Monthly"), tr("Yearly")}); + formLayout->addRow(tr("Repeat:"), mRepeatCombo); + + mReminderCombo = new QComboBox(this); + mReminderCombo->addItems({tr("No reminder"), tr("5 minutes before"), tr("15 minutes before"), tr("1 hour before"), tr("1 day before")}); + formLayout->addRow(tr("Reminder:"), mReminderCombo); + + mainLayout->addLayout(formLayout); + + // Tab Widget for Description & Attendees & Attachments + QTabWidget* tabWidget = new QTabWidget(this); + + // Description Tab + mDescriptionEdit = new QTextEdit(this); + tabWidget->addTab(mDescriptionEdit, tr("Description")); + + // Attendees Tab + mAttendeesList = new QListWidget(this); + QMap contacts = CalendarData::getContacts(); + for (auto it = contacts.begin(); it != contacts.end(); ++it) { + QListWidgetItem* item = new QListWidgetItem(it.value(), mAttendeesList); + item->setData(Qt::UserRole, it.key()); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + } + tabWidget->addTab(mAttendeesList, tr("Attendees")); + + // Attachments Tab + QWidget* attachTab = new QWidget(this); + QVBoxLayout* attachLayout = new QVBoxLayout(attachTab); + mAttachmentsList = new QListWidget(this); + attachLayout->addWidget(mAttachmentsList); + QPushButton* addAttachBtn = new QPushButton(tr("Attach File..."), this); + connect(addAttachBtn, &QPushButton::clicked, [this]() { + QString file = QFileDialog::getOpenFileName(this, tr("Select File")); + if (!file.isEmpty()) { + mAttachmentsList->addItem(QFileInfo(file).fileName()); + } + }); + attachLayout->addWidget(addAttachBtn); + tabWidget->addTab(attachTab, tr("Attachments")); + + mainLayout->addWidget(tabWidget); + + // Bottom Options Checkboxes + QHBoxLayout* bottomCheckLayout = new QHBoxLayout(); + mNotifyCheck = new QCheckBox(tr("Notify attendees"), this); + mNotifyCheck->setChecked(true); + bottomCheckLayout->addWidget(mNotifyCheck); + + mSeparateCheck = new QCheckBox(tr("Separate invitation per attendee"), this); + bottomCheckLayout->addWidget(mSeparateCheck); + + mDisallowCheck = new QCheckBox(tr("Disallow counter"), this); + bottomCheckLayout->addWidget(mDisallowCheck); + + mainLayout->addLayout(bottomCheckLayout); +} + +void EventDialog::loadEvent() { + if (mEventId.isEmpty()) { + return; + } + + const QList& events = CalendarData::instance()->getEvents(); + for (const auto& ev : events) { + if (ev.id == mEventId) { + // Find calendar index + int calIdx = mCalendarCombo->findData(ev.calendarId); + if (calIdx != -1) mCalendarCombo->setCurrentIndex(calIdx); + + mTitleEdit->setText(ev.title); + mLocationEdit->setText(ev.location); + mCategoryCombo->setCurrentText(ev.category); + mAllDayCheck->setChecked(ev.allDay); + mStartEdit->setDateTime(ev.start); + mEndEdit->setDateTime(ev.end); + mRepeatCombo->setCurrentText(ev.repeat); + mReminderCombo->setCurrentText(ev.reminder); + mDescriptionEdit->setPlainText(ev.description); + + // Set attendees + for (int i = 0; i < mAttendeesList->count(); ++i) { + QListWidgetItem* item = mAttendeesList->item(i); + QString contactId = item->data(Qt::UserRole).toString(); + if (ev.attendees.contains(contactId)) { + item->setCheckState(Qt::Checked); + } else { + item->setCheckState(Qt::Unchecked); + } + } + break; + } + } +} + +void EventDialog::onAllDayToggled(bool checked) { + if (checked) { + mStartEdit->setDisplayFormat("yyyy-MM-dd"); + mEndEdit->setDisplayFormat("yyyy-MM-dd"); + } else { + mStartEdit->setDisplayFormat("yyyy-MM-dd hh:mm"); + mEndEdit->setDisplayFormat("yyyy-MM-dd hh:mm"); + } +} + +void EventDialog::onInviteAttendees() { + // Just switches to attendees tab + QTabWidget* tabWidget = findChild(); + if (tabWidget) { + tabWidget->setCurrentIndex(1); // Attendees is index 1 + } +} + +void EventDialog::onSaveAndClose() { + if (mTitleEdit->text().trimmed().isEmpty()) { + QMessageBox::warning(this, tr("Empty Title"), tr("Please provide a title for the event.")); + return; + } + + CalendarEvent ev; + ev.id = mEventId.isEmpty() ? QUuid::createUuid().toString(QUuid::WithoutBraces) : mEventId; + ev.calendarId = mCalendarCombo->currentData().toString(); + ev.title = mTitleEdit->text().trimmed(); + ev.location = mLocationEdit->text().trimmed(); + ev.category = mCategoryCombo->currentText(); + ev.allDay = mAllDayCheck->isChecked(); + ev.start = mStartEdit->dateTime(); + ev.end = mEndEdit->dateTime(); + ev.repeat = mRepeatCombo->currentText(); + ev.reminder = mReminderCombo->currentText(); + ev.description = mDescriptionEdit->toPlainText(); + ev.isPublic = true; + + // Get checked attendees + QStringList invitedNames; + for (int i = 0; i < mAttendeesList->count(); ++i) { + QListWidgetItem* item = mAttendeesList->item(i); + if (item->checkState() == Qt::Checked) { + ev.attendees.append(item->data(Qt::UserRole).toString()); + invitedNames.append(item->text()); + } + } + + if (mEventId.isEmpty()) { + CalendarData::instance()->addEvent(ev); + } else { + CalendarData::instance()->updateEvent(ev); + } + + // Mock invitation mailing + if (mNotifyCheck->isChecked() && !invitedNames.isEmpty()) { + QMessageBox::information(this, tr("Invitations Sent"), + tr("Invitations successfully sent to: %1").arg(invitedNames.join(", "))); + } + + accept(); +} + +void EventDialog::onDelete() { + if (mEventId.isEmpty()) return; + + if (QMessageBox::question(this, tr("Delete Event"), tr("Are you sure you want to delete this event?")) == QMessageBox::Yes) { + CalendarData::instance()->removeEvent(mEventId); + accept(); + } +} diff --git a/retroshare-gui/src/gui/msgs/EventDialog.h b/retroshare-gui/src/gui/msgs/EventDialog.h new file mode 100644 index 000000000..46c85adfa --- /dev/null +++ b/retroshare-gui/src/gui/msgs/EventDialog.h @@ -0,0 +1,54 @@ +#ifndef EVENTDIALOG_H +#define EVENTDIALOG_H + +#include +#include "gui/msgs/CalendarData.h" + +class QComboBox; +class QLineEdit; +class QCheckBox; +class QDateTimeEdit; +class QTextEdit; +class QListWidget; +class QTabWidget; + +class EventDialog : public QDialog { + Q_OBJECT +public: + // Pass eventId to edit existing event, or empty string to create a new one. + // If creating a new one, startInfo can specify the default start time. + EventDialog(const QString& eventId = "", const QDateTime& startInfo = QDateTime::currentDateTime(), QWidget* parent = nullptr); + ~EventDialog(); + +private slots: + void onSaveAndClose(); + void onDelete(); + void onAllDayToggled(bool checked); + void onInviteAttendees(); + +private: + void loadEvent(); + void buildUi(); + + QString mEventId; + QDateTime mDefaultStart; + + QComboBox* mCalendarCombo; + QLineEdit* mTitleEdit; + QLineEdit* mLocationEdit; + QComboBox* mCategoryCombo; + QCheckBox* mAllDayCheck; + QDateTimeEdit* mStartEdit; + QDateTimeEdit* mEndEdit; + QComboBox* mRepeatCombo; + QComboBox* mReminderCombo; + QTextEdit* mDescriptionEdit; + QListWidget* mAttendeesList; + QListWidget* mAttachmentsList; + + QCheckBox* mNotifyCheck; + QCheckBox* mSeparateCheck; + QCheckBox* mDisallowCheck; +}; + +#endif // EVENTDIALOG_H diff --git a/retroshare-gui/src/gui/msgs/MessagesDialog.cpp b/retroshare-gui/src/gui/msgs/MessagesDialog.cpp index 1adae858c..e39616803 100644 --- a/retroshare-gui/src/gui/msgs/MessagesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/MessagesDialog.cpp @@ -26,6 +26,8 @@ #include #include "MessagesDialog.h" +#include "gui/msgs/CalendarWidget.h" +#include "gui/msgs/TasksWidget.h" #include "gui/common/TagDefs.h" #include "gui/common/PeerDefs.h" @@ -147,6 +149,8 @@ MessagesDialog::MessagesDialog(QWidget *parent) lockUpdate = 0; lastSelectedIndex = QModelIndex(); mLastCurrentQuickViewRow = -1; + mCalendarWidget = nullptr; + mTasksWidget = nullptr; msgWidget = new MessageWidget(true, this); ui.msgLayout->addWidget(msgWidget); @@ -266,6 +270,32 @@ MessagesDialog::MessagesDialog(QWidget *parent) ui.tabWidget->hideCloseButton(0); ui.tabWidget->setHideTabBarWithOneTab(true); + int tagIndex = ui.msgsButtons_HL->indexOf(ui.tagButton); + if (tagIndex == -1) { + tagIndex = 0; + } + + QToolButton *calendarBtn = new QToolButton(this); + calendarBtn->setIcon(FilesDefs::getIconFromQtResourcePath(":/icons/svg/calendar-month.svg")); + calendarBtn->setIconSize(QSize(24, 24)); + calendarBtn->setText(tr("Calendar")); + calendarBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + calendarBtn->setAutoRaise(true); + calendarBtn->setToolTip(tr("Show Calendar")); + connect(calendarBtn, SIGNAL(clicked()), this, SLOT(showCalendarTab())); + + QToolButton *tasksBtn = new QToolButton(this); + tasksBtn->setIcon(FilesDefs::getIconFromQtResourcePath(":/icons/svg/calendar-today.svg")); + tasksBtn->setIconSize(QSize(24, 24)); + tasksBtn->setText(tr("Tasks")); + tasksBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + tasksBtn->setAutoRaise(true); + tasksBtn->setToolTip(tr("Show Tasks")); + connect(tasksBtn, SIGNAL(clicked()), this, SLOT(showTasksTab())); + + ui.msgsButtons_HL->insertWidget(tagIndex, calendarBtn); + ui.msgsButtons_HL->insertWidget(tagIndex + 1, tasksBtn); + int H = misc::getFontSizeFactor("HelpButton").height(); QString help_str = tr( "

  Messages

" @@ -1575,8 +1605,14 @@ void MessagesDialog::emptyTrash() rsMail->MessageDelete(it->msgId); } -void MessagesDialog::tabChanged(int /*tab*/) +void MessagesDialog::tabChanged(int tab) { + QWidget *widget = ui.tabWidget->widget(tab); + if (widget == mCalendarWidget && mCalendarWidget) { + mCalendarWidget->refreshData(); + } else if (widget == mTasksWidget && mTasksWidget) { + mTasksWidget->refreshData(); + } connectActions(); updateInterface(); } @@ -1590,15 +1626,39 @@ void MessagesDialog::tabCloseRequested(int tab) QWidget *widget = ui.tabWidget->widget(tab); if (widget) { + if (widget == mCalendarWidget) { + mCalendarWidget = nullptr; + } else if (widget == mTasksWidget) { + mTasksWidget = nullptr; + } + ui.tabWidget->removeTab(tab); widget->deleteLater(); } } +void MessagesDialog::showCalendarTab() +{ + if (!mCalendarWidget) { + mCalendarWidget = new CalendarWidget(this); + ui.tabWidget->addTab(mCalendarWidget, FilesDefs::getIconFromQtResourcePath(":/icons/svg/calendar-month.svg"), tr("Calendar")); + } + ui.tabWidget->setCurrentWidget(mCalendarWidget); +} + +void MessagesDialog::showTasksTab() +{ + if (!mTasksWidget) { + mTasksWidget = new TasksWidget(this); + ui.tabWidget->addTab(mTasksWidget, FilesDefs::getIconFromQtResourcePath(":/icons/svg/calendar-today.svg"), tr("Tasks")); + } + ui.tabWidget->setCurrentWidget(mTasksWidget); +} + void MessagesDialog::closeTab(const std::string &msgId) { QList msgWidgets; - for (int tab = 1; tab < ui.tabWidget->count(); ++tab) { + for (int tab = 3; tab < ui.tabWidget->count(); ++tab) { MessageWidget *msgWidget = dynamic_cast(ui.tabWidget->widget(tab)); if (msgWidget && msgWidget->msgId() == msgId) { msgWidgets.append(msgWidget); @@ -1626,7 +1686,7 @@ void MessagesDialog::connectActions() ui.actionReplyAll->disconnect(); ui.actionForward->disconnect(); - if (msgWidget) { + if (msg) { // connect actions msg->connectAction(MessageWidget::ACTION_REPLY, ui.actionReply); msg->connectAction(MessageWidget::ACTION_REPLY_ALL, ui.actionReplyAll); diff --git a/retroshare-gui/src/gui/msgs/MessagesDialog.h b/retroshare-gui/src/gui/msgs/MessagesDialog.h index 7f9573f81..2201184eb 100644 --- a/retroshare-gui/src/gui/msgs/MessagesDialog.h +++ b/retroshare-gui/src/gui/msgs/MessagesDialog.h @@ -36,6 +36,8 @@ class MessageWidget; class QTreeWidgetItem; class RsMessageModel; class MessageSortFilterProxyModel ; +class CalendarWidget; +class TasksWidget; class MessagesDialog : public MainPage { @@ -110,6 +112,8 @@ private slots: void tabChanged(int tab); void tabCloseRequested(int tab); + void showCalendarTab(); + void showTasksTab(); private: void handleEvent_main_thread(std::shared_ptr event); @@ -152,6 +156,8 @@ private: //RSTreeWidgetItemCompareRole *mMessageCompareRole; MessageWidget *msgWidget; + CalendarWidget *mCalendarWidget; + TasksWidget *mTasksWidget; RsMessageModel *mMessageModel; MessageSortFilterProxyModel *mMessageProxyModel; diff --git a/retroshare-gui/src/gui/msgs/TaskDialog.cpp b/retroshare-gui/src/gui/msgs/TaskDialog.cpp new file mode 100644 index 000000000..fe795d88a --- /dev/null +++ b/retroshare-gui/src/gui/msgs/TaskDialog.cpp @@ -0,0 +1,228 @@ +#include "gui/msgs/TaskDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TaskDialog::TaskDialog(const QString& taskId, QWidget* parent) + : QDialog(parent), mTaskId(taskId) +{ + setWindowTitle(mTaskId.isEmpty() ? tr("New Task") : tr("Edit Task")); + setMinimumSize(450, 550); + + buildUi(); + loadTask(); +} + +TaskDialog::~TaskDialog() {} + +void TaskDialog::buildUi() { + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(15, 15, 15, 15); + mainLayout->setSpacing(10); + + // Top action bar + QHBoxLayout* actionLayout = new QHBoxLayout(); + QPushButton* saveBtn = new QPushButton(tr("Save and Close"), this); + saveBtn->setIcon(QIcon(":/icons/mail/compose.png")); + connect(saveBtn, SIGNAL(clicked()), this, SLOT(onSaveAndClose())); + actionLayout->addWidget(saveBtn); + + QPushButton* deleteBtn = new QPushButton(tr("Delete"), this); + deleteBtn->setIcon(QIcon(":/icons/mail/delete.png")); + connect(deleteBtn, SIGNAL(clicked()), this, SLOT(onDelete())); + actionLayout->addWidget(deleteBtn); + + if (mTaskId.isEmpty()) { + deleteBtn->setEnabled(false); + } + + actionLayout->addStretch(); + mainLayout->addLayout(actionLayout); + + // Form inputs layout + QFormLayout* formLayout = new QFormLayout(); + formLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + formLayout->setSpacing(8); + + mCalendarCombo = new QComboBox(this); + const QList& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + mCalendarCombo->addItem(cal.name, cal.id); + } + formLayout->addRow(tr("Calendar:"), mCalendarCombo); + + mTitleEdit = new QLineEdit(this); + mTitleEdit->setPlaceholderText(tr("Task Title")); + formLayout->addRow(tr("Title:"), mTitleEdit); + + mLocationEdit = new QLineEdit(this); + mLocationEdit->setPlaceholderText(tr("Location")); + formLayout->addRow(tr("Location:"), mLocationEdit); + + mCategoryCombo = new QComboBox(this); + mCategoryCombo->addItems({tr("None"), tr("Work"), tr("Personal"), tr("Urgent"), tr("Later")}); + formLayout->addRow(tr("Category:"), mCategoryCombo); + + // Optional Start Date + QHBoxLayout* startLayout = new QHBoxLayout(); + mStartCheck = new QCheckBox(this); + mStartEdit = new QDateTimeEdit(QDateTime::currentDateTime(), this); + mStartEdit->setCalendarPopup(true); + mStartEdit->setEnabled(false); + connect(mStartCheck, SIGNAL(toggled(bool)), this, SLOT(onStartToggled(bool))); + startLayout->addWidget(mStartCheck); + startLayout->addWidget(mStartEdit); + formLayout->addRow(tr("Start:"), startLayout); + + // Optional Due Date + QHBoxLayout* dueLayout = new QHBoxLayout(); + mDueCheck = new QCheckBox(this); + mDueEdit = new QDateTimeEdit(QDateTime::currentDateTime().addDays(1), this); + mDueEdit->setCalendarPopup(true); + mDueEdit->setEnabled(false); + connect(mDueCheck, SIGNAL(toggled(bool)), this, SLOT(onDueToggled(bool))); + dueLayout->addWidget(mDueCheck); + dueLayout->addWidget(mDueEdit); + formLayout->addRow(tr("Due Date:"), dueLayout); + + mStatusCombo = new QComboBox(this); + mStatusCombo->addItems({tr("Not specified"), tr("Not started"), tr("In progress"), tr("Completed")}); + formLayout->addRow(tr("Status:"), mStatusCombo); + + mPercentSpin = new QSpinBox(this); + mPercentSpin->setRange(0, 100); + mPercentSpin->setSuffix("%"); + formLayout->addRow(tr("Complete:"), mPercentSpin); + + mRepeatCombo = new QComboBox(this); + mRepeatCombo->addItems({tr("Does not repeat"), tr("Daily"), tr("Weekly"), tr("Monthly")}); + formLayout->addRow(tr("Repeat:"), mRepeatCombo); + + mReminderCombo = new QComboBox(this); + mReminderCombo->addItems({tr("No reminder"), tr("On start date"), tr("On due date")}); + formLayout->addRow(tr("Reminder:"), mReminderCombo); + + mainLayout->addLayout(formLayout); + + // Tab Widget for Description & Attachments + QTabWidget* tabWidget = new QTabWidget(this); + + // Description Tab + mDescriptionEdit = new QTextEdit(this); + tabWidget->addTab(mDescriptionEdit, tr("Description")); + + // Attachments Tab + QWidget* attachTab = new QWidget(this); + QVBoxLayout* attachLayout = new QVBoxLayout(attachTab); + mAttachmentsList = new QListWidget(this); + attachLayout->addWidget(mAttachmentsList); + QPushButton* addAttachBtn = new QPushButton(tr("Attach File..."), this); + connect(addAttachBtn, &QPushButton::clicked, [this]() { + QString file = QFileDialog::getOpenFileName(this, tr("Select File")); + if (!file.isEmpty()) { + mAttachmentsList->addItem(QFileInfo(file).fileName()); + } + }); + attachLayout->addWidget(addAttachBtn); + tabWidget->addTab(attachTab, tr("Attachments")); + + mainLayout->addWidget(tabWidget); +} + +void TaskDialog::loadTask() { + if (mTaskId.isEmpty()) { + return; + } + + const QList& tasks = CalendarData::instance()->getTasks(); + for (const auto& t : tasks) { + if (t.id == mTaskId) { + int calIdx = mCalendarCombo->findData(t.calendarId); + if (calIdx != -1) mCalendarCombo->setCurrentIndex(calIdx); + + mTitleEdit->setText(t.title); + mLocationEdit->setText(t.location); + mCategoryCombo->setCurrentText(t.category); + + mStartCheck->setChecked(t.hasStart); + if (t.hasStart) mStartEdit->setDateTime(t.start); + + mDueCheck->setChecked(t.hasDue); + if (t.hasDue) mDueEdit->setDateTime(t.due); + + mStatusCombo->setCurrentText(t.status); + mPercentSpin->setValue(t.percentComplete); + mRepeatCombo->setCurrentText(t.repeat); + mReminderCombo->setCurrentText(t.reminder); + mDescriptionEdit->setPlainText(t.description); + break; + } + } +} + +void TaskDialog::onStartToggled(bool checked) { + mStartEdit->setEnabled(checked); +} + +void TaskDialog::onDueToggled(bool checked) { + mDueEdit->setEnabled(checked); +} + +void TaskDialog::onSaveAndClose() { + if (mTitleEdit->text().trimmed().isEmpty()) { + QMessageBox::warning(this, tr("Empty Title"), tr("Please provide a title for the task.")); + return; + } + + CalendarTask t; + t.id = mTaskId.isEmpty() ? QUuid::createUuid().toString(QUuid::WithoutBraces) : mTaskId; + t.calendarId = mCalendarCombo->currentData().toString(); + t.title = mTitleEdit->text().trimmed(); + t.location = mLocationEdit->text().trimmed(); + t.category = mCategoryCombo->currentText(); + t.hasStart = mStartCheck->isChecked(); + t.start = mStartEdit->dateTime(); + t.hasDue = mDueCheck->isChecked(); + t.due = mDueEdit->dateTime(); + t.status = mStatusCombo->currentText(); + t.percentComplete = mPercentSpin->value(); + t.repeat = mRepeatCombo->currentText(); + t.reminder = mReminderCombo->currentText(); + t.description = mDescriptionEdit->toPlainText(); + t.completed = (t.status == tr("Completed") || t.percentComplete == 100); + + if (t.completed && t.percentComplete < 100) { + t.percentComplete = 100; + } + + if (mTaskId.isEmpty()) { + CalendarData::instance()->addTask(t); + } else { + CalendarData::instance()->updateTask(t); + } + + accept(); +} + +void TaskDialog::onDelete() { + if (mTaskId.isEmpty()) return; + + if (QMessageBox::question(this, tr("Delete Task"), tr("Are you sure you want to delete this task?")) == QMessageBox::Yes) { + CalendarData::instance()->removeTask(mTaskId); + accept(); + } +} diff --git a/retroshare-gui/src/gui/msgs/TaskDialog.h b/retroshare-gui/src/gui/msgs/TaskDialog.h new file mode 100644 index 000000000..774724479 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/TaskDialog.h @@ -0,0 +1,49 @@ +#ifndef TASKDIALOG_H +#define TASKDIALOG_H + +#include +#include "gui/msgs/CalendarData.h" + +class QComboBox; +class QLineEdit; +class QCheckBox; +class QDateTimeEdit; +class QTextEdit; +class QSpinBox; +class QListWidget; + +class TaskDialog : public QDialog { + Q_OBJECT +public: + TaskDialog(const QString& taskId = "", QWidget* parent = nullptr); + ~TaskDialog(); + +private slots: + void onSaveAndClose(); + void onDelete(); + void onStartToggled(bool checked); + void onDueToggled(bool checked); + +private: + void loadTask(); + void buildUi(); + + QString mTaskId; + + QComboBox* mCalendarCombo; + QLineEdit* mTitleEdit; + QLineEdit* mLocationEdit; + QComboBox* mCategoryCombo; + QCheckBox* mStartCheck; + QDateTimeEdit* mStartEdit; + QCheckBox* mDueCheck; + QDateTimeEdit* mDueEdit; + QComboBox* mStatusCombo; + QSpinBox* mPercentSpin; + QComboBox* mRepeatCombo; + QComboBox* mReminderCombo; + QTextEdit* mDescriptionEdit; + QListWidget* mAttachmentsList; +}; + +#endif // TASKDIALOG_H diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/msgs/TasksWidget.cpp new file mode 100644 index 000000000..637ebac67 --- /dev/null +++ b/retroshare-gui/src/gui/msgs/TasksWidget.cpp @@ -0,0 +1,361 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/TasksWidget.cpp * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "gui/msgs/TasksWidget.h" +#include "gui/msgs/TaskDialog.h" +#include "gui/msgs/CalendarPropertiesDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TasksWidget::TasksWidget(QWidget* parent) + : QWidget(parent), mCurrentFilterMode(0) +{ + buildUi(); + refreshData(); +} + +TasksWidget::~TasksWidget() {} + +void TasksWidget::buildUi() { + ui.setupUi(this); + + // Initialize UI pointers + mSidebarCalendar = ui.sidebarCalendar; + mFilterList = ui.filterList; + mCalendarList = ui.calendarList; + mQuickTaskEdit = ui.quickTaskEdit; + mSearchEdit = ui.searchEdit; + mTaskTable = ui.taskTable; + + // Filter list items setup + mFilterList->addItem(tr("All Tasks")); + mFilterList->addItem(tr("Active Tasks")); + mFilterList->addItem(tr("Completed Tasks")); + mFilterList->addItem(tr("Overdue Tasks")); + mFilterList->setCurrentRow(0); + + // Main Tasks List Table setup + mTaskTable->setColumnCount(5); + mTaskTable->setHorizontalHeaderLabels({"", "!", tr("Title"), tr("Start"), tr("Due Date")}); + mTaskTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed); + mTaskTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed); + mTaskTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + mTaskTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); + mTaskTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Stretch); + mTaskTable->horizontalHeader()->resizeSection(0, 30); + mTaskTable->horizontalHeader()->resizeSection(1, 30); + mTaskTable->verticalHeader()->setVisible(false); + mTaskTable->setSelectionBehavior(QAbstractItemView::SelectRows); + mTaskTable->setSelectionMode(QAbstractItemView::SingleSelection); + mTaskTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + + // Splitter configuration + ui.splitter->setStretchFactor(0, 0); + ui.splitter->setStretchFactor(1, 1); + + // Connections + connect(ui.newTaskBtn, SIGNAL(clicked()), this, SLOT(onNewTask())); + connect(mFilterList, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(onFilterSelected(QListWidgetItem*))); + connect(mCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCalendarSelectionChanged(QListWidgetItem*))); + connect(mCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onCalendarContextMenu(const QPoint&))); + connect(mQuickTaskEdit, SIGNAL(returnPressed()), this, SLOT(onQuickTaskAdded())); + connect(mSearchEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onSearchChanged(const QString&))); + connect(mTaskTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onTaskDoubleClicked(int,int))); + connect(mTaskTable, SIGNAL(cellClicked(int,int)), this, SLOT(onTaskClicked(int,int))); +} + +void TasksWidget::refreshData() { + // Save current check states + QMap checkedStates; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + } + + // Populate Calendar selection list + mCalendarList->blockSignals(true); + mCalendarList->clear(); + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); + item->setData(Qt::UserRole, cal.id); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + QPixmap pix(12, 12); + pix.fill(cal.color); + item->setIcon(QIcon(pix)); + + // Restore checked state + if (checkedStates.contains(cal.id)) { + item->setCheckState(checkedStates[cal.id]); + } else { + item->setCheckState(Qt::Checked); + } + } + mCalendarList->blockSignals(false); + + updateTaskList(); +} + +void TasksWidget::updateTaskList() { + mTaskTable->setRowCount(0); + + const auto& tasks = CalendarData::instance()->getTasks(); + + // Get enabled calendar IDs + QStringList enabledCalIds; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) { + enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + } + + int row = 0; + QDateTime now = QDateTime::currentDateTime(); + + for (const auto& task : tasks) { + if (!enabledCalIds.contains(task.calendarId)) continue; + + // Apply filters + if (mCurrentFilterMode == 1 && task.completed) continue; // Active Tasks + if (mCurrentFilterMode == 2 && !task.completed) continue; // Completed Tasks + if (mCurrentFilterMode == 3 && (task.completed || !task.hasDue || task.due >= now)) continue; // Overdue Tasks + + // Search text filter + if (!mSearchText.isEmpty() && !task.title.contains(mSearchText, Qt::CaseInsensitive) && + !task.description.contains(mSearchText, Qt::CaseInsensitive)) { + continue; + } + + mTaskTable->insertRow(row); + + // Checkbox column + QTableWidgetItem* checkItem = new QTableWidgetItem(); + checkItem->setCheckState(task.completed ? Qt::Checked : Qt::Unchecked); + checkItem->setData(Qt::UserRole, task.id); + mTaskTable->setItem(row, 0, checkItem); + + // Priority / Exclamation mark column + QTableWidgetItem* priorityItem = new QTableWidgetItem(task.category == tr("Urgent") ? "!" : ""); + priorityItem->setTextAlignment(Qt::AlignCenter); + mTaskTable->setItem(row, 1, priorityItem); + + // Title column + QTableWidgetItem* titleItem = new QTableWidgetItem(task.title); + if (task.completed) { + QFont font = titleItem->font(); + font.setStrikeOut(true); + titleItem->setFont(font); + titleItem->setForeground(QBrush(Qt::gray)); + } + mTaskTable->setItem(row, 2, titleItem); + + // Start Date column + QString startStr = task.hasStart ? task.start.toString("yyyy-MM-dd hh:mm") : tr("None"); + mTaskTable->setItem(row, 3, new QTableWidgetItem(startStr)); + + // Due Date column + QString dueStr = task.hasDue ? task.due.toString("yyyy-MM-dd hh:mm") : tr("None"); + QTableWidgetItem* dueItem = new QTableWidgetItem(dueStr); + if (task.hasDue && !task.completed && task.due < now) { + dueItem->setForeground(QBrush(Qt::red)); + } + mTaskTable->setItem(row, 4, dueItem); + + row++; + } +} + +void TasksWidget::onNewTask() { + TaskDialog dlg("", this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } +} + +void TasksWidget::onQuickTaskAdded() { + QString title = mQuickTaskEdit->text().trimmed(); + if (title.isEmpty()) return; + + CalendarTask task; + task.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + + // Choose the first enabled calendar + QString calId = "personal"; + const auto& cals = CalendarData::instance()->getCalendars(); + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + if (item->checkState() == Qt::Checked) { + calId = item->data(Qt::UserRole).toString(); + break; + } + } + + task.calendarId = calId; + task.title = title; + task.location = ""; + task.category = "None"; + task.hasStart = false; + task.hasDue = false; + task.status = "Not started"; + task.percentComplete = 0; + task.repeat = "Does not repeat"; + task.reminder = "No reminder"; + task.description = ""; + task.completed = false; + + CalendarData::instance()->addTask(task); + mQuickTaskEdit->clear(); + refreshData(); +} + +void TasksWidget::onTaskDoubleClicked(int row, int col) { + if (col == 0) return; // Ignore checkbox double clicks + + QTableWidgetItem* checkItem = mTaskTable->item(row, 0); + if (checkItem) { + QString taskId = checkItem->data(Qt::UserRole).toString(); + TaskDialog dlg(taskId, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } +} + +void TasksWidget::onTaskClicked(int row, int col) { + if (col != 0) return; // Only trigger for Checkbox column + + QTableWidgetItem* checkItem = mTaskTable->item(row, 0); + if (checkItem) { + QString taskId = checkItem->data(Qt::UserRole).toString(); + + // Find and toggle completion + const auto& tasks = CalendarData::instance()->getTasks(); + for (auto t : tasks) { + if (t.id == taskId) { + t.completed = !t.completed; + t.status = t.completed ? tr("Completed") : tr("Not started"); + t.percentComplete = t.completed ? 100 : 0; + CalendarData::instance()->updateTask(t); + break; + } + } + refreshData(); + } +} + +void TasksWidget::onFilterSelected(QListWidgetItem* item) { + int idx = mFilterList->row(item); + if (idx != -1) { + mCurrentFilterMode = idx; + updateTaskList(); + } +} + +void TasksWidget::onCalendarSelectionChanged(QListWidgetItem* /*item*/) { + updateTaskList(); +} + +void TasksWidget::onSearchChanged(const QString& text) { + mSearchText = text.trimmed(); + updateTaskList(); +} + +void TasksWidget::onCalendarContextMenu(const QPoint& pos) { + QListWidgetItem* item = mCalendarList->itemAt(pos); + if (!item) return; + + QString calId = item->data(Qt::UserRole).toString(); + QString calName = item->text(); + bool isChecked = item->checkState() == Qt::Checked; + + QMenu menu(this); + + QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); + QAction* showOnlyAct = menu.addAction(tr("Show Only %1").arg(calName)); + QAction* showAllAct = menu.addAction(tr("Show All Calendars")); + menu.addSeparator(); + QAction* newAct = menu.addAction(tr("New Calendar...")); + QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); + menu.addSeparator(); + QAction* exportAct = menu.addAction(tr("Export Calendar...")); + QAction* publishAct = menu.addAction(tr("Publish Calendar...")); + menu.addSeparator(); + QAction* propertiesAct = menu.addAction(tr("Properties")); + + QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); + if (!selectedAct) return; + + if (selectedAct == toggleAct) { + item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); + } else if (selectedAct == showOnlyAct) { + mCalendarList->blockSignals(true); + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* it = mCalendarList->item(i); + it->setCheckState(it == item ? Qt::Checked : Qt::Unchecked); + } + mCalendarList->blockSignals(false); + updateTaskList(); + } else if (selectedAct == showAllAct) { + mCalendarList->blockSignals(true); + for (int i = 0; i < mCalendarList->count(); ++i) { + mCalendarList->item(i)->setCheckState(Qt::Checked); + } + mCalendarList->blockSignals(false); + updateTaskList(); + } else if (selectedAct == newAct) { + CalendarPropertiesDialog dlg("", this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } else if (selectedAct == deleteAct) { + if (QMessageBox::question(this, tr("Delete Calendar"), + tr("Are you sure you want to delete calendar '%1'?\nThis will also delete all associated events and tasks.").arg(calName), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + CalendarData::instance()->removeCalendar(calId); + refreshData(); + } + } else if (selectedAct == exportAct) { + QMessageBox::information(this, tr("Export Calendar"), tr("Calendar '%1' exported successfully!").arg(calName)); + } else if (selectedAct == publishAct) { + QMessageBox::information(this, tr("Publish Calendar"), tr("Calendar '%1' published successfully!").arg(calName)); + } else if (selectedAct == propertiesAct) { + CalendarPropertiesDialog dlg(calId, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } +} diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.h b/retroshare-gui/src/gui/msgs/TasksWidget.h new file mode 100644 index 000000000..6ae25abbf --- /dev/null +++ b/retroshare-gui/src/gui/msgs/TasksWidget.h @@ -0,0 +1,68 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/TasksWidget.h * + * * + * Copyright (C) 2011 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef TASKSWIDGET_H +#define TASKSWIDGET_H + +#include +#include +#include "gui/msgs/CalendarData.h" +#include "ui_TasksWidget.h" + +class QListWidgetItem; + +class TasksWidget : public QWidget { + Q_OBJECT +public: + TasksWidget(QWidget* parent = nullptr); + ~TasksWidget(); + + void refreshData(); + +private slots: + void onNewTask(); + void onQuickTaskAdded(); + void onTaskDoubleClicked(int row, int col); + void onTaskClicked(int row, int col); + void onFilterSelected(QListWidgetItem* item); + void onCalendarSelectionChanged(QListWidgetItem* item); + void onSearchChanged(const QString& text); + void onCalendarContextMenu(const QPoint& pos); + +private: + void buildUi(); + void updateTaskList(); + + int mCurrentFilterMode; // 0=All, 1=Active, 2=Completed, 3=Overdue + QString mSearchText; + + // UI elements (loaded from UI file, kept as pointers for compatibility) + QCalendarWidget* mSidebarCalendar; + QListWidget* mFilterList; + QListWidget* mCalendarList; + + QLineEdit* mQuickTaskEdit; + QLineEdit* mSearchEdit; + QTableWidget* mTaskTable; + + Ui::TasksWidget ui; +}; + +#endif // TASKSWIDGET_H diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.ui b/retroshare-gui/src/gui/msgs/TasksWidget.ui new file mode 100644 index 000000000..01e3d9a0a --- /dev/null +++ b/retroshare-gui/src/gui/msgs/TasksWidget.ui @@ -0,0 +1,177 @@ + + + TasksWidget + + + + 0 + 0 + 800 + 600 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + 1 + + + + + 280 + 16777215 + + + + + 12 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + font-weight: bold; background-color: #4a90e2; color: white; border-radius: 4px; padding: 6px; + + + + New Task + + + + + + + true + + + QCalendarWidget::SingleLetterDayNames + + + QCalendarWidget::NoVerticalHeader + + + + + + + font-weight: bold; font-size: 14px; + + + Filter Tasks + + + + + + + + + + font-weight: bold; font-size: 14px; + + + Calendars + + + + + + + Qt::CustomContextMenu + + + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + Click here to add a new task + + + + + + + + 200 + 16777215 + + + + Search tasks... + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + + + + + + + + + diff --git a/retroshare-gui/src/retroshare-gui.pro b/retroshare-gui/src/retroshare-gui.pro index cdb9cef3d..fdba34c9b 100644 --- a/retroshare-gui/src/retroshare-gui.pro +++ b/retroshare-gui/src/retroshare-gui.pro @@ -477,6 +477,12 @@ HEADERS += rshare.h \ gui/connect/PGPKeyDialog.h \ gui/connect/FriendRecommendDialog.h \ gui/msgs/MessagesDialog.h \ + gui/msgs/CalendarData.h \ + gui/msgs/CalendarWidget.h \ + gui/msgs/TasksWidget.h \ + gui/msgs/CalendarPropertiesDialog.h \ + gui/msgs/EventDialog.h \ + gui/msgs/TaskDialog.h \ gui/msgs/MessageInterface.h \ gui/msgs/MessageComposer.h \ gui/msgs/MessageWindow.h \ @@ -666,6 +672,8 @@ FORMS += gui/StartDialog.ui \ gui/msgs/MessageComposer.ui \ gui/msgs/MessageWindow.ui\ gui/msgs/MessageWidget.ui\ + gui/msgs/CalendarWidget.ui \ + gui/msgs/TasksWidget.ui \ gui/settings/settingsw.ui \ gui/settings/GeneralPage.ui \ gui/settings/ServerPage.ui \ @@ -835,6 +843,12 @@ SOURCES += main.cpp \ gui/connect/ConfCertDialog.cpp \ gui/connect/PGPKeyDialog.cpp \ gui/msgs/MessagesDialog.cpp \ + gui/msgs/CalendarData.cpp \ + gui/msgs/CalendarWidget.cpp \ + gui/msgs/TasksWidget.cpp \ + gui/msgs/CalendarPropertiesDialog.cpp \ + gui/msgs/EventDialog.cpp \ + gui/msgs/TaskDialog.cpp \ gui/msgs/MessageComposer.cpp \ gui/msgs/MessageWidget.cpp \ gui/msgs/MessageWindow.cpp \ From 8af209032c357909494e01fbb3000119041a211a Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:14:42 +0200 Subject: [PATCH 14/26] Fixed remove fixed sizes Added to show Calendar weeks --- .../src/gui/msgs/CalendarWidget.cpp | 170 +++++++++++++- retroshare-gui/src/gui/msgs/CalendarWidget.h | 16 ++ retroshare-gui/src/gui/msgs/CalendarWidget.ui | 210 ++++++++---------- retroshare-gui/src/gui/msgs/TasksWidget.ui | 6 - 4 files changed, 269 insertions(+), 133 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index 2467c0530..c7ed4b12a 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -21,6 +21,7 @@ #include "gui/msgs/CalendarWidget.h" #include "gui/msgs/EventDialog.h" #include "gui/msgs/CalendarPropertiesDialog.h" +#include #include #include #include @@ -62,6 +63,14 @@ void CalendarWidget::buildUi() { mWeekTable = ui.weekTable; mMonthTable = ui.monthTable; + // Create and insert calendar week label dynamically + mCwLabel = new QLabel(this); + mCwLabel->setObjectName("cwLabel"); + mCwLabel->setStyleSheet("font-weight: bold; font-size: 14px; margin-right: 15px;"); + int btnIndex = ui.topControlLayout->indexOf(ui.dayViewBtn); + if (btnIndex == -1) btnIndex = 6; + ui.topControlLayout->insertWidget(btnIndex, mCwLabel); + // Sidebar Calendar configs mSidebarCalendar->setSelectedDate(mSelectedDate); @@ -73,7 +82,6 @@ void CalendarWidget::buildUi() { mEventTable->setSelectionBehavior(QAbstractItemView::SelectRows); mEventTable->setSelectionMode(QAbstractItemView::SingleSelection); mEventTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - mEventTable->setMaximumHeight(120); connect(mEventTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); // Stacked widget pages setup @@ -89,7 +97,7 @@ void CalendarWidget::buildUi() { // 2. Week Table mWeekTable->setColumnCount(7); - mWeekTable->setHorizontalHeaderLabels({tr("Mon"), tr("Tue"), tr("Wed"), tr("Thu"), tr("Fri"), tr("Sat"), tr("Sun")}); + mWeekTable->setHorizontalHeaderLabels({tr("Monday"), tr("Tuesday"), tr("Wednesday"), tr("Thursday"), tr("Friday"), tr("Saturday"), tr("Sunday")}); mWeekTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); mWeekTable->verticalHeader()->setVisible(false); mWeekTable->setEditTriggers(QAbstractItemView::NoEditTriggers); @@ -97,11 +105,13 @@ void CalendarWidget::buildUi() { // 3. Month Table mMonthTable->setColumnCount(7); - mMonthTable->setHorizontalHeaderLabels({tr("Mon"), tr("Tue"), tr("Wed"), tr("Thu"), tr("Fri"), tr("Sat"), tr("Sun")}); + mMonthTable->setHorizontalHeaderLabels({tr("Monday"), tr("Tuesday"), tr("Wednesday"), tr("Thursday"), tr("Friday"), tr("Saturday"), tr("Sunday")}); mMonthTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); mMonthTable->verticalHeader()->setVisible(false); mMonthTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + mMonthTable->setItemDelegate(new MonthCalendarDelegate(this)); connect(mMonthTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + connect(mMonthTable, SIGNAL(cellClicked(int,int)), this, SLOT(onMonthCellClicked(int,int))); mViewStack->setCurrentIndex(mCurrentViewMode); @@ -174,9 +184,11 @@ void CalendarWidget::refreshData() { void CalendarWidget::updateViews() { mCellEventMap.clear(); - // 1. Update the Period Label + // 1. Update the Period Label and CW Label if (mCurrentViewMode == 0) { // Day View mPeriodLabel->setText(mSelectedDate.toString("dd MMMM yyyy")); + int cw = mSelectedDate.weekNumber(); + mCwLabel->setText(QString("CW: %1").arg(cw)); } else if (mCurrentViewMode == 1) { // Week View QDate monday = mSelectedDate.addDays(-(mSelectedDate.dayOfWeek() - 1)); QDate sunday = monday.addDays(6); @@ -185,8 +197,23 @@ void CalendarWidget::updateViews() { } else { mPeriodLabel->setText(monday.toString("dd MMM") + " - " + sunday.toString("dd MMM") + " " + sunday.toString("yyyy")); } + int cw = monday.weekNumber(); + mCwLabel->setText(QString("CW: %1").arg(cw)); } else { // Month View mPeriodLabel->setText(mSelectedDate.toString("MMMM yyyy")); + QDate firstOfMonth(mSelectedDate.year(), mSelectedDate.month(), 1); + int startDayOfWeek = firstOfMonth.dayOfWeek(); + QDate startDate = firstOfMonth.addDays(-(startDayOfWeek - 1)); + int daysInMonth = mSelectedDate.daysInMonth(); + int remainingDays = daysInMonth - (8 - startDayOfWeek); + int rowsNeeded = 1 + (remainingDays + 6) / 7; + int firstWeek = startDate.weekNumber(); + int lastWeek = startDate.addDays((rowsNeeded - 1) * 7).weekNumber(); + if (firstWeek == lastWeek) { + mCwLabel->setText(QString("CW: %1").arg(firstWeek)); + } else { + mCwLabel->setText(QString("CWs: %1-%2").arg(firstWeek).arg(lastWeek)); + } } // 2. Load and Filter Active Events @@ -328,13 +355,19 @@ void CalendarWidget::updateWeekView() { } void CalendarWidget::updateMonthView() { - mMonthTable->setRowCount(6); // A month calendar grid needs up to 6 rows + mMonthTable->clearContents(); // Find first day of the month QDate firstOfMonth(mSelectedDate.year(), mSelectedDate.month(), 1); int startDayOfWeek = firstOfMonth.dayOfWeek(); // 1=Mon, 7=Sun QDate startDate = firstOfMonth.addDays(-(startDayOfWeek - 1)); + int daysInMonth = mSelectedDate.daysInMonth(); + int remainingDays = daysInMonth - (8 - startDayOfWeek); + int rowsNeeded = 1 + (remainingDays + 6) / 7; + + mMonthTable->setRowCount(rowsNeeded); + const auto& events = CalendarData::instance()->getEvents(); QStringList enabledCalIds; @@ -343,13 +376,17 @@ void CalendarWidget::updateMonthView() { if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } - for (int row = 0; row < 6; ++row) { + for (int row = 0; row < rowsNeeded; ++row) { for (int col = 0; col < 7; ++col) { QDate date = startDate.addDays(row * 7 + col); // Build cell contents: "Date \n Event1 \n Event2..." QStringList cellLines; - cellLines << QString::number(date.day()); + if (date.day() == 1 || date.day() == date.daysInMonth()) { + cellLines << date.toString("d MMM"); + } else { + cellLines << QString::number(date.day()); + } QString matchedEventId = ""; for (const auto& ev : events) { @@ -361,11 +398,12 @@ void CalendarWidget::updateMonthView() { } QTableWidgetItem* cellItem = new QTableWidgetItem(cellLines.join("\n")); + cellItem->setData(Qt::UserRole + 1, date); // Store the QDate + if (date.month() != mSelectedDate.month()) { cellItem->setForeground(QBrush(Qt::gray)); } if (!matchedEventId.isEmpty()) { - cellItem->setBackground(QBrush(QColor("#eef5fc"))); mCellEventMap[QString("2_%1_%2").arg(row).arg(col)] = matchedEventId; } mMonthTable->setItem(row, col, cellItem); @@ -373,8 +411,8 @@ void CalendarWidget::updateMonthView() { } // Set row heights to expand nicely in the month grid - for (int row = 0; row < 6; ++row) { - mMonthTable->setRowHeight(row, 60); + for (int row = 0; row < rowsNeeded; ++row) { + mMonthTable->setRowHeight(row, 80); } } @@ -547,3 +585,115 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { } } } + +void CalendarWidget::onMonthCellClicked(int row, int col) { + QTableWidgetItem* item = mMonthTable->item(row, col); + if (item) { + QDate date = item->data(Qt::UserRole + 1).toDate(); + if (date.isValid()) { + mSelectedDate = date; + mSidebarCalendar->blockSignals(true); + mSidebarCalendar->setSelectedDate(date); + mSidebarCalendar->blockSignals(false); + updateViews(); + } + } +} + +MonthCalendarDelegate::MonthCalendarDelegate(CalendarWidget* parent) + : QStyledItemDelegate(parent), mCalendarWidget(parent) {} + +void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + + QDate cellDate = index.data(Qt::UserRole + 1).toDate(); + bool isSelected = (cellDate.isValid() && cellDate == mCalendarWidget->selectedDate()); + + // Draw background + QColor bgColor; + if (isSelected) { + bgColor = QColor("#eff6ff"); // Light blue highlight for selected day + } else if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { + bgColor = QColor("#f8fafc"); // Slate-50 for days outside the current month + } else if (index.column() == 5 || index.column() == 6) { + bgColor = QColor("#f1f5f9"); // Slate-100 for weekends + } else { + bgColor = QColor("#ffffff"); // White for standard weekdays + } + painter->fillRect(option.rect, bgColor); + + // Draw cell border + if (isSelected) { + painter->setPen(QPen(QColor("#3b82f6"), 2)); + painter->drawRect(option.rect.adjusted(1, 1, -1, -1)); + } else { + painter->setPen(QPen(QColor("#e2e8f0"), 1)); + painter->drawRect(option.rect); + } + + // Get item text + QString text = index.data(Qt::DisplayRole).toString(); + QStringList lines = text.split('\n'); + if (!lines.isEmpty()) { + QString dayStr = lines.first(); + + // 1. Draw day number in top right + QFont dayFont = option.font; + dayFont.setBold(true); + painter->setFont(dayFont); + + if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { + painter->setPen(QColor("#94a3b8")); // Muted grey for other month days + } else if (isSelected) { + painter->setPen(QColor("#2563eb")); // Darker blue for selected day number + } else { + painter->setPen(QColor("#1e293b")); // Slate-800 for standard days + } + + QRect dayRect = option.rect.adjusted(5, 5, -8, -5); + painter->drawText(dayRect, Qt::AlignTop | Qt::AlignRight, dayStr); + + // 2. Draw Week Badge if it's the first column + if (index.column() == 0 && cellDate.isValid()) { + int weekNum = cellDate.weekNumber(); + QString weekStr = QString("W %1").arg(weekNum); + + QRect badgeRect(option.rect.left() + 6, option.rect.top() + 5, 38, 16); + painter->setPen(Qt::NoPen); + painter->setBrush(QColor("#e2e8f0")); // Slate-200 + painter->drawRoundedRect(badgeRect, 8, 8); + + QFont badgeFont = option.font; + badgeFont.setPointSize(badgeFont.pointSize() - 2); + badgeFont.setBold(true); + painter->setFont(badgeFont); + painter->setPen(QColor("#475569")); // Slate-600 + painter->drawText(badgeRect, Qt::AlignCenter, weekStr); + } + + // 3. Draw events list below + int yOffset = option.rect.top() + 26; + QFont eventFont = option.font; + eventFont.setPointSize(eventFont.pointSize() - 1); + painter->setFont(eventFont); + + for (int i = 1; i < lines.size(); ++i) { + if (yOffset + 18 > option.rect.bottom()) break; // Out of bounds + + QString eventTitle = lines[i]; + QRect eventRect(option.rect.left() + 6, yOffset, option.rect.width() - 12, 16); + + painter->setPen(Qt::NoPen); + painter->setBrush(QColor("#e0f2fe")); // Light blue event background + painter->drawRoundedRect(eventRect, 3, 3); + + painter->setPen(QColor("#0369a1")); // Blue text for events + painter->drawText(eventRect.adjusted(4, 0, -4, 0), Qt::AlignVCenter | Qt::AlignLeft, eventTitle); + + yOffset += 19; + } + } + + painter->restore(); +} diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h index ad0cb9b6d..8ce9416f3 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -18,16 +18,29 @@ * * *******************************************************************************/ + #ifndef CALENDARWIDGET_H #define CALENDARWIDGET_H #include #include #include +#include #include "gui/msgs/CalendarData.h" #include "ui_CalendarWidget.h" class QListWidgetItem; +class QLabel; +class CalendarWidget; + +class MonthCalendarDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit MonthCalendarDelegate(CalendarWidget* parent); + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; +private: + CalendarWidget* mCalendarWidget; +}; class CalendarWidget : public QWidget { Q_OBJECT @@ -36,6 +49,7 @@ public: ~CalendarWidget(); void refreshData(); + QDate selectedDate() const { return mSelectedDate; } private slots: void onNewEvent(); @@ -49,6 +63,7 @@ private slots: void onCalendarSelectionChanged(QListWidgetItem* item); void onSearchChanged(const QString& text); void onCalendarContextMenu(const QPoint& pos); + void onMonthCellClicked(int row, int col); private: void buildUi(); @@ -67,6 +82,7 @@ private: QListWidget* mCalendarList; QLabel* mPeriodLabel; + QLabel* mCwLabel; QLineEdit* mSearchEdit; QTableWidget* mEventTable; // Upcoming events list at top diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.ui b/retroshare-gui/src/gui/msgs/CalendarWidget.ui index ff62a2bbb..d99c3d9c7 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.ui +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.ui @@ -35,12 +35,6 @@ 1 - - - 280 - 16777215 - - 12 @@ -107,22 +101,7 @@ - - - 10 - - - 10 - - - 10 - - - 10 - - - 10 - + @@ -218,103 +197,100 @@ - - - - 16777215 - 120 - + + + Qt::Orientation::Vertical - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QAbstractItemView::NoEditTriggers - - - - + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + QAbstractItemView::SelectionMode::SingleSelection + + + QAbstractItemView::SelectionBehavior::SelectRows + - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QAbstractItemView::NoEditTriggers - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QAbstractItemView::NoEditTriggers - - - - + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + + + diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.ui b/retroshare-gui/src/gui/msgs/TasksWidget.ui index 01e3d9a0a..452043cf7 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.ui +++ b/retroshare-gui/src/gui/msgs/TasksWidget.ui @@ -35,12 +35,6 @@ 1 - - - 280 - 16777215 - - 12 From 46a0c849fc9fde32920bdf8c8bb1417d7d18a5d3 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:32:45 +0200 Subject: [PATCH 15/26] add export and import calendars --- .../src/gui/msgs/CalendarPropertiesDialog.cpp | 12 + .../src/gui/msgs/CalendarPropertiesDialog.h | 2 + .../src/gui/msgs/CalendarWidget.cpp | 248 +++++++++++++++++- retroshare-gui/src/gui/msgs/CalendarWidget.h | 2 + retroshare-gui/src/gui/msgs/TasksWidget.cpp | 1 - 5 files changed, 262 insertions(+), 3 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp index 1cebf2a8f..61bf8c376 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp @@ -114,6 +114,10 @@ void CalendarPropertiesDialog::setupUi() { mRadioNetwork->setStyleSheet("font-size: 13px; font-weight: bold; padding: 5px;"); page1Layout->addWidget(mRadioNetwork); + mRadioImport = new QRadioButton(tr("Import a calendar from an iCalendar (.ics) file"), mPage1); + mRadioImport->setStyleSheet("font-size: 13px; font-weight: bold; padding: 5px;"); + page1Layout->addWidget(mRadioImport); + page1Layout->addStretch(); mStackedWidget->addWidget(mPage1); @@ -211,6 +215,10 @@ void CalendarPropertiesDialog::loadIdentities() { } void CalendarPropertiesDialog::onNext() { + if (mRadioImport && mRadioImport->isChecked()) { + accept(); + return; + } mStackedWidget->setCurrentWidget(mPage2); mBackBtn->show(); mCreateOrSaveBtn->show(); @@ -262,3 +270,7 @@ CalendarInfo CalendarPropertiesDialog::getCalendarInfo() const { info.owner = "local"; return info; } + +bool CalendarPropertiesDialog::isImportMode() const { + return mRadioImport && mRadioImport->isChecked(); +} diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h index 158d8ee68..35c0a6990 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h @@ -39,6 +39,7 @@ public: ~CalendarPropertiesDialog(); CalendarInfo getCalendarInfo() const; + bool isImportMode() const; private slots: void onNext(); @@ -62,6 +63,7 @@ private: // Page 1 widgets QRadioButton* mRadioComputer; QRadioButton* mRadioNetwork; + QRadioButton* mRadioImport; // Page 2 widgets QLineEdit* mNameEdit; diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index c7ed4b12a..f91db0649 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -22,6 +22,12 @@ #include "gui/msgs/EventDialog.h" #include "gui/msgs/CalendarPropertiesDialog.h" #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -469,7 +475,11 @@ void CalendarWidget::onNewEvent() { void CalendarWidget::onNewCalendar() { CalendarPropertiesDialog dlg("", this); if (dlg.exec() == QDialog::Accepted) { - refreshData(); + if (dlg.isImportMode()) { + importCalendar(); + } else { + refreshData(); + } } } @@ -575,7 +585,7 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { refreshData(); } } else if (selectedAct == exportAct) { - QMessageBox::information(this, tr("Export Calendar"), tr("Calendar '%1' exported successfully!").arg(calName)); + exportCalendar(calId, calName); } else if (selectedAct == publishAct) { QMessageBox::information(this, tr("Publish Calendar"), tr("Calendar '%1' published successfully!").arg(calName)); } else if (selectedAct == propertiesAct) { @@ -586,6 +596,240 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { } } +void CalendarWidget::exportCalendar(const QString& calId, const QString& calName) { + QString icsContent = "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//RetroShare//Calendar//EN\r\n"; + + const auto& events = CalendarData::instance()->getEvents(); + for (const auto& ev : events) { + if (ev.calendarId != calId) continue; + + icsContent += "BEGIN:VEVENT\r\n"; + icsContent += QString("UID:%1\r\n").arg(ev.id); + icsContent += QString("SUMMARY:%1\r\n").arg(ev.title); + + if (!ev.description.isEmpty()) { + QString desc = ev.description; + desc.replace("\n", "\\n").replace("\r", ""); + icsContent += QString("DESCRIPTION:%1\r\n").arg(desc); + } + + if (!ev.location.isEmpty()) { + icsContent += QString("LOCATION:%1\r\n").arg(ev.location); + } + + if (!ev.category.isEmpty()) { + icsContent += QString("CATEGORIES:%1\r\n").arg(ev.category); + } + + if (ev.allDay) { + icsContent += QString("DTSTART;VALUE=DATE:%1\r\n").arg(ev.start.toString("yyyyMMdd")); + icsContent += QString("DTEND;VALUE=DATE:%1\r\n").arg(ev.end.toString("yyyyMMdd")); + } else { + icsContent += QString("DTSTART:%1\r\n").arg(ev.start.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + icsContent += QString("DTEND:%1\r\n").arg(ev.end.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + } + + icsContent += "END:VEVENT\r\n"; + } + icsContent += "END:VCALENDAR\r\n"; + + QString defaultFileName = QString("%1.ics").arg(calName); + defaultFileName.replace(QRegExp("[\\\\/:*?\"<>|]"), "_"); + + QString selectedFilter; + QString filePath = QFileDialog::getSaveFileName( + this, + tr("Export Calendar"), + defaultFileName, + tr("iCalendar files (*.ics);;All Files (*)"), + &selectedFilter + ); + + if (!filePath.isEmpty()) { + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical( + this, + tr("Export Error"), + tr("Could not open file %1 for writing.").arg(filePath) + ); + } else { + QTextStream out(&file); + out.setCodec("UTF-8"); + out << icsContent; + file.close(); + + QMessageBox::information( + this, + tr("Export Calendar"), + tr("Calendar '%1' exported successfully to %2!").arg(calName).arg(QDir::toNativeSeparators(filePath)) + ); + } + } +} + +void CalendarWidget::importCalendar() { + QString filePath = QFileDialog::getOpenFileName( + this, + tr("Import Calendar"), + "", + tr("iCalendar files (*.ics);;All Files (*)") + ); + + if (filePath.isEmpty()) return; + + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical( + this, + tr("Import Error"), + tr("Could not open file %1 for reading.").arg(filePath) + ); + return; + } + + QTextStream in(&file); + in.setCodec("UTF-8"); + + QStringList rawLines; + while (!in.atEnd()) { + rawLines.append(in.readLine()); + } + file.close(); + + // iCalendar line unfolding (RFC 5545) + QStringList lines; + for (int i = 0; i < rawLines.size(); ++i) { + QString line = rawLines[i]; + while (i + 1 < rawLines.size() && (rawLines[i + 1].startsWith(" ") || rawLines[i + 1].startsWith("\t"))) { + line += rawLines[i + 1].mid(1); + i++; + } + lines.append(line); + } + + QString calendarName = QFileInfo(filePath).baseName(); + QList importedEvents; + + auto parseIcsDateTime = [](const QString& val) -> QDateTime { + QDateTime dt; + if (val.endsWith('Z')) { + dt = QDateTime::fromString(val, "yyyyMMdd'T'HHmmss'Z'"); + dt.setTimeSpec(Qt::UTC); + dt = dt.toLocalTime(); + } else { + dt = QDateTime::fromString(val, "yyyyMMdd'T'HHmmss"); + dt.setTimeSpec(Qt::LocalTime); + } + return dt; + }; + + bool inEvent = false; + CalendarEvent currentEvent; + + for (const QString& line : lines) { + QString trimmedLine = line.trimmed(); + if (trimmedLine.isEmpty()) continue; + + if (trimmedLine.startsWith("X-WR-CALNAME:", Qt::CaseInsensitive)) { + QString nameVal = trimmedLine.mid(13).trimmed(); + if (!nameVal.isEmpty()) calendarName = nameVal; + } else if (trimmedLine.startsWith("BEGIN:VEVENT", Qt::CaseInsensitive)) { + inEvent = true; + currentEvent = CalendarEvent(); + currentEvent.id = QUuid::createUuid().toString(); + currentEvent.allDay = false; + currentEvent.isPublic = false; + } else if (trimmedLine.startsWith("END:VEVENT", Qt::CaseInsensitive)) { + if (inEvent) { + // Validate dates + if (!currentEvent.start.isValid()) { + currentEvent.start = QDateTime::currentDateTime(); + } + if (!currentEvent.end.isValid()) { + currentEvent.end = currentEvent.start.addSecs(3600); + } + importedEvents.append(currentEvent); + inEvent = false; + } + } else if (inEvent) { + int colonIdx = trimmedLine.indexOf(':'); + int semiIdx = trimmedLine.indexOf(';'); + int splitIdx = -1; + + if (colonIdx != -1 && semiIdx != -1) { + splitIdx = qMin(colonIdx, semiIdx); + } else if (colonIdx != -1) { + splitIdx = colonIdx; + } else if (semiIdx != -1) { + splitIdx = semiIdx; + } + + if (splitIdx != -1) { + QString key = trimmedLine.left(splitIdx).trimmed(); + QString val = trimmedLine.mid(colonIdx + 1).trimmed(); + + if (key.compare("UID", Qt::CaseInsensitive) == 0) { + currentEvent.id = val; + } else if (key.compare("SUMMARY", Qt::CaseInsensitive) == 0) { + currentEvent.title = val; + } else if (key.compare("LOCATION", Qt::CaseInsensitive) == 0) { + currentEvent.location = val; + } else if (key.compare("CATEGORIES", Qt::CaseInsensitive) == 0) { + currentEvent.category = val; + } else if (key.compare("DESCRIPTION", Qt::CaseInsensitive) == 0) { + QString desc = val; + desc.replace("\\n", "\n").replace("\\r", "").replace("\\,", ","); + currentEvent.description = desc; + } else if (key.startsWith("DTSTART", Qt::CaseInsensitive)) { + if (trimmedLine.contains("VALUE=DATE", Qt::CaseInsensitive)) { + currentEvent.allDay = true; + currentEvent.start = QDateTime(QDate::fromString(val, "yyyyMMdd"), QTime(0, 0)); + } else { + currentEvent.start = parseIcsDateTime(val); + } + } else if (key.startsWith("DTEND", Qt::CaseInsensitive)) { + if (trimmedLine.contains("VALUE=DATE", Qt::CaseInsensitive)) { + currentEvent.allDay = true; + currentEvent.end = QDateTime(QDate::fromString(val, "yyyyMMdd"), QTime(0, 0)); + } else { + currentEvent.end = parseIcsDateTime(val); + } + } + } + } + } + + // Create the calendar info + CalendarInfo cal; + cal.id = QUuid::createUuid().toString(); + cal.name = calendarName; + cal.color = QColor("#4a90e2"); + cal.isPublic = false; + cal.owner = "local"; + cal.showReminders = true; + cal.email = ""; + cal.onNetwork = false; + + CalendarData::instance()->addCalendar(cal); + + // Add all events to CalendarData + for (auto& ev : importedEvents) { + ev.calendarId = cal.id; + CalendarData::instance()->addEvent(ev); + } + + QMessageBox::information( + this, + tr("Import Calendar"), + tr("Successfully imported calendar '%1' with %2 events!").arg(calendarName).arg(importedEvents.size()) + ); + + refreshData(); +} + void CalendarWidget::onMonthCellClicked(int row, int col) { QTableWidgetItem* item = mMonthTable->item(row, col); if (item) { diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h index 8ce9416f3..c80d48d0f 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -72,6 +72,8 @@ private: void updateWeekView(); void updateMonthView(); void updateEventList(); + void exportCalendar(const QString& calId, const QString& calName); + void importCalendar(); QDate mSelectedDate; int mCurrentViewMode; // 0=Day, 1=Week, 2=Month diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/msgs/TasksWidget.cpp index 637ebac67..0335488f9 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.cpp +++ b/retroshare-gui/src/gui/msgs/TasksWidget.cpp @@ -215,7 +215,6 @@ void TasksWidget::onQuickTaskAdded() { // Choose the first enabled calendar QString calId = "personal"; - const auto& cals = CalendarData::instance()->getCalendars(); for (int i = 0; i < mCalendarList->count(); ++i) { QListWidgetItem* item = mCalendarList->item(i); if (item->checkState() == Qt::Checked) { From 7de89372ad7daf541726ba0b84b5de5770fe6ac3 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 5 Jun 2026 00:13:22 +0200 Subject: [PATCH 16/26] Added gxs backend for calendar --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 505 +++++++++++++++++- retroshare-gui/src/gui/msgs/CalendarData.h | 26 +- .../src/gui/msgs/CalendarPropertiesDialog.cpp | 39 +- .../src/gui/msgs/CalendarWidget.cpp | 145 +++-- retroshare-gui/src/gui/msgs/CalendarWidget.h | 8 + retroshare-gui/src/gui/msgs/TasksWidget.cpp | 187 +++++-- retroshare-gui/src/gui/msgs/TasksWidget.h | 9 + 7 files changed, 845 insertions(+), 74 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index fe475a9ca..3603ab89d 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -21,6 +21,8 @@ #include "gui/msgs/CalendarData.h" #include #include +#include +#include #include #include #include @@ -34,12 +36,24 @@ CalendarData* CalendarData::instance() { return mInstance; } -CalendarData::CalendarData() { +CalendarData::CalendarData() : QObject(), mEventHandlerId(0) { loadData(); + + if (rsEvents && rsGxsCalendar) { + rsEvents->registerEventsHandler( + [this](std::shared_ptr event) { + RsQThreadUtils::postToObject([=]() { handleGxsEvent(event); }, this); + }, + mEventHandlerId, RsEventType::GXS_CALENDAR + ); + } } CalendarData::~CalendarData() { saveData(); + if (rsEvents && mEventHandlerId != 0) { + rsEvents->unregisterEventsHandler(mEventHandlerId); + } } void CalendarData::loadData() { @@ -78,7 +92,7 @@ void CalendarData::loadData() { defaultCal.isPublic = false; defaultCal.owner = "local"; defaultCal.showReminders = true; - defaultCal.email = "defnator "; + defaultCal.email = "retroshare "; defaultCal.onNetwork = false; mCalendars.append(defaultCal); @@ -89,7 +103,7 @@ void CalendarData::loadData() { testCal.isPublic = true; testCal.owner = "local"; testCal.showReminders = true; - testCal.email = "defnator "; + testCal.email = "retroshare "; testCal.onNetwork = true; mCalendars.append(testCal); } @@ -225,6 +239,11 @@ void CalendarData::updateCalendar(const CalendarInfo& cal) { void CalendarData::removeCalendar(const QString& id) { for (int i = 0; i < mCalendars.size(); ++i) { if (mCalendars[i].id == id) { + // Unsubscribe from GXS if network calendar + if (mCalendars[i].onNetwork && rsGxsCalendar) { + std::string errMsg; + rsGxsCalendar->subscribeToCalendar(RsGxsGroupId(id.toStdString()), false, errMsg); + } mCalendars.removeAt(i); break; } @@ -242,6 +261,7 @@ void CalendarData::removeCalendar(const QString& id) { void CalendarData::addEvent(const CalendarEvent& ev) { mEvents.append(ev); saveData(); + publishCalendarUpdates(ev.calendarId); } void CalendarData::updateEvent(const CalendarEvent& ev) { @@ -252,21 +272,28 @@ void CalendarData::updateEvent(const CalendarEvent& ev) { } } saveData(); + publishCalendarUpdates(ev.calendarId); } void CalendarData::removeEvent(const QString& id) { + QString calId; for (int i = 0; i < mEvents.size(); ++i) { if (mEvents[i].id == id) { + calId = mEvents[i].calendarId; mEvents.removeAt(i); break; } } saveData(); + if (!calId.isEmpty()) { + publishCalendarUpdates(calId); + } } void CalendarData::addTask(const CalendarTask& task) { mTasks.append(task); saveData(); + publishCalendarUpdates(task.calendarId); } void CalendarData::updateTask(const CalendarTask& task) { @@ -277,16 +304,22 @@ void CalendarData::updateTask(const CalendarTask& task) { } } saveData(); + publishCalendarUpdates(task.calendarId); } void CalendarData::removeTask(const QString& id) { + QString calId; for (int i = 0; i < mTasks.size(); ++i) { if (mTasks[i].id == id) { + calId = mTasks[i].calendarId; mTasks.removeAt(i); break; } } saveData(); + if (!calId.isEmpty()) { + publishCalendarUpdates(calId); + } } QMap CalendarData::getContacts() { @@ -315,3 +348,469 @@ QMap CalendarData::getContacts() { return contacts; } + +static RsGxsId getGxsIdFromEmail(const QString& email) { + int idx = email.lastIndexOf('@'); + int endIdx = email.lastIndexOf('>'); + if (idx != -1 && endIdx != -1 && endIdx > idx) { + std::string gxsIdStr = email.mid(idx + 1, endIdx - idx - 1).toStdString(); + return RsGxsId(gxsIdStr); + } + return RsGxsId(); +} + +QString CalendarData::exportCalendarToIcs(const QString& calId) const { + QString icsContent = "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//RetroShare//Calendar//EN\r\n"; + + for (const auto& ev : mEvents) { + if (ev.calendarId != calId) continue; + + icsContent += "BEGIN:VEVENT\r\n"; + icsContent += QString("UID:%1\r\n").arg(ev.id); + icsContent += QString("SUMMARY:%1\r\n").arg(ev.title); + + if (!ev.description.isEmpty()) { + QString desc = ev.description; + desc.replace("\n", "\\n").replace("\r", ""); + icsContent += QString("DESCRIPTION:%1\r\n").arg(desc); + } + + if (!ev.location.isEmpty()) { + icsContent += QString("LOCATION:%1\r\n").arg(ev.location); + } + + if (!ev.category.isEmpty()) { + icsContent += QString("CATEGORIES:%1\r\n").arg(ev.category); + } + + if (ev.allDay) { + icsContent += QString("DTSTART;VALUE=DATE:%1\r\n").arg(ev.start.toString("yyyyMMdd")); + icsContent += QString("DTEND;VALUE=DATE:%1\r\n").arg(ev.end.toString("yyyyMMdd")); + } else { + icsContent += QString("DTSTART:%1\r\n").arg(ev.start.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + icsContent += QString("DTEND:%1\r\n").arg(ev.end.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + } + + icsContent += "END:VEVENT\r\n"; + } + + for (const auto& t : mTasks) { + if (t.calendarId != calId) continue; + + icsContent += "BEGIN:VTODO\r\n"; + icsContent += QString("UID:%1\r\n").arg(t.id); + icsContent += QString("SUMMARY:%1\r\n").arg(t.title); + + if (!t.description.isEmpty()) { + QString desc = t.description; + desc.replace("\n", "\\n").replace("\r", ""); + icsContent += QString("DESCRIPTION:%1\r\n").arg(desc); + } + + if (!t.location.isEmpty()) { + icsContent += QString("LOCATION:%1\r\n").arg(t.location); + } + + if (!t.category.isEmpty()) { + icsContent += QString("CATEGORIES:%1\r\n").arg(t.category); + } + + if (t.hasStart) { + icsContent += QString("DTSTART:%1\r\n").arg(t.start.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + } + + if (t.hasDue) { + icsContent += QString("DUE:%1\r\n").arg(t.due.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); + } + + if (!t.status.isEmpty()) { + icsContent += QString("STATUS:%1\r\n").arg(t.status); + } + + icsContent += QString("PERCENT-COMPLETE:%1\r\n").arg(t.percentComplete); + icsContent += QString("COMPLETED:%1\r\n").arg(t.completed ? "TRUE" : "FALSE"); + + icsContent += "END:VTODO\r\n"; + } + + icsContent += "END:VCALENDAR\r\n"; + return icsContent; +} + +void CalendarData::importCalendarFromIcs(const QString& calId, const QString& icsData) { + auto evIt = mEvents.begin(); + while (evIt != mEvents.end()) { + if (evIt->calendarId == calId) { + evIt = mEvents.erase(evIt); + } else { + ++evIt; + } + } + + auto tIt = mTasks.begin(); + while (tIt != mTasks.end()) { + if (tIt->calendarId == calId) { + tIt = mTasks.erase(tIt); + } else { + ++tIt; + } + } + + QStringList rawLines = icsData.split(QRegExp("[\r\n]"), QString::SkipEmptyParts); + QStringList lines; + for (int i = 0; i < rawLines.size(); ++i) { + QString line = rawLines[i]; + while (i + 1 < rawLines.size() && (rawLines[i + 1].startsWith(" ") || rawLines[i + 1].startsWith("\t"))) { + line += rawLines[i + 1].mid(1); + i++; + } + lines.append(line); + } + + auto parseIcsDateTime = [](const QString& val) -> QDateTime { + QDateTime dt; + if (val.endsWith('Z')) { + dt = QDateTime::fromString(val, "yyyyMMdd'T'HHmmss'Z'"); + dt.setTimeSpec(Qt::UTC); + dt = dt.toLocalTime(); + } else { + dt = QDateTime::fromString(val, "yyyyMMdd'T'HHmmss"); + dt.setTimeSpec(Qt::LocalTime); + } + return dt; + }; + + bool inEvent = false; + bool inTask = false; + CalendarEvent currentEvent; + CalendarTask currentTask; + + for (const QString& line : lines) { + QString trimmedLine = line.trimmed(); + if (trimmedLine.isEmpty()) continue; + + if (trimmedLine.startsWith("BEGIN:VEVENT", Qt::CaseInsensitive)) { + inEvent = true; + currentEvent = CalendarEvent(); + currentEvent.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + currentEvent.calendarId = calId; + currentEvent.allDay = false; + currentEvent.isPublic = true; + } else if (trimmedLine.startsWith("END:VEVENT", Qt::CaseInsensitive)) { + if (inEvent) { + if (!currentEvent.start.isValid()) currentEvent.start = QDateTime::currentDateTime(); + if (!currentEvent.end.isValid()) currentEvent.end = currentEvent.start.addSecs(3600); + mEvents.append(currentEvent); + inEvent = false; + } + } else if (trimmedLine.startsWith("BEGIN:VTODO", Qt::CaseInsensitive)) { + inTask = true; + currentTask = CalendarTask(); + currentTask.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + currentTask.calendarId = calId; + currentTask.hasStart = false; + currentTask.hasDue = false; + currentTask.completed = false; + currentTask.percentComplete = 0; + } else if (trimmedLine.startsWith("END:VTODO", Qt::CaseInsensitive)) { + if (inTask) { + mTasks.append(currentTask); + inTask = false; + } + } else if (inEvent) { + int colonIdx = trimmedLine.indexOf(':'); + int semiIdx = trimmedLine.indexOf(';'); + int splitIdx = -1; + + if (colonIdx != -1 && semiIdx != -1) splitIdx = qMin(colonIdx, semiIdx); + else if (colonIdx != -1) splitIdx = colonIdx; + else if (semiIdx != -1) splitIdx = semiIdx; + + if (splitIdx != -1) { + QString key = trimmedLine.left(splitIdx).trimmed(); + QString val = trimmedLine.mid(colonIdx + 1).trimmed(); + + if (key.compare("UID", Qt::CaseInsensitive) == 0) { + currentEvent.id = val; + } else if (key.compare("SUMMARY", Qt::CaseInsensitive) == 0) { + currentEvent.title = val; + } else if (key.compare("LOCATION", Qt::CaseInsensitive) == 0) { + currentEvent.location = val; + } else if (key.compare("CATEGORIES", Qt::CaseInsensitive) == 0) { + currentEvent.category = val; + } else if (key.compare("DESCRIPTION", Qt::CaseInsensitive) == 0) { + currentEvent.description = val.replace("\\n", "\n").replace("\\r", "").replace("\\,", ","); + } else if (key.startsWith("DTSTART", Qt::CaseInsensitive)) { + if (trimmedLine.contains("VALUE=DATE", Qt::CaseInsensitive)) { + currentEvent.allDay = true; + currentEvent.start = QDateTime(QDate::fromString(val, "yyyyMMdd"), QTime(0, 0)); + } else { + currentEvent.start = parseIcsDateTime(val); + } + } else if (key.startsWith("DTEND", Qt::CaseInsensitive)) { + if (trimmedLine.contains("VALUE=DATE", Qt::CaseInsensitive)) { + currentEvent.allDay = true; + currentEvent.end = QDateTime(QDate::fromString(val, "yyyyMMdd"), QTime(0, 0)); + } else { + currentEvent.end = parseIcsDateTime(val); + } + } + } + } else if (inTask) { + int colonIdx = trimmedLine.indexOf(':'); + int semiIdx = trimmedLine.indexOf(';'); + int splitIdx = -1; + + if (colonIdx != -1 && semiIdx != -1) splitIdx = qMin(colonIdx, semiIdx); + else if (colonIdx != -1) splitIdx = colonIdx; + else if (semiIdx != -1) splitIdx = semiIdx; + + if (splitIdx != -1) { + QString key = trimmedLine.left(splitIdx).trimmed(); + QString val = trimmedLine.mid(colonIdx + 1).trimmed(); + + if (key.compare("UID", Qt::CaseInsensitive) == 0) { + currentTask.id = val; + } else if (key.compare("SUMMARY", Qt::CaseInsensitive) == 0) { + currentTask.title = val; + } else if (key.compare("LOCATION", Qt::CaseInsensitive) == 0) { + currentTask.location = val; + } else if (key.compare("CATEGORIES", Qt::CaseInsensitive) == 0) { + currentTask.category = val; + } else if (key.compare("DESCRIPTION", Qt::CaseInsensitive) == 0) { + currentTask.description = val.replace("\\n", "\n").replace("\\r", "").replace("\\,", ","); + } else if (key.startsWith("DTSTART", Qt::CaseInsensitive)) { + currentTask.hasStart = true; + currentTask.start = parseIcsDateTime(val); + } else if (key.startsWith("DUE", Qt::CaseInsensitive)) { + currentTask.hasDue = true; + currentTask.due = parseIcsDateTime(val); + } else if (key.compare("STATUS", Qt::CaseInsensitive) == 0) { + currentTask.status = val; + } else if (key.compare("PERCENT-COMPLETE", Qt::CaseInsensitive) == 0) { + currentTask.percentComplete = val.toInt(); + } else if (key.compare("COMPLETED", Qt::CaseInsensitive) == 0) { + currentTask.completed = (val.compare("TRUE", Qt::CaseInsensitive) == 0); + } + } + } + } +} + +void CalendarData::migrateCalendarData(const QString& oldId, const QString& newId) { + for (auto& ev : mEvents) { + if (ev.calendarId == oldId) { + ev.calendarId = newId; + } + } + for (auto& t : mTasks) { + if (t.calendarId == oldId) { + t.calendarId = newId; + } + } +} + +void CalendarData::publishCalendarUpdates(const QString& calId) { + if (!rsGxsCalendar) return; + + CalendarInfo cal; + bool found = false; + for (const auto& c : mCalendars) { + if (c.id == calId) { + cal = c; + found = true; + break; + } + } + if (!found || !cal.onNetwork) return; + + RsGxsId authorId = getGxsIdFromEmail(cal.email); + RsGxsGroupId groupId(calId.toStdString()); + QString ics = exportCalendarToIcs(calId); + RsGxsMessageId msgId; + std::string errMsg; + rsGxsCalendar->publishCalendarIcs(groupId, ics.toStdString(), authorId, msgId, errMsg); +} + +bool CalendarData::publishCalendar(const QString& oldId, const QString& email, QString& newIdOut) { + if (!rsGxsCalendar) return false; + + // Find local calendar + int calIdx = -1; + for (int i = 0; i < mCalendars.size(); ++i) { + if (mCalendars[i].id == oldId) { + calIdx = i; + break; + } + } + if (calIdx == -1) return false; + + CalendarInfo& cal = mCalendars[calIdx]; + if (cal.onNetwork) return false; // Already on network + + // Extract GXS ID from email + RsGxsId authorId = getGxsIdFromEmail(email); + + RsGxsGroupId groupId; + std::string errMsg; + if (!rsGxsCalendar->createCalendar(cal.name.toStdString(), "RetroShare Calendar", authorId, groupId, errMsg)) { + return false; + } + + QString newId = QString::fromStdString(groupId.toStdString()); + newIdOut = newId; + + // Migrate events and tasks + migrateCalendarData(oldId, newId); + + // Update calendar info + cal.id = newId; + cal.onNetwork = true; + cal.isPublic = true; + cal.email = email; + cal.owner = "local"; + + saveData(); + + // Subscribe and publish initial ICS + rsGxsCalendar->subscribeToCalendar(groupId, true, errMsg); + publishCalendarUpdates(newId); + + emit calendarDataChanged(); + return true; +} + +bool CalendarData::subscribeToCalendar(const QString& id, bool subscribe, const QString& name) { + if (!rsGxsCalendar) return false; + + RsGxsGroupId groupId(id.toStdString()); + std::string errMsg; + if (!rsGxsCalendar->subscribeToCalendar(groupId, subscribe, errMsg)) { + return false; + } + + if (subscribe) { + // Find if it's already in mCalendars + bool found = false; + for (const auto& c : mCalendars) { + if (c.id == id) { + found = true; + break; + } + } + if (!found) { + CalendarInfo localCal; + localCal.id = id; + localCal.name = name.isEmpty() ? tr("Shared Calendar") : name; + localCal.color = QColor("#4a90e2"); + localCal.isPublic = true; + localCal.onNetwork = true; + localCal.showReminders = true; + localCal.owner = "network"; + mCalendars.append(localCal); + saveData(); + } + emit calendarDataChanged(); + // Trigger sync to fetch contents + syncWithGxs(); + } else { + // Remove from local list + for (int i = 0; i < mCalendars.size(); ++i) { + if (mCalendars[i].id == id) { + mCalendars.removeAt(i); + break; + } + } + // Remove associated events and tasks + mEvents.erase(std::remove_if(mEvents.begin(), mEvents.end(), + [&id](const CalendarEvent& ev) { return ev.calendarId == id; }), mEvents.end()); + mTasks.erase(std::remove_if(mTasks.begin(), mTasks.end(), + [&id](const CalendarTask& t) { return t.calendarId == id; }), mTasks.end()); + + saveData(); + emit calendarDataChanged(); + } + return true; +} + +void CalendarData::syncWithGxs() { + if (!rsGxsCalendar) return; + + std::list calendars; + if (rsGxsCalendar->getCalendarsSummaries(calendars)) { + bool changed = false; + for (const auto& meta : calendars) { + bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); + if (isSubscribed) { + QString calId = QString::fromStdString(meta.mGroupId.toStdString()); + + bool found = false; + CalendarInfo localCal; + for (auto& c : mCalendars) { + if (c.id == calId) { + localCal = c; + found = true; + break; + } + } + + if (!found) { + localCal.id = calId; + localCal.name = QString::fromUtf8(meta.mGroupName.c_str()); + localCal.color = QColor("#4a90e2"); + localCal.isPublic = true; + localCal.onNetwork = true; + localCal.showReminders = true; + localCal.owner = "network"; + mCalendars.append(localCal); + changed = true; + } + + std::vector messages; + if (rsGxsCalendar->getCalendarContent(meta.mGroupId, messages)) { + if (!messages.empty()) { + uint32_t latestTime = 0; + size_t latestIdx = 0; + for (size_t i = 0; i < messages.size(); ++i) { + if (messages[i].mMeta.mPublishTs > latestTime) { + latestTime = messages[i].mMeta.mPublishTs; + latestIdx = i; + } + } + QString msgIdStr = QString::fromStdString(messages[latestIdx].mMeta.mMsgId.toStdString()); + if (!mLastMsgIds.contains(calId) || mLastMsgIds[calId] != msgIdStr) { + importCalendarFromIcs(calId, QString::fromStdString(messages[latestIdx].mIcsData)); + mLastMsgIds[calId] = msgIdStr; + changed = true; + } + } + } + } + } + + if (changed) { + saveData(); + emit calendarDataChanged(); + } + } +} + +void CalendarData::handleGxsEvent(std::shared_ptr event) { + const RsGxsCalendarEvent *e = dynamic_cast(event.get()); + if (e) { + switch (e->mCalendarEventCode) { + case RsCalendarEventCode::NEW_CALENDAR: + case RsCalendarEventCode::UPDATED_CALENDAR: + syncWithGxs(); + case RsCalendarEventCode::NEW_EVENT: + case RsCalendarEventCode::UPDATED_EVENT: + case RsCalendarEventCode::SUBSCRIBE_STATUS_CHANGED: + syncWithGxs(); + break; + default: + break; + } + } +} diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/msgs/CalendarData.h index 63109d455..51405728f 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.h +++ b/retroshare-gui/src/gui/msgs/CalendarData.h @@ -27,6 +27,9 @@ #include #include #include +#include +#include +#include struct CalendarInfo { QString id; @@ -73,7 +76,8 @@ struct CalendarTask { bool completed; }; -class CalendarData { +class CalendarData : public QObject { + Q_OBJECT public: static CalendarData* instance(); @@ -99,15 +103,33 @@ public: // Helpers static QMap getContacts(); // map PGP ID -> Name + QString exportCalendarToIcs(const QString& calId) const; + void importCalendarFromIcs(const QString& calId, const QString& icsData); + void migrateCalendarData(const QString& oldId, const QString& newId); + void publishCalendarUpdates(const QString& calId); + bool publishCalendar(const QString& oldId, const QString& email, QString& newIdOut); + bool subscribeToCalendar(const QString& id, bool subscribe, const QString& name = ""); + +signals: + void calendarDataChanged(); + +public slots: + void syncWithGxs(); + +private slots: + void handleGxsEvent(std::shared_ptr event); + private: CalendarData(); - ~CalendarData(); + ~CalendarData() override; QList mCalendars; QList mEvents; QList mTasks; + QMap mLastMsgIds; static CalendarData* mInstance; + uint32_t mEventHandlerId; }; #endif // CALENDARDATA_H diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp index 61bf8c376..0c0a24615 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp @@ -34,6 +34,7 @@ #include #include #include +#include CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget* parent) : QDialog(parent), mCalId(calId), mEditMode(!calId.isEmpty()), mSelectedColor(QColor("#4a90e2")) @@ -249,10 +250,42 @@ void CalendarPropertiesDialog::onAccept() { CalendarInfo info = getCalendarInfo(); - if (mEditMode) { - CalendarData::instance()->updateCalendar(info); + if (info.onNetwork && rsGxsCalendar) { + // Extract GXS ID from email + RsGxsId authorId; + int idx = info.email.lastIndexOf('@'); + int endIdx = info.email.lastIndexOf('>'); + if (idx != -1 && endIdx != -1 && endIdx > idx) { + std::string gxsIdStr = info.email.mid(idx + 1, endIdx - idx - 1).toStdString(); + authorId = RsGxsId(gxsIdStr); + } + + std::string errMsg; + if (mEditMode) { + RsGxsGroupId groupId(info.id.toStdString()); + if (rsGxsCalendar->updateCalendar(groupId, info.name.toStdString(), "RetroShare Calendar", authorId, errMsg)) { + CalendarData::instance()->updateCalendar(info); + } else { + QMessageBox::critical(this, tr("GXS Error"), tr("Failed to update network calendar: %1").arg(QString::fromStdString(errMsg))); + return; + } + } else { + RsGxsGroupId groupId; + if (rsGxsCalendar->createCalendar(info.name.toStdString(), "RetroShare Calendar", authorId, groupId, errMsg)) { + info.id = QString::fromStdString(groupId.toStdString()); + rsGxsCalendar->subscribeToCalendar(groupId, true, errMsg); + CalendarData::instance()->addCalendar(info); + } else { + QMessageBox::critical(this, tr("GXS Error"), tr("Failed to create network calendar: %1").arg(QString::fromStdString(errMsg))); + return; + } + } } else { - CalendarData::instance()->addCalendar(info); + if (mEditMode) { + CalendarData::instance()->updateCalendar(info); + } else { + CalendarData::instance()->addCalendar(info); + } } accept(); diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index f91db0649..2ae14086c 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -21,7 +21,10 @@ #include "gui/msgs/CalendarWidget.h" #include "gui/msgs/EventDialog.h" #include "gui/msgs/CalendarPropertiesDialog.h" +#include +#include #include +#include #include #include #include @@ -44,17 +47,28 @@ #include #include #include +#include #include CalendarWidget::CalendarWidget(QWidget* parent) - : QWidget(parent), mSelectedDate(QDate::currentDate()), mCurrentViewMode(2) // Default to Month View + : QWidget(parent), mSelectedDate(QDate::currentDate()), mCurrentViewMode(2), mCalendarListMode(0), mCalendarViewCombo(nullptr), mInitialLoadDone(false) { buildUi(); refreshData(); + + connect(CalendarData::instance(), SIGNAL(calendarDataChanged()), this, SLOT(refreshData())); } CalendarWidget::~CalendarWidget() {} +void CalendarWidget::showEvent(QShowEvent* event) { + QWidget::showEvent(event); + if (!mInitialLoadDone) { + mInitialLoadDone = true; + CalendarData::instance()->syncWithGxs(); + } +} + void CalendarWidget::buildUi() { ui.setupUi(this); @@ -77,6 +91,19 @@ void CalendarWidget::buildUi() { if (btnIndex == -1) btnIndex = 6; ui.topControlLayout->insertWidget(btnIndex, mCwLabel); + // Hide calendarsLabel + ui.calendarsLabel->hide(); + + // Create and insert calendarViewCombo dynamically + mCalendarViewCombo = new QComboBox(this); + mCalendarViewCombo->setObjectName("calendarViewCombo"); + mCalendarViewCombo->addItems({tr("My Calendars"), tr("Shared Calendars")}); + mCalendarViewCombo->setStyleSheet("font-weight: bold; font-size: 13px; margin-bottom: 4px;"); + + int labelIndex = ui.sidebarLayout->indexOf(ui.calendarsLabel); + if (labelIndex == -1) labelIndex = 2; // Default fallback position + ui.sidebarLayout->insertWidget(labelIndex, mCalendarViewCombo); + // Sidebar Calendar configs mSidebarCalendar->setSelectedDate(mSelectedDate); @@ -131,6 +158,7 @@ void CalendarWidget::buildUi() { connect(mCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCalendarSelectionChanged(QListWidgetItem*))); connect(mCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onCalendarContextMenu(const QPoint&))); connect(ui.newCalBtn, SIGNAL(clicked()), this, SLOT(onNewCalendar())); + connect(mCalendarViewCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onCalendarViewModeChanged(int))); // Connect top control signals connect(ui.prevBtn, SIGNAL(clicked()), this, SLOT(onPrevPeriod())); @@ -154,35 +182,63 @@ void CalendarWidget::buildUi() { } void CalendarWidget::refreshData() { - // Save current check states - QMap checkedStates; - for (int i = 0; i < mCalendarList->count(); ++i) { - QListWidgetItem* item = mCalendarList->item(i); - checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); - } - - // Populate Calendar selection list - mCalendarList->blockSignals(true); - mCalendarList->clear(); - const auto& cals = CalendarData::instance()->getCalendars(); - for (const auto& cal : cals) { - QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); - item->setData(Qt::UserRole, cal.id); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - - // Render colored bullet point icon - QPixmap pix(12, 12); - pix.fill(cal.color); - item->setIcon(QIcon(pix)); - - // Restore checked state - if (checkedStates.contains(cal.id)) { - item->setCheckState(checkedStates[cal.id]); - } else { - item->setCheckState(Qt::Checked); + if (mCalendarListMode == 0) { + // Save current check states + QMap checkedStates; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); } + + // Populate Calendar selection list + mCalendarList->blockSignals(true); + mCalendarList->clear(); + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); + item->setData(Qt::UserRole, cal.id); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + // Render colored bullet point icon + QPixmap pix(12, 12); + pix.fill(cal.color); + item->setIcon(QIcon(pix)); + + // Restore checked state + if (checkedStates.contains(cal.id)) { + item->setCheckState(checkedStates[cal.id]); + } else { + item->setCheckState(Qt::Checked); + } + } + mCalendarList->blockSignals(false); + } else { + // Shared Calendars mode + mCalendarList->blockSignals(true); + mCalendarList->clear(); + if (rsGxsCalendar) { + std::list calendars; + if (rsGxsCalendar->getCalendarsSummaries(calendars)) { + for (const auto& meta : calendars) { + QString calId = QString::fromStdString(meta.mGroupId.toStdString()); + QString calName = QString::fromUtf8(meta.mGroupName.c_str()); + bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); + + QListWidgetItem* item = new QListWidgetItem(calName, mCalendarList); + item->setData(Qt::UserRole, calId); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + // Render blue bullet for subscribed, grey for unsubscribed + QPixmap pix(12, 12); + pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); + item->setIcon(QIcon(pix)); + + item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + } + } + } + mCalendarList->blockSignals(false); } - mCalendarList->blockSignals(false); updateViews(); } @@ -524,8 +580,16 @@ void CalendarWidget::onEventSelected(int row, int col) { } } -void CalendarWidget::onCalendarSelectionChanged(QListWidgetItem* /*item*/) { - updateViews(); +void CalendarWidget::onCalendarSelectionChanged(QListWidgetItem* item) { + if (mCalendarListMode == 1) { + if (item) { + QString calId = item->data(Qt::UserRole).toString(); + bool subscribe = (item->checkState() == Qt::Checked); + CalendarData::instance()->subscribeToCalendar(calId, subscribe, item->text()); + } + } else { + updateViews(); + } } void CalendarWidget::onSearchChanged(const QString& text) { @@ -541,6 +605,16 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { QString calName = item->text(); bool isChecked = item->checkState() == Qt::Checked; + if (mCalendarListMode == 1) { + QMenu menu(this); + QAction* subAct = menu.addAction(isChecked ? tr("Unsubscribe") : tr("Subscribe")); + QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); + if (selectedAct == subAct) { + item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); + } + return; + } + QMenu menu(this); QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); @@ -551,7 +625,6 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); menu.addSeparator(); QAction* exportAct = menu.addAction(tr("Export Calendar...")); - QAction* publishAct = menu.addAction(tr("Publish Calendar...")); menu.addSeparator(); QAction* propertiesAct = menu.addAction(tr("Properties")); @@ -586,8 +659,6 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { } } else if (selectedAct == exportAct) { exportCalendar(calId, calName); - } else if (selectedAct == publishAct) { - QMessageBox::information(this, tr("Publish Calendar"), tr("Calendar '%1' published successfully!").arg(calName)); } else if (selectedAct == propertiesAct) { CalendarPropertiesDialog dlg(calId, this); if (dlg.exec() == QDialog::Accepted) { @@ -941,3 +1012,11 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& painter->restore(); } + +void CalendarWidget::onCalendarViewModeChanged(int index) { + mCalendarListMode = index; + if (index == 0) { + CalendarData::instance()->syncWithGxs(); + } + refreshData(); +} diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h index c80d48d0f..0e5334017 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -31,6 +31,7 @@ class QListWidgetItem; class QLabel; +class QComboBox; class CalendarWidget; class MonthCalendarDelegate : public QStyledItemDelegate { @@ -64,6 +65,7 @@ private slots: void onSearchChanged(const QString& text); void onCalendarContextMenu(const QPoint& pos); void onMonthCellClicked(int row, int col); + void onCalendarViewModeChanged(int index); private: void buildUi(); @@ -78,10 +80,16 @@ private: QDate mSelectedDate; int mCurrentViewMode; // 0=Day, 1=Week, 2=Month QString mSearchText; + int mCalendarListMode; // 0=My Calendars, 1=Shared Calendars + bool mInitialLoadDone; + +protected: + void showEvent(QShowEvent* event) override; // UI elements (now loaded from UI file but kept as pointers for compatibility) QCalendarWidget* mSidebarCalendar; QListWidget* mCalendarList; + QComboBox* mCalendarViewCombo; QLabel* mPeriodLabel; QLabel* mCwLabel; diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/msgs/TasksWidget.cpp index 0335488f9..c215d43b2 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.cpp +++ b/retroshare-gui/src/gui/msgs/TasksWidget.cpp @@ -21,11 +21,18 @@ #include "gui/msgs/TasksWidget.h" #include "gui/msgs/TaskDialog.h" #include "gui/msgs/CalendarPropertiesDialog.h" +#include +#include #include #include +#include #include #include #include +#include +#include +#include +#include #include #include #include @@ -37,17 +44,28 @@ #include #include #include +#include #include TasksWidget::TasksWidget(QWidget* parent) - : QWidget(parent), mCurrentFilterMode(0) + : QWidget(parent), mCurrentFilterMode(0), mCalendarListMode(0), mCalendarViewCombo(nullptr), mInitialLoadDone(false) { buildUi(); refreshData(); + + connect(CalendarData::instance(), SIGNAL(calendarDataChanged()), this, SLOT(refreshData())); } TasksWidget::~TasksWidget() {} +void TasksWidget::showEvent(QShowEvent* event) { + QWidget::showEvent(event); + if (!mInitialLoadDone) { + mInitialLoadDone = true; + CalendarData::instance()->syncWithGxs(); + } +} + void TasksWidget::buildUi() { ui.setupUi(this); @@ -81,6 +99,19 @@ void TasksWidget::buildUi() { mTaskTable->setSelectionMode(QAbstractItemView::SingleSelection); mTaskTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + // Hide calendarsLabel + ui.calendarsLabel->hide(); + + // Create and insert calendarViewCombo dynamically + mCalendarViewCombo = new QComboBox(this); + mCalendarViewCombo->setObjectName("calendarViewCombo"); + mCalendarViewCombo->addItems({tr("My Calendars"), tr("Shared Calendars")}); + mCalendarViewCombo->setStyleSheet("font-weight: bold; font-size: 13px; margin-bottom: 4px;"); + + int labelIndex = ui.sidebarLayout->indexOf(ui.calendarsLabel); + if (labelIndex == -1) labelIndex = 4; // Default fallback position + ui.sidebarLayout->insertWidget(labelIndex, mCalendarViewCombo); + // Splitter configuration ui.splitter->setStretchFactor(0, 0); ui.splitter->setStretchFactor(1, 1); @@ -94,37 +125,66 @@ void TasksWidget::buildUi() { connect(mSearchEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onSearchChanged(const QString&))); connect(mTaskTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onTaskDoubleClicked(int,int))); connect(mTaskTable, SIGNAL(cellClicked(int,int)), this, SLOT(onTaskClicked(int,int))); + connect(mCalendarViewCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onCalendarViewModeChanged(int))); } void TasksWidget::refreshData() { - // Save current check states - QMap checkedStates; - for (int i = 0; i < mCalendarList->count(); ++i) { - QListWidgetItem* item = mCalendarList->item(i); - checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); - } - - // Populate Calendar selection list - mCalendarList->blockSignals(true); - mCalendarList->clear(); - const auto& cals = CalendarData::instance()->getCalendars(); - for (const auto& cal : cals) { - QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); - item->setData(Qt::UserRole, cal.id); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - - QPixmap pix(12, 12); - pix.fill(cal.color); - item->setIcon(QIcon(pix)); - - // Restore checked state - if (checkedStates.contains(cal.id)) { - item->setCheckState(checkedStates[cal.id]); - } else { - item->setCheckState(Qt::Checked); + if (mCalendarListMode == 0) { + // Save current check states + QMap checkedStates; + for (int i = 0; i < mCalendarList->count(); ++i) { + QListWidgetItem* item = mCalendarList->item(i); + checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); } + + // Populate Calendar selection list + mCalendarList->blockSignals(true); + mCalendarList->clear(); + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& cal : cals) { + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); + item->setData(Qt::UserRole, cal.id); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + QPixmap pix(12, 12); + pix.fill(cal.color); + item->setIcon(QIcon(pix)); + + // Restore checked state + if (checkedStates.contains(cal.id)) { + item->setCheckState(checkedStates[cal.id]); + } else { + item->setCheckState(Qt::Checked); + } + } + mCalendarList->blockSignals(false); + } else { + // Shared Calendars mode + mCalendarList->blockSignals(true); + mCalendarList->clear(); + if (rsGxsCalendar) { + std::list calendars; + if (rsGxsCalendar->getCalendarsSummaries(calendars)) { + for (const auto& meta : calendars) { + QString calId = QString::fromStdString(meta.mGroupId.toStdString()); + QString calName = QString::fromUtf8(meta.mGroupName.c_str()); + bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); + + QListWidgetItem* item = new QListWidgetItem(calName, mCalendarList); + item->setData(Qt::UserRole, calId); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + // Render blue bullet for subscribed, grey for unsubscribed + QPixmap pix(12, 12); + pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); + item->setIcon(QIcon(pix)); + + item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + } + } + } + mCalendarList->blockSignals(false); } - mCalendarList->blockSignals(false); updateTaskList(); } @@ -284,8 +344,16 @@ void TasksWidget::onFilterSelected(QListWidgetItem* item) { } } -void TasksWidget::onCalendarSelectionChanged(QListWidgetItem* /*item*/) { - updateTaskList(); +void TasksWidget::onCalendarSelectionChanged(QListWidgetItem* item) { + if (mCalendarListMode == 1) { + if (item) { + QString calId = item->data(Qt::UserRole).toString(); + bool subscribe = (item->checkState() == Qt::Checked); + CalendarData::instance()->subscribeToCalendar(calId, subscribe, item->text()); + } + } else { + updateTaskList(); + } } void TasksWidget::onSearchChanged(const QString& text) { @@ -301,6 +369,16 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { QString calName = item->text(); bool isChecked = item->checkState() == Qt::Checked; + if (mCalendarListMode == 1) { + QMenu menu(this); + QAction* subAct = menu.addAction(isChecked ? tr("Unsubscribe") : tr("Subscribe")); + QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); + if (selectedAct == subAct) { + item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); + } + return; + } + QMenu menu(this); QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); @@ -311,7 +389,6 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); menu.addSeparator(); QAction* exportAct = menu.addAction(tr("Export Calendar...")); - QAction* publishAct = menu.addAction(tr("Publish Calendar...")); menu.addSeparator(); QAction* propertiesAct = menu.addAction(tr("Properties")); @@ -348,9 +425,7 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { refreshData(); } } else if (selectedAct == exportAct) { - QMessageBox::information(this, tr("Export Calendar"), tr("Calendar '%1' exported successfully!").arg(calName)); - } else if (selectedAct == publishAct) { - QMessageBox::information(this, tr("Publish Calendar"), tr("Calendar '%1' published successfully!").arg(calName)); + exportCalendar(calId, calName); } else if (selectedAct == propertiesAct) { CalendarPropertiesDialog dlg(calId, this); if (dlg.exec() == QDialog::Accepted) { @@ -358,3 +433,49 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { } } } + +void TasksWidget::exportCalendar(const QString& calId, const QString& calName) { + QString icsContent = CalendarData::instance()->exportCalendarToIcs(calId); + + QString defaultFileName = QString("%1.ics").arg(calName); + defaultFileName.replace(QRegExp("[\\\\/:*?\"<>|]"), "_"); + + QString selectedFilter; + QString filePath = QFileDialog::getSaveFileName( + this, + tr("Export Calendar"), + defaultFileName, + tr("iCalendar files (*.ics);;All Files (*)"), + &selectedFilter + ); + + if (!filePath.isEmpty()) { + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical( + this, + tr("Export Error"), + tr("Could not open file %1 for writing.").arg(filePath) + ); + } else { + QTextStream out(&file); + out.setCodec("UTF-8"); + out << icsContent; + file.close(); + + QMessageBox::information( + this, + tr("Export Calendar"), + tr("Calendar '%1' exported successfully to %2!").arg(calName).arg(QDir::toNativeSeparators(filePath)) + ); + } + } +} + +void TasksWidget::onCalendarViewModeChanged(int index) { + mCalendarListMode = index; + if (index == 0) { + CalendarData::instance()->syncWithGxs(); + } + refreshData(); +} diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.h b/retroshare-gui/src/gui/msgs/TasksWidget.h index 6ae25abbf..1817324ee 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.h +++ b/retroshare-gui/src/gui/msgs/TasksWidget.h @@ -27,6 +27,7 @@ #include "ui_TasksWidget.h" class QListWidgetItem; +class QComboBox; class TasksWidget : public QWidget { Q_OBJECT @@ -45,18 +46,26 @@ private slots: void onCalendarSelectionChanged(QListWidgetItem* item); void onSearchChanged(const QString& text); void onCalendarContextMenu(const QPoint& pos); + void onCalendarViewModeChanged(int index); private: void buildUi(); void updateTaskList(); + void exportCalendar(const QString& calId, const QString& calName); int mCurrentFilterMode; // 0=All, 1=Active, 2=Completed, 3=Overdue QString mSearchText; + int mCalendarListMode; // 0=My Calendars, 1=Shared Calendars + bool mInitialLoadDone; + +protected: + void showEvent(QShowEvent* event) override; // UI elements (loaded from UI file, kept as pointers for compatibility) QCalendarWidget* mSidebarCalendar; QListWidget* mFilterList; QListWidget* mCalendarList; + QComboBox* mCalendarViewCombo; QLineEdit* mQuickTaskEdit; QLineEdit* mSearchEdit; From f6a0a6ccf1811f24b7606b1d3358ee87a4adfc6b Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:34:11 +0200 Subject: [PATCH 17/26] changed eventType to use the dynamic way --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index 3603ab89d..f3eaf74ab 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -40,11 +40,13 @@ CalendarData::CalendarData() : QObject(), mEventHandlerId(0) { loadData(); if (rsEvents && rsGxsCalendar) { + RsEventType calendarEventType = (RsEventType)rsEvents->getDynamicEventType("GXS_CALENDAR"); + rsEvents->registerEventsHandler( [this](std::shared_ptr event) { RsQThreadUtils::postToObject([=]() { handleGxsEvent(event); }, this); }, - mEventHandlerId, RsEventType::GXS_CALENDAR + mEventHandlerId, calendarEventType ); } } From c9c8d31342530198e362ce1c1d7626ecd2038e4b Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:13:43 +0200 Subject: [PATCH 18/26] Added own list for shared calendars *rename function --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 8 +- retroshare-gui/src/gui/msgs/CalendarData.h | 2 +- .../src/gui/msgs/CalendarWidget.cpp | 267 ++++++++++++++---- retroshare-gui/src/gui/msgs/CalendarWidget.h | 6 +- retroshare-gui/src/gui/msgs/CalendarWidget.ui | 35 ++- retroshare-gui/src/gui/msgs/TasksWidget.cpp | 158 +++++++---- retroshare-gui/src/gui/msgs/TasksWidget.h | 5 +- retroshare-gui/src/gui/msgs/TasksWidget.ui | 19 +- 8 files changed, 370 insertions(+), 130 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index f3eaf74ab..6d1b9794f 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -716,7 +716,7 @@ bool CalendarData::subscribeToCalendar(const QString& id, bool subscribe, const } emit calendarDataChanged(); // Trigger sync to fetch contents - syncWithGxs(); + updateCalendars(); } else { // Remove from local list for (int i = 0; i < mCalendars.size(); ++i) { @@ -737,7 +737,7 @@ bool CalendarData::subscribeToCalendar(const QString& id, bool subscribe, const return true; } -void CalendarData::syncWithGxs() { +void CalendarData::updateCalendars() { if (!rsGxsCalendar) return; std::list calendars; @@ -805,11 +805,11 @@ void CalendarData::handleGxsEvent(std::shared_ptr event) { switch (e->mCalendarEventCode) { case RsCalendarEventCode::NEW_CALENDAR: case RsCalendarEventCode::UPDATED_CALENDAR: - syncWithGxs(); + updateCalendars(); case RsCalendarEventCode::NEW_EVENT: case RsCalendarEventCode::UPDATED_EVENT: case RsCalendarEventCode::SUBSCRIBE_STATUS_CHANGED: - syncWithGxs(); + updateCalendars(); break; default: break; diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/msgs/CalendarData.h index 51405728f..dcf48cd65 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.h +++ b/retroshare-gui/src/gui/msgs/CalendarData.h @@ -114,7 +114,7 @@ signals: void calendarDataChanged(); public slots: - void syncWithGxs(); + void updateCalendars(); private slots: void handleGxsEvent(std::shared_ptr event); diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index 2ae14086c..03b7a7d73 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -23,6 +23,7 @@ #include "gui/msgs/CalendarPropertiesDialog.h" #include #include +#include "retroshare/rsgxsflags.h" #include #include #include @@ -51,7 +52,7 @@ #include CalendarWidget::CalendarWidget(QWidget* parent) - : QWidget(parent), mSelectedDate(QDate::currentDate()), mCurrentViewMode(2), mCalendarListMode(0), mCalendarViewCombo(nullptr), mInitialLoadDone(false) + : QWidget(parent), mSelectedDate(QDate::currentDate()), mCurrentViewMode(2), mInitialLoadDone(false) { buildUi(); refreshData(); @@ -65,7 +66,7 @@ void CalendarWidget::showEvent(QShowEvent* event) { QWidget::showEvent(event); if (!mInitialLoadDone) { mInitialLoadDone = true; - CalendarData::instance()->syncWithGxs(); + CalendarData::instance()->updateCalendars(); } } @@ -75,6 +76,7 @@ void CalendarWidget::buildUi() { // Initialize UI pointers mSidebarCalendar = ui.sidebarCalendar; mCalendarList = ui.calendarList; + mSharedCalendarList = ui.sharedCalendarList; mPeriodLabel = ui.periodLabel; mSearchEdit = ui.searchEdit; mEventTable = ui.eventTable; @@ -91,19 +93,6 @@ void CalendarWidget::buildUi() { if (btnIndex == -1) btnIndex = 6; ui.topControlLayout->insertWidget(btnIndex, mCwLabel); - // Hide calendarsLabel - ui.calendarsLabel->hide(); - - // Create and insert calendarViewCombo dynamically - mCalendarViewCombo = new QComboBox(this); - mCalendarViewCombo->setObjectName("calendarViewCombo"); - mCalendarViewCombo->addItems({tr("My Calendars"), tr("Shared Calendars")}); - mCalendarViewCombo->setStyleSheet("font-weight: bold; font-size: 13px; margin-bottom: 4px;"); - - int labelIndex = ui.sidebarLayout->indexOf(ui.calendarsLabel); - if (labelIndex == -1) labelIndex = 2; // Default fallback position - ui.sidebarLayout->insertWidget(labelIndex, mCalendarViewCombo); - // Sidebar Calendar configs mSidebarCalendar->setSelectedDate(mSelectedDate); @@ -116,6 +105,8 @@ void CalendarWidget::buildUi() { mEventTable->setSelectionMode(QAbstractItemView::SingleSelection); mEventTable->setEditTriggers(QAbstractItemView::NoEditTriggers); connect(mEventTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); + mEventTable->setContextMenuPolicy(Qt::CustomContextMenu); + connect(mEventTable, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onEventTableContextMenu(const QPoint&))); // Stacked widget pages setup // 1. Day Table @@ -157,8 +148,9 @@ void CalendarWidget::buildUi() { connect(mSidebarCalendar, SIGNAL(clicked(const QDate&)), this, SLOT(onDateSelected(const QDate&))); connect(mCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCalendarSelectionChanged(QListWidgetItem*))); connect(mCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onCalendarContextMenu(const QPoint&))); + connect(mSharedCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onSharedCalendarSelectionChanged(QListWidgetItem*))); + connect(mSharedCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onSharedCalendarContextMenu(const QPoint&))); connect(ui.newCalBtn, SIGNAL(clicked()), this, SLOT(onNewCalendar())); - connect(mCalendarViewCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onCalendarViewModeChanged(int))); // Connect top control signals connect(ui.prevBtn, SIGNAL(clicked()), this, SLOT(onPrevPeriod())); @@ -182,7 +174,10 @@ void CalendarWidget::buildUi() { } void CalendarWidget::refreshData() { - if (mCalendarListMode == 0) { + const auto& cals = CalendarData::instance()->getCalendars(); + + // 1. Populate My Calendars (owned by us, i.e. owner == "local") + { // Save current check states QMap checkedStates; for (int i = 0; i < mCalendarList->count(); ++i) { @@ -190,11 +185,11 @@ void CalendarWidget::refreshData() { checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); } - // Populate Calendar selection list mCalendarList->blockSignals(true); mCalendarList->clear(); - const auto& cals = CalendarData::instance()->getCalendars(); for (const auto& cal : cals) { + if (cal.owner != "local") continue; + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); item->setData(Qt::UserRole, cal.id); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); @@ -212,19 +207,39 @@ void CalendarWidget::refreshData() { } } mCalendarList->blockSignals(false); - } else { - // Shared Calendars mode - mCalendarList->blockSignals(true); - mCalendarList->clear(); + } + + // 2. Populate Shared Calendars (not owned by us) + { + // Save current check states + QMap sharedCheckedStates; + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + sharedCheckedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + } + + mSharedCalendarList->blockSignals(true); + mSharedCalendarList->clear(); if (rsGxsCalendar) { std::list calendars; if (rsGxsCalendar->getCalendarsSummaries(calendars)) { for (const auto& meta : calendars) { QString calId = QString::fromStdString(meta.mGroupId.toStdString()); + + // Filter out calendars owned by us + bool ownedByUs = false; + for (const auto& c : cals) { + if (c.id == calId && c.owner == "local") { + ownedByUs = true; + break; + } + } + if (ownedByUs) continue; + QString calName = QString::fromUtf8(meta.mGroupName.c_str()); bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); - QListWidgetItem* item = new QListWidgetItem(calName, mCalendarList); + QListWidgetItem* item = new QListWidgetItem(calName, mSharedCalendarList); item->setData(Qt::UserRole, calId); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); @@ -233,11 +248,17 @@ void CalendarWidget::refreshData() { pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); item->setIcon(QIcon(pix)); - item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + // Restore checked state if we have a saved state, + // otherwise default to Checked if subscribed, Unchecked if unsubscribed + if (sharedCheckedStates.contains(calId)) { + item->setCheckState(sharedCheckedStates[calId]); + } else { + item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + } } } } - mCalendarList->blockSignals(false); + mSharedCalendarList->blockSignals(false); } updateViews(); @@ -289,6 +310,7 @@ void CalendarWidget::updateViews() { } else { updateMonthView(); } + } void CalendarWidget::updateEventList() { @@ -305,6 +327,12 @@ void CalendarWidget::updateEventList() { enabledCalIds.append(item->data(Qt::UserRole).toString()); } } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + if (item->checkState() == Qt::Checked) { + enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + } int row = 0; for (const auto& ev : events) { @@ -351,6 +379,10 @@ void CalendarWidget::updateDayView() { QListWidgetItem* item = mCalendarList->item(i); if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } for (int hour = 0; hour < 24; ++hour) { QString timeText = QString("%1:00").arg(hour, 2, 10, QChar('0')); @@ -397,6 +429,10 @@ void CalendarWidget::updateWeekView() { QListWidgetItem* item = mCalendarList->item(i); if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } // Populate week cells for (int dayIdx = 0; dayIdx < 7; ++dayIdx) { @@ -437,6 +473,10 @@ void CalendarWidget::updateMonthView() { QListWidgetItem* item = mCalendarList->item(i); if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); + } for (int row = 0; row < rowsNeeded; ++row) { for (int col = 0; col < 7; ++col) { @@ -555,6 +595,40 @@ void CalendarWidget::onEventSelected(int row, int col) { // If double clicked a cell/row containing an event, edit it. Otherwise create a new one. if (!eventId.isEmpty()) { + // Check if user can edit this event (admin check for shared calendars) + QString calendarId; + const auto& events = CalendarData::instance()->getEvents(); + for (const auto& ev : events) { + if (ev.id == eventId) { + calendarId = ev.calendarId; + break; + } + } + + bool canEdit = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calendarId) { + if (!c.onNetwork) { + canEdit = true; + } else if (rsGxsCalendar) { + std::list summaries; + if (rsGxsCalendar->getCalendarsSummaries(summaries)) { + RsGxsGroupId groupId(calendarId.toStdString()); + for (const auto& meta : summaries) { + if (meta.mGroupId == groupId) { + canEdit = IS_GROUP_ADMIN(meta.mSubscribeFlags); + break; + } + } + } + } + break; + } + } + + if (!canEdit) return; + EventDialog dlg(eventId, QDateTime::currentDateTime(), this); if (dlg.exec() == QDialog::Accepted) { refreshData(); @@ -581,15 +655,11 @@ void CalendarWidget::onEventSelected(int row, int col) { } void CalendarWidget::onCalendarSelectionChanged(QListWidgetItem* item) { - if (mCalendarListMode == 1) { - if (item) { - QString calId = item->data(Qt::UserRole).toString(); - bool subscribe = (item->checkState() == Qt::Checked); - CalendarData::instance()->subscribeToCalendar(calId, subscribe, item->text()); - } - } else { - updateViews(); - } + updateViews(); +} + +void CalendarWidget::onSharedCalendarSelectionChanged(QListWidgetItem* item) { + updateViews(); } void CalendarWidget::onSearchChanged(const QString& text) { @@ -605,24 +675,29 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { QString calName = item->text(); bool isChecked = item->checkState() == Qt::Checked; - if (mCalendarListMode == 1) { - QMenu menu(this); - QAction* subAct = menu.addAction(isChecked ? tr("Unsubscribe") : tr("Subscribe")); - QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); - if (selectedAct == subAct) { - item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); - } - return; - } - QMenu menu(this); QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); QAction* showOnlyAct = menu.addAction(tr("Show Only %1").arg(calName)); QAction* showAllAct = menu.addAction(tr("Show All Calendars")); - menu.addSeparator(); - QAction* newAct = menu.addAction(tr("New Calendar...")); - QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); + + // Check if selected calendar is a shared/network calendar + bool isSharedCal = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calId) { + isSharedCal = (c.owner != "local"); + break; + } + } + + QAction* newAct = nullptr; + QAction* deleteAct = nullptr; + if (!isSharedCal) { + menu.addSeparator(); + newAct = menu.addAction(tr("New Calendar...")); + deleteAct = menu.addAction(tr("Delete Calendar...")); + } menu.addSeparator(); QAction* exportAct = menu.addAction(tr("Export Calendar...")); menu.addSeparator(); @@ -635,11 +710,16 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); } else if (selectedAct == showOnlyAct) { mCalendarList->blockSignals(true); + mSharedCalendarList->blockSignals(true); for (int i = 0; i < mCalendarList->count(); ++i) { QListWidgetItem* it = mCalendarList->item(i); it->setCheckState(it == item ? Qt::Checked : Qt::Unchecked); } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + mSharedCalendarList->item(i)->setCheckState(Qt::Unchecked); + } mCalendarList->blockSignals(false); + mSharedCalendarList->blockSignals(false); updateViews(); } else if (selectedAct == showAllAct) { mCalendarList->blockSignals(true); @@ -667,6 +747,90 @@ void CalendarWidget::onCalendarContextMenu(const QPoint& pos) { } } +void CalendarWidget::onSharedCalendarContextMenu(const QPoint& pos) { + QListWidgetItem* item = mSharedCalendarList->itemAt(pos); + if (!item) return; + + QString calId = item->data(Qt::UserRole).toString(); + QString calName = item->text(); + + bool isSubscribed = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calId) { + isSubscribed = true; + break; + } + } + + QMenu menu(this); + QAction* subAct = menu.addAction(isSubscribed ? tr("Unsubscribe") : tr("Subscribe")); + QAction* selectedAct = menu.exec(mSharedCalendarList->mapToGlobal(pos)); + if (selectedAct == subAct) { + CalendarData::instance()->subscribeToCalendar(calId, !isSubscribed, calName); + } +} + +void CalendarWidget::onEventTableContextMenu(const QPoint& pos) { + QTableWidgetItem* titleItem = mEventTable->itemAt(pos); + if (!titleItem) return; + + int row = titleItem->row(); + QTableWidgetItem* firstColItem = mEventTable->item(row, 0); + if (!firstColItem) return; + + QString eventId = firstColItem->data(Qt::UserRole).toString(); + if (eventId.isEmpty()) return; + + // Find the event and its calendar + QString calendarId; + const auto& events = CalendarData::instance()->getEvents(); + for (const auto& ev : events) { + if (ev.id == eventId) { + calendarId = ev.calendarId; + break; + } + } + if (calendarId.isEmpty()) return; + + // Determine if user can edit: local calendars are always editable, + // network calendars require admin (owner) status on the GXS group. + bool canEdit = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calendarId) { + if (!c.onNetwork) { + canEdit = true; // Local calendar — always editable + } else if (rsGxsCalendar) { + // Check GXS admin flag + std::list summaries; + if (rsGxsCalendar->getCalendarsSummaries(summaries)) { + RsGxsGroupId groupId(calendarId.toStdString()); + for (const auto& meta : summaries) { + if (meta.mGroupId == groupId) { + canEdit = IS_GROUP_ADMIN(meta.mSubscribeFlags); + break; + } + } + } + } + break; + } + } + + QMenu menu(this); + QAction* editAct = menu.addAction(tr("Edit Event")); + editAct->setEnabled(canEdit); + + QAction* selectedAct = menu.exec(mEventTable->viewport()->mapToGlobal(pos)); + if (selectedAct == editAct && canEdit) { + EventDialog dlg(eventId, QDateTime::currentDateTime(), this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } +} + void CalendarWidget::exportCalendar(const QString& calId, const QString& calName) { QString icsContent = "BEGIN:VCALENDAR\r\n" "VERSION:2.0\r\n" @@ -1013,10 +1177,3 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& painter->restore(); } -void CalendarWidget::onCalendarViewModeChanged(int index) { - mCalendarListMode = index; - if (index == 0) { - CalendarData::instance()->syncWithGxs(); - } - refreshData(); -} diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h index 0e5334017..326d2ae62 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -62,10 +62,12 @@ private slots: void onDateSelected(const QDate& date); void onEventSelected(int row, int col); void onCalendarSelectionChanged(QListWidgetItem* item); + void onSharedCalendarSelectionChanged(QListWidgetItem* item); void onSearchChanged(const QString& text); void onCalendarContextMenu(const QPoint& pos); + void onSharedCalendarContextMenu(const QPoint& pos); + void onEventTableContextMenu(const QPoint& pos); void onMonthCellClicked(int row, int col); - void onCalendarViewModeChanged(int index); private: void buildUi(); @@ -89,7 +91,7 @@ protected: // UI elements (now loaded from UI file but kept as pointers for compatibility) QCalendarWidget* mSidebarCalendar; QListWidget* mCalendarList; - QComboBox* mCalendarViewCombo; + QListWidget* mSharedCalendarList; QLabel* mPeriodLabel; QLabel* mCwLabel; diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.ui b/retroshare-gui/src/gui/msgs/CalendarWidget.ui index d99c3d9c7..053da1b99 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.ui +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.ui @@ -6,7 +6,7 @@ 0 0 - 800 + 833 600 @@ -80,7 +80,7 @@ font-weight: bold; font-size: 14px; - Calendars + My Calendars @@ -91,6 +91,23 @@ + + + + font-weight: bold; font-size: 14px; margin-top: 10px; + + + Shared Calendars + + + + + + + Qt::CustomContextMenu + + + @@ -199,17 +216,17 @@ - Qt::Orientation::Vertical + Qt::Vertical - QAbstractItemView::EditTrigger::NoEditTriggers + QAbstractItemView::NoEditTriggers - QAbstractItemView::SelectionMode::SingleSelection + QAbstractItemView::NoSelection - QAbstractItemView::SelectionBehavior::SelectRows + QAbstractItemView::SelectItems @@ -233,7 +250,7 @@ - QAbstractItemView::EditTrigger::NoEditTriggers + QAbstractItemView::NoEditTriggers @@ -259,7 +276,7 @@ - QAbstractItemView::EditTrigger::NoEditTriggers + QAbstractItemView::NoEditTriggers @@ -285,7 +302,7 @@ - QAbstractItemView::EditTrigger::NoEditTriggers + QAbstractItemView::NoEditTriggers diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/msgs/TasksWidget.cpp index c215d43b2..82ff328ad 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.cpp +++ b/retroshare-gui/src/gui/msgs/TasksWidget.cpp @@ -48,7 +48,7 @@ #include TasksWidget::TasksWidget(QWidget* parent) - : QWidget(parent), mCurrentFilterMode(0), mCalendarListMode(0), mCalendarViewCombo(nullptr), mInitialLoadDone(false) + : QWidget(parent), mCurrentFilterMode(0), mInitialLoadDone(false) { buildUi(); refreshData(); @@ -62,7 +62,7 @@ void TasksWidget::showEvent(QShowEvent* event) { QWidget::showEvent(event); if (!mInitialLoadDone) { mInitialLoadDone = true; - CalendarData::instance()->syncWithGxs(); + CalendarData::instance()->updateCalendars(); } } @@ -73,6 +73,7 @@ void TasksWidget::buildUi() { mSidebarCalendar = ui.sidebarCalendar; mFilterList = ui.filterList; mCalendarList = ui.calendarList; + mSharedCalendarList = ui.sharedCalendarList; mQuickTaskEdit = ui.quickTaskEdit; mSearchEdit = ui.searchEdit; mTaskTable = ui.taskTable; @@ -99,19 +100,6 @@ void TasksWidget::buildUi() { mTaskTable->setSelectionMode(QAbstractItemView::SingleSelection); mTaskTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - // Hide calendarsLabel - ui.calendarsLabel->hide(); - - // Create and insert calendarViewCombo dynamically - mCalendarViewCombo = new QComboBox(this); - mCalendarViewCombo->setObjectName("calendarViewCombo"); - mCalendarViewCombo->addItems({tr("My Calendars"), tr("Shared Calendars")}); - mCalendarViewCombo->setStyleSheet("font-weight: bold; font-size: 13px; margin-bottom: 4px;"); - - int labelIndex = ui.sidebarLayout->indexOf(ui.calendarsLabel); - if (labelIndex == -1) labelIndex = 4; // Default fallback position - ui.sidebarLayout->insertWidget(labelIndex, mCalendarViewCombo); - // Splitter configuration ui.splitter->setStretchFactor(0, 0); ui.splitter->setStretchFactor(1, 1); @@ -121,15 +109,19 @@ void TasksWidget::buildUi() { connect(mFilterList, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(onFilterSelected(QListWidgetItem*))); connect(mCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onCalendarSelectionChanged(QListWidgetItem*))); connect(mCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onCalendarContextMenu(const QPoint&))); + connect(mSharedCalendarList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(onSharedCalendarSelectionChanged(QListWidgetItem*))); + connect(mSharedCalendarList, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onSharedCalendarContextMenu(const QPoint&))); connect(mQuickTaskEdit, SIGNAL(returnPressed()), this, SLOT(onQuickTaskAdded())); connect(mSearchEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onSearchChanged(const QString&))); connect(mTaskTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onTaskDoubleClicked(int,int))); connect(mTaskTable, SIGNAL(cellClicked(int,int)), this, SLOT(onTaskClicked(int,int))); - connect(mCalendarViewCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onCalendarViewModeChanged(int))); } void TasksWidget::refreshData() { - if (mCalendarListMode == 0) { + const auto& cals = CalendarData::instance()->getCalendars(); + + // 1. Populate My Calendars (owned by us, i.e. owner == "local") + { // Save current check states QMap checkedStates; for (int i = 0; i < mCalendarList->count(); ++i) { @@ -137,11 +129,11 @@ void TasksWidget::refreshData() { checkedStates[item->data(Qt::UserRole).toString()] = item->checkState(); } - // Populate Calendar selection list mCalendarList->blockSignals(true); mCalendarList->clear(); - const auto& cals = CalendarData::instance()->getCalendars(); for (const auto& cal : cals) { + if (cal.owner != "local") continue; + QListWidgetItem* item = new QListWidgetItem(cal.name, mCalendarList); item->setData(Qt::UserRole, cal.id); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); @@ -158,19 +150,39 @@ void TasksWidget::refreshData() { } } mCalendarList->blockSignals(false); - } else { - // Shared Calendars mode - mCalendarList->blockSignals(true); - mCalendarList->clear(); + } + + // 2. Populate Shared Calendars (not owned by us) + { + // Save current check states + QMap sharedCheckedStates; + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + sharedCheckedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + } + + mSharedCalendarList->blockSignals(true); + mSharedCalendarList->clear(); if (rsGxsCalendar) { std::list calendars; if (rsGxsCalendar->getCalendarsSummaries(calendars)) { for (const auto& meta : calendars) { QString calId = QString::fromStdString(meta.mGroupId.toStdString()); + + // Filter out calendars owned by us + bool ownedByUs = false; + for (const auto& c : cals) { + if (c.id == calId && c.owner == "local") { + ownedByUs = true; + break; + } + } + if (ownedByUs) continue; + QString calName = QString::fromUtf8(meta.mGroupName.c_str()); bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); - QListWidgetItem* item = new QListWidgetItem(calName, mCalendarList); + QListWidgetItem* item = new QListWidgetItem(calName, mSharedCalendarList); item->setData(Qt::UserRole, calId); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); @@ -179,11 +191,17 @@ void TasksWidget::refreshData() { pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); item->setIcon(QIcon(pix)); - item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + // Restore checked state if we have a saved state, + // otherwise default to Checked if subscribed, Unchecked if unsubscribed + if (sharedCheckedStates.contains(calId)) { + item->setCheckState(sharedCheckedStates[calId]); + } else { + item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + } } } } - mCalendarList->blockSignals(false); + mSharedCalendarList->blockSignals(false); } updateTaskList(); @@ -202,6 +220,12 @@ void TasksWidget::updateTaskList() { enabledCalIds.append(item->data(Qt::UserRole).toString()); } } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + QListWidgetItem* item = mSharedCalendarList->item(i); + if (item->checkState() == Qt::Checked) { + enabledCalIds.append(item->data(Qt::UserRole).toString()); + } + } int row = 0; QDateTime now = QDateTime::currentDateTime(); @@ -345,15 +369,11 @@ void TasksWidget::onFilterSelected(QListWidgetItem* item) { } void TasksWidget::onCalendarSelectionChanged(QListWidgetItem* item) { - if (mCalendarListMode == 1) { - if (item) { - QString calId = item->data(Qt::UserRole).toString(); - bool subscribe = (item->checkState() == Qt::Checked); - CalendarData::instance()->subscribeToCalendar(calId, subscribe, item->text()); - } - } else { - updateTaskList(); - } + updateTaskList(); +} + +void TasksWidget::onSharedCalendarSelectionChanged(QListWidgetItem* item) { + updateTaskList(); } void TasksWidget::onSearchChanged(const QString& text) { @@ -369,24 +389,29 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { QString calName = item->text(); bool isChecked = item->checkState() == Qt::Checked; - if (mCalendarListMode == 1) { - QMenu menu(this); - QAction* subAct = menu.addAction(isChecked ? tr("Unsubscribe") : tr("Subscribe")); - QAction* selectedAct = menu.exec(mCalendarList->mapToGlobal(pos)); - if (selectedAct == subAct) { - item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); - } - return; - } - QMenu menu(this); QAction* toggleAct = menu.addAction(isChecked ? tr("Hide %1").arg(calName) : tr("Show %1").arg(calName)); QAction* showOnlyAct = menu.addAction(tr("Show Only %1").arg(calName)); QAction* showAllAct = menu.addAction(tr("Show All Calendars")); - menu.addSeparator(); - QAction* newAct = menu.addAction(tr("New Calendar...")); - QAction* deleteAct = menu.addAction(tr("Delete Calendar...")); + + // Check if selected calendar is a shared/network calendar + bool isSharedCal = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calId) { + isSharedCal = (c.owner != "local"); + break; + } + } + + QAction* newAct = nullptr; + QAction* deleteAct = nullptr; + if (!isSharedCal) { + menu.addSeparator(); + newAct = menu.addAction(tr("New Calendar...")); + deleteAct = menu.addAction(tr("Delete Calendar...")); + } menu.addSeparator(); QAction* exportAct = menu.addAction(tr("Export Calendar...")); menu.addSeparator(); @@ -399,11 +424,16 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { item->setCheckState(isChecked ? Qt::Unchecked : Qt::Checked); } else if (selectedAct == showOnlyAct) { mCalendarList->blockSignals(true); + mSharedCalendarList->blockSignals(true); for (int i = 0; i < mCalendarList->count(); ++i) { QListWidgetItem* it = mCalendarList->item(i); it->setCheckState(it == item ? Qt::Checked : Qt::Unchecked); } + for (int i = 0; i < mSharedCalendarList->count(); ++i) { + mSharedCalendarList->item(i)->setCheckState(Qt::Unchecked); + } mCalendarList->blockSignals(false); + mSharedCalendarList->blockSignals(false); updateTaskList(); } else if (selectedAct == showAllAct) { mCalendarList->blockSignals(true); @@ -434,6 +464,30 @@ void TasksWidget::onCalendarContextMenu(const QPoint& pos) { } } +void TasksWidget::onSharedCalendarContextMenu(const QPoint& pos) { + QListWidgetItem* item = mSharedCalendarList->itemAt(pos); + if (!item) return; + + QString calId = item->data(Qt::UserRole).toString(); + QString calName = item->text(); + + bool isSubscribed = false; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calId) { + isSubscribed = true; + break; + } + } + + QMenu menu(this); + QAction* subAct = menu.addAction(isSubscribed ? tr("Unsubscribe") : tr("Subscribe")); + QAction* selectedAct = menu.exec(mSharedCalendarList->mapToGlobal(pos)); + if (selectedAct == subAct) { + CalendarData::instance()->subscribeToCalendar(calId, !isSubscribed, calName); + } +} + void TasksWidget::exportCalendar(const QString& calId, const QString& calName) { QString icsContent = CalendarData::instance()->exportCalendarToIcs(calId); @@ -471,11 +525,3 @@ void TasksWidget::exportCalendar(const QString& calId, const QString& calName) { } } } - -void TasksWidget::onCalendarViewModeChanged(int index) { - mCalendarListMode = index; - if (index == 0) { - CalendarData::instance()->syncWithGxs(); - } - refreshData(); -} diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.h b/retroshare-gui/src/gui/msgs/TasksWidget.h index 1817324ee..630dd8660 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.h +++ b/retroshare-gui/src/gui/msgs/TasksWidget.h @@ -44,9 +44,10 @@ private slots: void onTaskClicked(int row, int col); void onFilterSelected(QListWidgetItem* item); void onCalendarSelectionChanged(QListWidgetItem* item); + void onSharedCalendarSelectionChanged(QListWidgetItem* item); void onSearchChanged(const QString& text); void onCalendarContextMenu(const QPoint& pos); - void onCalendarViewModeChanged(int index); + void onSharedCalendarContextMenu(const QPoint& pos); private: void buildUi(); @@ -65,7 +66,7 @@ protected: QCalendarWidget* mSidebarCalendar; QListWidget* mFilterList; QListWidget* mCalendarList; - QComboBox* mCalendarViewCombo; + QListWidget* mSharedCalendarList; QLineEdit* mQuickTaskEdit; QLineEdit* mSearchEdit; diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.ui b/retroshare-gui/src/gui/msgs/TasksWidget.ui index 452043cf7..cd5d0a8bb 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.ui +++ b/retroshare-gui/src/gui/msgs/TasksWidget.ui @@ -93,7 +93,7 @@ font-weight: bold; font-size: 14px; - Calendars + My Calendars @@ -104,6 +104,23 @@
+ + + + font-weight: bold; font-size: 14px; margin-top: 10px; + + + Shared Calendars + + + + + + + Qt::CustomContextMenu + + + From a5cf8bbb0ab8be4ac2742071be6804eaaeda91f5 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 7 Jun 2026 23:45:44 +0200 Subject: [PATCH 19/26] Added color change Improve calendar creator --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 32 ++- retroshare-gui/src/gui/msgs/CalendarData.h | 6 + .../src/gui/msgs/CalendarPropertiesDialog.cpp | 228 ++++++++++++++---- .../src/gui/msgs/CalendarPropertiesDialog.h | 24 +- .../src/gui/msgs/CalendarWidget.cpp | 172 +++++++++++-- 5 files changed, 385 insertions(+), 77 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index 6d1b9794f..4eecadd48 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -81,6 +82,11 @@ void CalendarData::loadData() { cal.showReminders = settings.value("showReminders", true).toBool(); cal.email = settings.value("email", "").toString(); cal.onNetwork = settings.value("onNetwork", false).toBool(); + cal.circleType = settings.value("circleType", 1).toUInt(); + cal.circleId = settings.value("circleId", "").toString(); + cal.internalCircle = settings.value("internalCircle", "").toString(); + cal.groupFlags = settings.value("groupFlags", 4).toUInt(); // Default FLAG_PRIVACY_PUBLIC (4) + cal.description = settings.value("description", "").toString(); mCalendars.append(cal); } settings.endArray(); @@ -175,6 +181,11 @@ void CalendarData::saveData() { settings.setValue("showReminders", mCalendars[i].showReminders); settings.setValue("email", mCalendars[i].email); settings.setValue("onNetwork", mCalendars[i].onNetwork); + settings.setValue("circleType", mCalendars[i].circleType); + settings.setValue("circleId", mCalendars[i].circleId); + settings.setValue("internalCircle", mCalendars[i].internalCircle); + settings.setValue("groupFlags", mCalendars[i].groupFlags); + settings.setValue("description", mCalendars[i].description); } settings.endArray(); @@ -657,7 +668,7 @@ bool CalendarData::publishCalendar(const QString& oldId, const QString& email, Q RsGxsGroupId groupId; std::string errMsg; - if (!rsGxsCalendar->createCalendar(cal.name.toStdString(), "RetroShare Calendar", authorId, groupId, errMsg)) { + if (!rsGxsCalendar->createCalendar(cal.name.toStdString(), "RetroShare Calendar", authorId, cal.circleType, RsGxsCircleId(cal.circleId.toStdString()), RsGxsCircleId(cal.internalCircle.toStdString()), cal.groupFlags, groupId, errMsg)) { return false; } @@ -711,6 +722,11 @@ bool CalendarData::subscribeToCalendar(const QString& id, bool subscribe, const localCal.onNetwork = true; localCal.showReminders = true; localCal.owner = "network"; + localCal.circleType = 1; + localCal.circleId = ""; + localCal.internalCircle = ""; + localCal.groupFlags = 4; + localCal.description = ""; mCalendars.append(localCal); saveData(); } @@ -766,8 +782,21 @@ void CalendarData::updateCalendars() { localCal.onNetwork = true; localCal.showReminders = true; localCal.owner = "network"; + localCal.circleType = meta.mCircleType; + localCal.circleId = QString::fromStdString(meta.mCircleId.toStdString()); + localCal.internalCircle = QString::fromStdString(meta.mInternalCircle.toStdString()); + localCal.groupFlags = meta.mGroupFlags; + localCal.description = ""; mCalendars.append(localCal); changed = true; + } else { + QString remoteName = QString::fromUtf8(meta.mGroupName.c_str()); + for (auto& c : mCalendars) { + if (c.id == calId && c.name != remoteName) { + c.name = remoteName; + changed = true; + } + } } std::vector messages; @@ -805,7 +834,6 @@ void CalendarData::handleGxsEvent(std::shared_ptr event) { switch (e->mCalendarEventCode) { case RsCalendarEventCode::NEW_CALENDAR: case RsCalendarEventCode::UPDATED_CALENDAR: - updateCalendars(); case RsCalendarEventCode::NEW_EVENT: case RsCalendarEventCode::UPDATED_EVENT: case RsCalendarEventCode::SUBSCRIBE_STATUS_CHANGED: diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/msgs/CalendarData.h index dcf48cd65..d76e5637a 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.h +++ b/retroshare-gui/src/gui/msgs/CalendarData.h @@ -40,6 +40,12 @@ struct CalendarInfo { bool showReminders; QString email; bool onNetwork; + + uint32_t circleType; + QString circleId; + QString internalCircle; + uint32_t groupFlags; + QString description; }; struct CalendarEvent { diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp index 0c0a24615..62eb3f158 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp @@ -35,12 +35,24 @@ #include #include #include +#include "gui/gxs/GxsIdChooser.h" +#include "gui/gxs/GxsCircleChooser.h" +#include "gui/common/GroupChooser.h" +#include +#include +#include CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget* parent) : QDialog(parent), mCalId(calId), mEditMode(!calId.isEmpty()), mSelectedColor(QColor("#4a90e2")) { setupUi(); - loadIdentities(); + + // Default initialization for GXS choosers + mIdChooser->loadIds(0, RsGxsId()); + mCircleCombo->loadCircles(RsGxsCircleId()); + mLocalCombo->loadGroups(0, RsNodeGroupId()); + mRadioPublic->setChecked(true); + updateCircleOptions(); if (mEditMode) { setWindowTitle(tr("Calendar Properties")); @@ -58,21 +70,42 @@ CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget if (found) { mNameEdit->setText(existingCal.name); mSelectedColor = existingCal.color; - mRemindersCheckBox->setChecked(existingCal.showReminders); mRadioNetwork->setChecked(existingCal.onNetwork); mRadioComputer->setChecked(!existingCal.onNetwork); - - // Try to find the email in the combo box - int idx = mEmailCombo->findText(existingCal.email); - if (idx != -1) { - mEmailCombo->setCurrentIndex(idx); - } else if (!existingCal.email.isEmpty()) { - mEmailCombo->addItem(existingCal.email); - mEmailCombo->setCurrentIndex(mEmailCombo->count() - 1); + + // GXS fields loading if calendar is on network + RsGxsId authorId; + int idxGxs = existingCal.email.lastIndexOf('@'); + int endIdxGxs = existingCal.email.lastIndexOf('>'); + if (idxGxs != -1 && endIdxGxs != -1 && endIdxGxs > idxGxs) { + std::string gxsIdStr = existingCal.email.mid(idxGxs + 1, endIdxGxs - idxGxs - 1).toStdString(); + authorId = RsGxsId(gxsIdStr); } + mIdChooser->loadIds(0, authorId); + + mDescEdit->setPlainText(existingCal.description); + + if (existingCal.circleType == GXS_CIRCLE_TYPE_PUBLIC) { + mRadioPublic->setChecked(true); + } else if (existingCal.circleType == GXS_CIRCLE_TYPE_EXTERNAL) { + mRadioCircle->setChecked(true); + } else if (existingCal.circleType == GXS_CIRCLE_TYPE_YOUR_FRIENDS_ONLY) { + mRadioNodeGroup->setChecked(true); + } else { + mRadioPublic->setChecked(true); + } + + RsGxsCircleId cid(existingCal.circleId.toStdString()); + mCircleCombo->loadCircles(cid); + + RsNodeGroupId ngi(existingCal.internalCircle.toStdString()); + mLocalCombo->loadGroups(0, ngi); + + updateCircleOptions(); } updateColorButton(); mStackedWidget->setCurrentWidget(mPage2); + updatePage2Layout(); } else { setWindowTitle(tr("Create New Calendar")); mStackedWidget->setCurrentWidget(mPage1); @@ -99,7 +132,7 @@ void CalendarPropertiesDialog::setupUi() { page1Layout->setSpacing(15); QLabel* descLabel = new QLabel( - tr("Your calendar can be stored on your computer or be stored on a server in order to access it remotely or share it with your friends or co-workers."), + tr("Your calendar can be stored on your computer or share it with your friends or co-workers."), mPage1 ); descLabel->setWordWrap(true); @@ -128,30 +161,61 @@ void CalendarPropertiesDialog::setupUi() { page2Layout->setContentsMargins(5, 5, 5, 5); page2Layout->setSpacing(15); - QFormLayout* formLayout = new QFormLayout(); - formLayout->setSpacing(12); - formLayout->setLabelAlignment(Qt::AlignRight); + mFormLayout = new QFormLayout(); + mFormLayout->setSpacing(12); + mFormLayout->setLabelAlignment(Qt::AlignRight); mNameEdit = new QLineEdit(mPage2); mNameEdit->setMinimumHeight(26); - formLayout->addRow(tr("Calendar Name:"), mNameEdit); + mFormLayout->addRow(tr("Calendar Name:"), mNameEdit); mColorBtn = new QPushButton(mPage2); mColorBtn->setFixedWidth(80); mColorBtn->setCursor(Qt::PointingHandCursor); updateColorButton(); connect(mColorBtn, SIGNAL(clicked()), this, SLOT(onSelectColor())); - formLayout->addRow(tr("Colour:"), mColorBtn); + mFormLayout->addRow(tr("Colour:"), mColorBtn); - mRemindersCheckBox = new QCheckBox(tr("Show Reminders"), mPage2); - mRemindersCheckBox->setChecked(true); - formLayout->addRow(QString(), mRemindersCheckBox); + mIdChooser = new GxsIdChooser(mPage2); + mFormLayout->addRow(tr("Owner:"), mIdChooser); - mEmailCombo = new QComboBox(mPage2); - mEmailCombo->setMinimumHeight(26); - formLayout->addRow(tr("Email:"), mEmailCombo); + page2Layout->addLayout(mFormLayout); + + // Message Distribution group box + mDistribGroupBox = new QGroupBox(tr("Message Distribution"), mPage2); + QVBoxLayout* distribLayout = new QVBoxLayout(mDistribGroupBox); + distribLayout->setContentsMargins(10, 10, 10, 10); + distribLayout->setSpacing(8); + + QHBoxLayout* radioLayout = new QHBoxLayout(); + mRadioPublic = new QRadioButton(tr("Public"), mDistribGroupBox); + mRadioCircle = new QRadioButton(tr("Restricted to Circle"), mDistribGroupBox); + mRadioNodeGroup = new QRadioButton(tr("Restricted node group"), mDistribGroupBox); + + mRadioPublic->setChecked(true); + radioLayout->addWidget(mRadioPublic); + radioLayout->addWidget(mRadioCircle); + radioLayout->addWidget(mRadioNodeGroup); + distribLayout->addLayout(radioLayout); + + mCircleCombo = new GxsCircleChooser(mDistribGroupBox); + mCircleCombo->setMinimumHeight(26); + mLocalCombo = new GroupChooser(mDistribGroupBox); + mLocalCombo->setMinimumHeight(26); + + distribLayout->addWidget(mCircleCombo); + distribLayout->addWidget(mLocalCombo); + + page2Layout->addWidget(mDistribGroupBox); + + mDescLabel = new QLabel(tr("Description"), mPage2); + mDescEdit = new QPlainTextEdit(mPage2); + mDescEdit->setPlaceholderText(tr("Set a descriptive description here")); + mDescEdit->setMaximumHeight(80); + + page2Layout->addWidget(mDescLabel); + page2Layout->addWidget(mDescEdit); - page2Layout->addLayout(formLayout); page2Layout->addStretch(); mStackedWidget->addWidget(mPage2); @@ -164,6 +228,10 @@ void CalendarPropertiesDialog::setupUi() { mCreateOrSaveBtn = new QPushButton(mEditMode ? tr("OK") : tr("Create Calendar"), this); mCancelBtn = new QPushButton(tr("Cancel"), this); + connect(mRadioPublic, SIGNAL(clicked()), this, SLOT(updateCircleOptions())); + connect(mRadioCircle, SIGNAL(clicked()), this, SLOT(updateCircleOptions())); + connect(mRadioNodeGroup, SIGNAL(clicked()), this, SLOT(updateCircleOptions())); + connect(mBackBtn, SIGNAL(clicked()), this, SLOT(onBack())); connect(mNextBtn, SIGNAL(clicked()), this, SLOT(onNext())); connect(mCreateOrSaveBtn, SIGNAL(clicked()), this, SLOT(onAccept())); @@ -193,28 +261,6 @@ void CalendarPropertiesDialog::updateColorButton() { ).arg(mSelectedColor.name())); } -void CalendarPropertiesDialog::loadIdentities() { - mEmailCombo->clear(); - QStringList emails; - - if (rsIdentity) { - std::list own_identities; - rsIdentity->getOwnIds(own_identities); - for (const auto& id : own_identities) { - RsIdentityDetails details; - if (rsIdentity->getIdDetails(id, details)) { - QString nickname = QString::fromUtf8(details.mNickname.c_str()).trimmed(); - QString gxsId = QString::fromStdString(id.toStdString()); - if (!nickname.isEmpty()) { - emails.append(QString("%1 <%1@%2>").arg(nickname).arg(gxsId)); - } - } - } - } - - mEmailCombo->addItems(emails); -} - void CalendarPropertiesDialog::onNext() { if (mRadioImport && mRadioImport->isChecked()) { accept(); @@ -250,7 +296,7 @@ void CalendarPropertiesDialog::onAccept() { CalendarInfo info = getCalendarInfo(); - if (info.onNetwork && rsGxsCalendar) { + if (info.onNetwork && rsGxsCalendar && info.owner == "local") { // Extract GXS ID from email RsGxsId authorId; int idx = info.email.lastIndexOf('@'); @@ -261,9 +307,13 @@ void CalendarPropertiesDialog::onAccept() { } std::string errMsg; + std::string descStr = info.description.toStdString(); + RsGxsCircleId circleId(info.circleId.toStdString()); + RsGxsCircleId internalCircle(info.internalCircle.toStdString()); + if (mEditMode) { RsGxsGroupId groupId(info.id.toStdString()); - if (rsGxsCalendar->updateCalendar(groupId, info.name.toStdString(), "RetroShare Calendar", authorId, errMsg)) { + if (rsGxsCalendar->updateCalendar(groupId, info.name.toStdString(), descStr, authorId, info.circleType, circleId, internalCircle, info.groupFlags, errMsg)) { CalendarData::instance()->updateCalendar(info); } else { QMessageBox::critical(this, tr("GXS Error"), tr("Failed to update network calendar: %1").arg(QString::fromStdString(errMsg))); @@ -271,7 +321,7 @@ void CalendarPropertiesDialog::onAccept() { } } else { RsGxsGroupId groupId; - if (rsGxsCalendar->createCalendar(info.name.toStdString(), "RetroShare Calendar", authorId, groupId, errMsg)) { + if (rsGxsCalendar->createCalendar(info.name.toStdString(), descStr, authorId, info.circleType, circleId, internalCircle, info.groupFlags, groupId, errMsg)) { info.id = QString::fromStdString(groupId.toStdString()); rsGxsCalendar->subscribeToCalendar(groupId, true, errMsg); CalendarData::instance()->addCalendar(info); @@ -298,12 +348,90 @@ CalendarInfo CalendarPropertiesDialog::getCalendarInfo() const { info.color = mSelectedColor; info.onNetwork = mRadioNetwork->isChecked(); info.isPublic = info.onNetwork; - info.showReminders = mRemindersCheckBox->isChecked(); - info.email = mEmailCombo->currentText(); + + // Preserve owner and defaults if in edit mode info.owner = "local"; + info.showReminders = true; + info.email = ""; + if (mEditMode) { + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == mCalId) { + info.owner = c.owner; + info.showReminders = c.showReminders; + info.email = c.email; + break; + } + } + } + + info.circleType = GXS_CIRCLE_TYPE_PUBLIC; + info.circleId = ""; + info.internalCircle = ""; + info.groupFlags = GXS_SERV::FLAG_PRIVACY_PUBLIC; + info.description = ""; + + if (info.onNetwork) { + RsGxsId authorId; + if (mIdChooser->getChosenId(authorId) == GxsIdChooser::KnowId) { + std::string nickname = ""; + if (rsIdentity) { + RsIdentityDetails details; + if (rsIdentity->getIdDetails(authorId, details)) { + nickname = details.mNickname; + } + } + if (!nickname.empty()) { + info.email = QString("%1 <%1@%2>").arg(QString::fromStdString(nickname)).arg(QString::fromStdString(authorId.toStdString())); + } else { + info.email = QString("<%1@%1>").arg(QString::fromStdString(authorId.toStdString())); + } + } else { + info.email = ""; + } + + if (mRadioPublic->isChecked()) { + info.circleType = GXS_CIRCLE_TYPE_PUBLIC; + info.groupFlags = GXS_SERV::FLAG_PRIVACY_PUBLIC; + } else if (mRadioCircle->isChecked()) { + info.circleType = GXS_CIRCLE_TYPE_EXTERNAL; + RsGxsCircleId cid; + mCircleCombo->getChosenCircle(cid); + info.circleId = QString::fromStdString(cid.toStdString()); + info.groupFlags = GXS_SERV::FLAG_PRIVACY_RESTRICTED; + } else if (mRadioNodeGroup->isChecked()) { + info.circleType = GXS_CIRCLE_TYPE_YOUR_FRIENDS_ONLY; + RsNodeGroupId ngi; + mLocalCombo->getChosenGroup(ngi); + info.internalCircle = QString::fromStdString(ngi.toStdString()); + info.groupFlags = GXS_SERV::FLAG_PRIVACY_PRIVATE; + } + info.description = mDescEdit->toPlainText(); + } + return info; } bool CalendarPropertiesDialog::isImportMode() const { return mRadioImport && mRadioImport->isChecked(); } + +void CalendarPropertiesDialog::updateCircleOptions() { + mCircleCombo->setVisible(mRadioCircle->isChecked()); + mLocalCombo->setVisible(mRadioNodeGroup->isChecked()); +} + +void CalendarPropertiesDialog::updatePage2Layout() { + bool onNetwork = mRadioNetwork->isChecked(); + + mIdChooser->setVisible(onNetwork); + if (QWidget* lbl = mFormLayout->labelForField(mIdChooser)) { + lbl->setVisible(onNetwork); + } + + mDistribGroupBox->setVisible(onNetwork); + mDescLabel->setVisible(onNetwork); + mDescEdit->setVisible(onNetwork); + + updateCircleOptions(); +} diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h index 35c0a6990..5d3933445 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h @@ -31,6 +31,13 @@ class QLineEdit; class QPushButton; class QCheckBox; class QComboBox; +class GxsIdChooser; +class GxsCircleChooser; +class GroupChooser; +class QGroupBox; +class QPlainTextEdit; +class QLabel; +class QFormLayout; class CalendarPropertiesDialog : public QDialog { Q_OBJECT @@ -46,11 +53,12 @@ private slots: void onBack(); void onSelectColor(); void onAccept(); + void updateCircleOptions(); private: void setupUi(); - void loadIdentities(); void updateColorButton(); + void updatePage2Layout(); QString mCalId; bool mEditMode; @@ -66,10 +74,20 @@ private: QRadioButton* mRadioImport; // Page 2 widgets + QFormLayout* mFormLayout; QLineEdit* mNameEdit; QPushButton* mColorBtn; - QCheckBox* mRemindersCheckBox; - QComboBox* mEmailCombo; + + // GXS network calendar widgets + GxsIdChooser* mIdChooser; + QGroupBox* mDistribGroupBox; + QRadioButton* mRadioPublic; + QRadioButton* mRadioCircle; + QRadioButton* mRadioNodeGroup; + GxsCircleChooser* mCircleCombo; + GroupChooser* mLocalCombo; + QLabel* mDescLabel; + QPlainTextEdit* mDescEdit; // Buttons QPushButton* mNextBtn; diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index 03b7a7d73..10fb91bac 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -237,15 +237,25 @@ void CalendarWidget::refreshData() { if (ownedByUs) continue; QString calName = QString::fromUtf8(meta.mGroupName.c_str()); - bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); QListWidgetItem* item = new QListWidgetItem(calName, mSharedCalendarList); item->setData(Qt::UserRole, calId); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - // Render blue bullet for subscribed, grey for unsubscribed - QPixmap pix(12, 12); - pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); + // Find if it has a saved local color and check subscription status locally + bool isSubscribedLocal = false; + QColor calColor = QColor("#4a90e2"); + for (const auto& c : cals) { + if (c.id == calId) { + isSubscribedLocal = true; + calColor = c.color; + break; + } + } + + // Render custom color bullet for subscribed, grey for unsubscribed + QPixmap pix(16, 16); + pix.fill(isSubscribedLocal ? calColor : Qt::gray); item->setIcon(QIcon(pix)); // Restore checked state if we have a saved state, @@ -253,7 +263,7 @@ void CalendarWidget::refreshData() { if (sharedCheckedStates.contains(calId)) { item->setCheckState(sharedCheckedStates[calId]); } else { - item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + item->setCheckState(isSubscribedLocal ? Qt::Checked : Qt::Unchecked); } } } @@ -367,12 +377,43 @@ void CalendarWidget::updateEventList() { } } +static QColor blendColors(const QColor& color1, const QColor& color2, qreal ratio) { + int r = color1.red() * ratio + color2.red() * (1.0 - ratio); + int g = color1.green() * ratio + color2.green() * (1.0 - ratio); + int b = color1.blue() * ratio + color2.blue() * (1.0 - ratio); + return QColor(r, g, b); +} + +static void styleEventItem(QTableWidgetItem* item, const QColor& eventColor, const QColor& baseBg) { + bool isDark = (baseBg.value() < 128); + QColor bgCol; + QColor fgCol; + if (isDark) { + bgCol = blendColors(eventColor, baseBg, 0.25); + fgCol = eventColor.lighter(130); + } else { + bgCol = blendColors(eventColor, baseBg, 0.15); + fgCol = eventColor.darker(140); + } + item->setBackground(QBrush(bgCol)); + item->setForeground(QBrush(fgCol)); + QFont font = item->font(); + font.setBold(true); + item->setFont(font); +} + void CalendarWidget::updateDayView() { mDayTable->setRowCount(0); mDayTable->setRowCount(24); // List of events for the selected day const auto& events = CalendarData::instance()->getEvents(); + const auto& cals = CalendarData::instance()->getCalendars(); + + QMap calColors; + for (const auto& cal : cals) { + calColors[cal.id] = cal.color; + } QStringList enabledCalIds; for (int i = 0; i < mCalendarList->count(); ++i) { @@ -384,6 +425,8 @@ void CalendarWidget::updateDayView() { if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } + QColor baseBg = mDayTable->palette().color(QPalette::Base); + for (int hour = 0; hour < 24; ++hour) { QString timeText = QString("%1:00").arg(hour, 2, 10, QChar('0')); mDayTable->setItem(hour, 0, new QTableWidgetItem(timeText)); @@ -402,7 +445,17 @@ void CalendarWidget::updateDayView() { QTableWidgetItem* evCell = new QTableWidgetItem(matchedEvents.join(", ")); if (!lastEventId.isEmpty()) { mCellEventMap[QString("0_%1_%2").arg(hour).arg(1)] = lastEventId; - evCell->setBackground(QBrush(QColor("#eef5fc"))); + + QColor eventColor("#4a90e2"); // default fallback + for (const auto& ev : events) { + if (ev.id == lastEventId) { + if (calColors.contains(ev.calendarId)) { + eventColor = calColors[ev.calendarId]; + } + break; + } + } + styleEventItem(evCell, eventColor, baseBg); } mDayTable->setItem(hour, 1, evCell); } @@ -423,6 +476,12 @@ void CalendarWidget::updateWeekView() { mWeekTable->setHorizontalHeaderLabels(headers); const auto& events = CalendarData::instance()->getEvents(); + const auto& cals = CalendarData::instance()->getCalendars(); + + QMap calColors; + for (const auto& cal : cals) { + calColors[cal.id] = cal.color; + } QStringList enabledCalIds; for (int i = 0; i < mCalendarList->count(); ++i) { @@ -434,6 +493,8 @@ void CalendarWidget::updateWeekView() { if (item->checkState() == Qt::Checked) enabledCalIds.append(item->data(Qt::UserRole).toString()); } + QColor baseBg = mWeekTable->palette().color(QPalette::Base); + // Populate week cells for (int dayIdx = 0; dayIdx < 7; ++dayIdx) { QDate date = monday.addDays(dayIdx); @@ -443,7 +504,13 @@ void CalendarWidget::updateWeekView() { if (ev.start.date() == date) { if (rowIdx >= mWeekTable->rowCount()) mWeekTable->insertRow(rowIdx); QTableWidgetItem* cellItem = new QTableWidgetItem(ev.title); - cellItem->setBackground(QBrush(QColor("#eef5fc"))); + + QColor eventColor("#4a90e2"); // default fallback + if (calColors.contains(ev.calendarId)) { + eventColor = calColors[ev.calendarId]; + } + styleEventItem(cellItem, eventColor, baseBg); + mWeekTable->setItem(rowIdx, dayIdx, cellItem); mCellEventMap[QString("1_%1_%2").arg(rowIdx).arg(dayIdx)] = ev.id; rowIdx++; @@ -490,17 +557,20 @@ void CalendarWidget::updateMonthView() { cellLines << QString::number(date.day()); } + QStringList eventCalIds; QString matchedEventId = ""; for (const auto& ev : events) { if (!enabledCalIds.contains(ev.calendarId)) continue; if (ev.start.date() == date) { cellLines << ev.title; + eventCalIds << ev.calendarId; matchedEventId = ev.id; } } QTableWidgetItem* cellItem = new QTableWidgetItem(cellLines.join("\n")); cellItem->setData(Qt::UserRole + 1, date); // Store the QDate + cellItem->setData(Qt::UserRole + 2, eventCalIds); // Store list of calendar IDs if (date.month() != mSelectedDate.month()) { cellItem->setForeground(QBrush(Qt::gray)); @@ -765,9 +835,22 @@ void CalendarWidget::onSharedCalendarContextMenu(const QPoint& pos) { QMenu menu(this); QAction* subAct = menu.addAction(isSubscribed ? tr("Unsubscribe") : tr("Subscribe")); + QAction* propertiesAct = nullptr; + if (isSubscribed) { + menu.addSeparator(); + propertiesAct = menu.addAction(tr("Properties")); + } + QAction* selectedAct = menu.exec(mSharedCalendarList->mapToGlobal(pos)); + if (!selectedAct) return; + if (selectedAct == subAct) { CalendarData::instance()->subscribeToCalendar(calId, !isSubscribed, calName); + } else if (propertiesAct && selectedAct == propertiesAct) { + CalendarPropertiesDialog dlg(calId, this); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } } } @@ -1047,6 +1130,11 @@ void CalendarWidget::importCalendar() { cal.showReminders = true; cal.email = ""; cal.onNetwork = false; + cal.circleType = 1; + cal.circleId = ""; + cal.internalCircle = ""; + cal.groupFlags = 4; + cal.description = ""; CalendarData::instance()->addCalendar(cal); @@ -1089,16 +1177,31 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& QDate cellDate = index.data(Qt::UserRole + 1).toDate(); bool isSelected = (cellDate.isValid() && cellDate == mCalendarWidget->selectedDate()); + QColor baseBg = mCalendarWidget->palette().color(QPalette::Base); + bool isDark = (baseBg.value() < 128); + // Draw background QColor bgColor; - if (isSelected) { - bgColor = QColor("#eff6ff"); // Light blue highlight for selected day - } else if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { - bgColor = QColor("#f8fafc"); // Slate-50 for days outside the current month - } else if (index.column() == 5 || index.column() == 6) { - bgColor = QColor("#f1f5f9"); // Slate-100 for weekends + if (isDark) { + if (isSelected) { + bgColor = QColor("#1e3a8a"); // Dark blue highlight + } else if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { + bgColor = QColor("#0f172a"); // Very dark slate for days outside month + } else if (index.column() == 5 || index.column() == 6) { + bgColor = QColor("#1e293b"); // Dark slate for weekends + } else { + bgColor = QColor("#111827"); // Dark background for weekdays + } } else { - bgColor = QColor("#ffffff"); // White for standard weekdays + if (isSelected) { + bgColor = QColor("#eff6ff"); // Light blue highlight for selected day + } else if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { + bgColor = QColor("#f8fafc"); // Slate-50 for days outside the current month + } else if (index.column() == 5 || index.column() == 6) { + bgColor = QColor("#f1f5f9"); // Slate-100 for weekends + } else { + bgColor = QColor("#ffffff"); // White for standard weekdays + } } painter->fillRect(option.rect, bgColor); @@ -1107,7 +1210,7 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& painter->setPen(QPen(QColor("#3b82f6"), 2)); painter->drawRect(option.rect.adjusted(1, 1, -1, -1)); } else { - painter->setPen(QPen(QColor("#e2e8f0"), 1)); + painter->setPen(QPen(isDark ? QColor("#334155") : QColor("#e2e8f0"), 1)); painter->drawRect(option.rect); } @@ -1123,11 +1226,11 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& painter->setFont(dayFont); if (cellDate.isValid() && cellDate.month() != mCalendarWidget->selectedDate().month()) { - painter->setPen(QColor("#94a3b8")); // Muted grey for other month days + painter->setPen(isDark ? QColor("#475569") : QColor("#94a3b8")); // Muted grey for other month days } else if (isSelected) { - painter->setPen(QColor("#2563eb")); // Darker blue for selected day number + painter->setPen(isDark ? QColor("#60a5fa") : QColor("#2563eb")); // Blue for selected day number } else { - painter->setPen(QColor("#1e293b")); // Slate-800 for standard days + painter->setPen(isDark ? QColor("#f1f5f9") : QColor("#1e293b")); // Light/slate-800 for standard days } QRect dayRect = option.rect.adjusted(5, 5, -8, -5); @@ -1140,14 +1243,14 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& QRect badgeRect(option.rect.left() + 6, option.rect.top() + 5, 38, 16); painter->setPen(Qt::NoPen); - painter->setBrush(QColor("#e2e8f0")); // Slate-200 + painter->setBrush(isDark ? QColor("#334155") : QColor("#e2e8f0")); // Slate badge background painter->drawRoundedRect(badgeRect, 8, 8); QFont badgeFont = option.font; badgeFont.setPointSize(badgeFont.pointSize() - 2); badgeFont.setBold(true); painter->setFont(badgeFont); - painter->setPen(QColor("#475569")); // Slate-600 + painter->setPen(isDark ? QColor("#cbd5e1") : QColor("#475569")); // Badge text painter->drawText(badgeRect, Qt::AlignCenter, weekStr); } @@ -1157,17 +1260,42 @@ void MonthCalendarDelegate::paint(QPainter* painter, const QStyleOptionViewItem& eventFont.setPointSize(eventFont.pointSize() - 1); painter->setFont(eventFont); + QStringList eventCalIds = index.data(Qt::UserRole + 2).toStringList(); + for (int i = 1; i < lines.size(); ++i) { if (yOffset + 18 > option.rect.bottom()) break; // Out of bounds QString eventTitle = lines[i]; QRect eventRect(option.rect.left() + 6, yOffset, option.rect.width() - 12, 16); + // Find event calendar color + QColor eventColor("#4a90e2"); // default fallback + if (i - 1 < eventCalIds.size()) { + QString calId = eventCalIds[i - 1]; + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calId) { + eventColor = c.color; + break; + } + } + } + + QColor bgCol; + QColor fgCol; + if (isDark) { + bgCol = blendColors(eventColor, bgColor, 0.25); + fgCol = eventColor.lighter(130); + } else { + bgCol = blendColors(eventColor, bgColor, 0.15); + fgCol = eventColor.darker(140); + } + painter->setPen(Qt::NoPen); - painter->setBrush(QColor("#e0f2fe")); // Light blue event background + painter->setBrush(bgCol); painter->drawRoundedRect(eventRect, 3, 3); - painter->setPen(QColor("#0369a1")); // Blue text for events + painter->setPen(fgCol); painter->drawText(eventRect.adjusted(4, 0, -4, 0), Qt::AlignVCenter | Qt::AlignLeft, eventTitle); yOffset += 19; From f26a2aef7864bb6b44207c1ddbf4a0659063d89f Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:55:16 +0200 Subject: [PATCH 20/26] fix calendardata refresh --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index 4eecadd48..b516f8634 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -823,8 +823,11 @@ void CalendarData::updateCalendars() { if (changed) { saveData(); - emit calendarDataChanged(); } + // Always emit so the UI refreshes the shared calendar list + // from GXS group metadata (getCalendarsSummaries), even when + // local calendar data didn't change. + emit calendarDataChanged(); } } From 39fe59f2cf59c59e958f50fa02800950bec309cf Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:18:17 +0200 Subject: [PATCH 21/26] Fixed update of the shared calendars & events update Fixed to enable some widgets only for the admins of the calendar --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 19 ++++------- .../src/gui/msgs/CalendarPropertiesDialog.cpp | 15 ++++++++- .../src/gui/msgs/CalendarWidget.cpp | 21 ++++++++---- retroshare-gui/src/gui/msgs/CalendarWidget.h | 4 ++- retroshare-gui/src/gui/msgs/TasksWidget.cpp | 33 ++++++++++++++----- retroshare-gui/src/gui/msgs/TasksWidget.h | 1 + 6 files changed, 65 insertions(+), 28 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index b516f8634..a2972a516 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -95,7 +95,7 @@ void CalendarData::loadData() { if (mCalendars.isEmpty()) { CalendarInfo defaultCal; defaultCal.id = "personal"; - defaultCal.name = "Privat"; + defaultCal.name = "Private"; defaultCal.color = QColor("#4a90e2"); defaultCal.isPublic = false; defaultCal.owner = "local"; @@ -103,17 +103,6 @@ void CalendarData::loadData() { defaultCal.email = "retroshare "; defaultCal.onNetwork = false; mCalendars.append(defaultCal); - - CalendarInfo testCal; - testCal.id = "test"; - testCal.name = "test"; - testCal.color = QColor("#50e3c2"); - testCal.isPublic = true; - testCal.owner = "local"; - testCal.showReminders = true; - testCal.email = "retroshare "; - testCal.onNetwork = true; - mCalendars.append(testCal); } // Load Events @@ -237,6 +226,7 @@ void CalendarData::saveData() { void CalendarData::addCalendar(const CalendarInfo& cal) { mCalendars.append(cal); saveData(); + emit calendarDataChanged(); } void CalendarData::updateCalendar(const CalendarInfo& cal) { @@ -247,6 +237,7 @@ void CalendarData::updateCalendar(const CalendarInfo& cal) { } } saveData(); + emit calendarDataChanged(); } void CalendarData::removeCalendar(const QString& id) { @@ -269,6 +260,7 @@ void CalendarData::removeCalendar(const QString& id) { [&id](const CalendarTask& t) { return t.calendarId == id; }), mTasks.end()); saveData(); + emit calendarDataChanged(); } void CalendarData::addEvent(const CalendarEvent& ev) { @@ -823,6 +815,7 @@ void CalendarData::updateCalendars() { if (changed) { saveData(); + emit calendarDataChanged(); } // Always emit so the UI refreshes the shared calendar list // from GXS group metadata (getCalendarsSummaries), even when @@ -837,6 +830,8 @@ void CalendarData::handleGxsEvent(std::shared_ptr event) { switch (e->mCalendarEventCode) { case RsCalendarEventCode::NEW_CALENDAR: case RsCalendarEventCode::UPDATED_CALENDAR: + updateCalendars(); + break; case RsCalendarEventCode::NEW_EVENT: case RsCalendarEventCode::UPDATED_EVENT: case RsCalendarEventCode::SUBSCRIBE_STATUS_CHANGED: diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp index 62eb3f158..e5821b209 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp @@ -102,6 +102,19 @@ CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget mLocalCombo->loadGroups(0, ngi); updateCircleOptions(); + + // Disable distribution and description controls for non-admin users + // owner == "local" means the current user created (and administers) this calendar + bool isAdmin = (existingCal.owner == "local"); + if (existingCal.onNetwork && !isAdmin) { + mIdChooser->setEnabled(false); + mRadioPublic->setEnabled(false); + mRadioCircle->setEnabled(false); + mRadioNodeGroup->setEnabled(false); + mDescEdit->setReadOnly(true); + mDescEdit->setEnabled(false); + mNameEdit->setEnabled(false); + } } updateColorButton(); mStackedWidget->setCurrentWidget(mPage2); @@ -182,7 +195,7 @@ void CalendarPropertiesDialog::setupUi() { page2Layout->addLayout(mFormLayout); // Message Distribution group box - mDistribGroupBox = new QGroupBox(tr("Message Distribution"), mPage2); + mDistribGroupBox = new QGroupBox(tr("Calendar Distribution"), mPage2); QVBoxLayout* distribLayout = new QVBoxLayout(mDistribGroupBox); distribLayout->setContentsMargins(10, 10, 10, 10); distribLayout->setSpacing(8); diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index 10fb91bac..5d302561e 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -195,7 +195,7 @@ void CalendarWidget::refreshData() { item->setFlags(item->flags() | Qt::ItemIsUserCheckable); // Render colored bullet point icon - QPixmap pix(12, 12); + QPixmap pix(16, 16); pix.fill(cal.color); item->setIcon(QIcon(pix)); @@ -211,11 +211,14 @@ void CalendarWidget::refreshData() { // 2. Populate Shared Calendars (not owned by us) { - // Save current check states + // Save current check states and subscription states QMap sharedCheckedStates; + QMap sharedSubscribedStates; for (int i = 0; i < mSharedCalendarList->count(); ++i) { QListWidgetItem* item = mSharedCalendarList->item(i); - sharedCheckedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + QString calId = item->data(Qt::UserRole).toString(); + sharedCheckedStates[calId] = item->checkState(); + sharedSubscribedStates[calId] = item->data(Qt::UserRole + 1).toBool(); } mSharedCalendarList->blockSignals(true); @@ -257,11 +260,17 @@ void CalendarWidget::refreshData() { QPixmap pix(16, 16); pix.fill(isSubscribedLocal ? calColor : Qt::gray); item->setIcon(QIcon(pix)); + item->setData(Qt::UserRole + 1, isSubscribedLocal); - // Restore checked state if we have a saved state, - // otherwise default to Checked if subscribed, Unchecked if unsubscribed + // Restore checked state if we have a saved state and subscription status did not change. + // If subscription state changed, set checked state based on new subscription status. if (sharedCheckedStates.contains(calId)) { - item->setCheckState(sharedCheckedStates[calId]); + bool wasSubscribed = sharedSubscribedStates.value(calId, false); + if (wasSubscribed != isSubscribedLocal) { + item->setCheckState(isSubscribedLocal ? Qt::Checked : Qt::Unchecked); + } else { + item->setCheckState(sharedCheckedStates[calId]); + } } else { item->setCheckState(isSubscribedLocal ? Qt::Checked : Qt::Unchecked); } diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/msgs/CalendarWidget.h index 326d2ae62..70beaecfe 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.h @@ -49,9 +49,11 @@ public: CalendarWidget(QWidget* parent = nullptr); ~CalendarWidget(); - void refreshData(); QDate selectedDate() const { return mSelectedDate; } +public slots: + void refreshData(); + private slots: void onNewEvent(); void onNewCalendar(); diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/msgs/TasksWidget.cpp index 82ff328ad..7265bf3c1 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.cpp +++ b/retroshare-gui/src/gui/msgs/TasksWidget.cpp @@ -154,11 +154,14 @@ void TasksWidget::refreshData() { // 2. Populate Shared Calendars (not owned by us) { - // Save current check states + // Save current check states and subscription states QMap sharedCheckedStates; + QMap sharedSubscribedStates; for (int i = 0; i < mSharedCalendarList->count(); ++i) { QListWidgetItem* item = mSharedCalendarList->item(i); - sharedCheckedStates[item->data(Qt::UserRole).toString()] = item->checkState(); + QString calId = item->data(Qt::UserRole).toString(); + sharedCheckedStates[calId] = item->checkState(); + sharedSubscribedStates[calId] = item->data(Qt::UserRole + 1).toBool(); } mSharedCalendarList->blockSignals(true); @@ -180,7 +183,15 @@ void TasksWidget::refreshData() { if (ownedByUs) continue; QString calName = QString::fromUtf8(meta.mGroupName.c_str()); - bool isSubscribed = (meta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_SUBSCRIBED); + + // Check subscription status locally (immediate) + bool isSubscribedLocal = false; + for (const auto& c : cals) { + if (c.id == calId) { + isSubscribedLocal = true; + break; + } + } QListWidgetItem* item = new QListWidgetItem(calName, mSharedCalendarList); item->setData(Qt::UserRole, calId); @@ -188,15 +199,21 @@ void TasksWidget::refreshData() { // Render blue bullet for subscribed, grey for unsubscribed QPixmap pix(12, 12); - pix.fill(isSubscribed ? QColor("#4a90e2") : Qt::gray); + pix.fill(isSubscribedLocal ? QColor("#4a90e2") : Qt::gray); item->setIcon(QIcon(pix)); + item->setData(Qt::UserRole + 1, isSubscribedLocal); - // Restore checked state if we have a saved state, - // otherwise default to Checked if subscribed, Unchecked if unsubscribed + // Restore checked state if we have a saved state and subscription status did not change. + // If subscription state changed, set checked state based on new subscription status. if (sharedCheckedStates.contains(calId)) { - item->setCheckState(sharedCheckedStates[calId]); + bool wasSubscribed = sharedSubscribedStates.value(calId, false); + if (wasSubscribed != isSubscribedLocal) { + item->setCheckState(isSubscribedLocal ? Qt::Checked : Qt::Unchecked); + } else { + item->setCheckState(sharedCheckedStates[calId]); + } } else { - item->setCheckState(isSubscribed ? Qt::Checked : Qt::Unchecked); + item->setCheckState(isSubscribedLocal ? Qt::Checked : Qt::Unchecked); } } } diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.h b/retroshare-gui/src/gui/msgs/TasksWidget.h index 630dd8660..a3154c122 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.h +++ b/retroshare-gui/src/gui/msgs/TasksWidget.h @@ -35,6 +35,7 @@ public: TasksWidget(QWidget* parent = nullptr); ~TasksWidget(); +public slots: void refreshData(); private slots: From 46368f3067e28eb05f240a2d79022249c0af63e6 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:33:32 +0200 Subject: [PATCH 22/26] Fix to get work attachments Enabled sorting for the event table Added view Mode for Events Trying fix Attendees invite --- retroshare-gui/src/gui/msgs/CalendarData.cpp | 16 + retroshare-gui/src/gui/msgs/CalendarData.h | 2 + .../src/gui/msgs/CalendarWidget.cpp | 17 +- retroshare-gui/src/gui/msgs/EventDialog.cpp | 365 ++++++++++++++++-- retroshare-gui/src/gui/msgs/EventDialog.h | 16 +- retroshare-gui/src/gui/msgs/TaskDialog.cpp | 131 ++++++- retroshare-gui/src/gui/msgs/TaskDialog.h | 2 + 7 files changed, 495 insertions(+), 54 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/msgs/CalendarData.cpp index a2972a516..45c9effc2 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarData.cpp @@ -123,6 +123,7 @@ void CalendarData::loadData() { ev.description = settings.value("description").toString(); ev.attendees = settings.value("attendees").toStringList(); ev.isPublic = settings.value("isPublic").toBool(); + ev.attachments = settings.value("attachments").toStringList(); mEvents.append(ev); } settings.endArray(); @@ -147,6 +148,7 @@ void CalendarData::loadData() { task.reminder = settings.value("reminder").toString(); task.description = settings.value("description").toString(); task.completed = settings.value("completed").toBool(); + task.attachments = settings.value("attachments").toStringList(); mTasks.append(task); } settings.endArray(); @@ -195,6 +197,7 @@ void CalendarData::saveData() { settings.setValue("description", mEvents[i].description); settings.setValue("attendees", mEvents[i].attendees); settings.setValue("isPublic", mEvents[i].isPublic); + settings.setValue("attachments", mEvents[i].attachments); } settings.endArray(); @@ -217,6 +220,7 @@ void CalendarData::saveData() { settings.setValue("reminder", mTasks[i].reminder); settings.setValue("description", mTasks[i].description); settings.setValue("completed", mTasks[i].completed); + settings.setValue("attachments", mTasks[i].attachments); } settings.endArray(); @@ -398,6 +402,10 @@ QString CalendarData::exportCalendarToIcs(const QString& calId) const { icsContent += QString("DTEND:%1\r\n").arg(ev.end.toUTC().toString("yyyyMMdd'T'HHmmss'Z'")); } + for (const auto& att : ev.attachments) { + icsContent += QString("ATTACH:%1\r\n").arg(att); + } + icsContent += "END:VEVENT\r\n"; } @@ -437,6 +445,10 @@ QString CalendarData::exportCalendarToIcs(const QString& calId) const { icsContent += QString("PERCENT-COMPLETE:%1\r\n").arg(t.percentComplete); icsContent += QString("COMPLETED:%1\r\n").arg(t.completed ? "TRUE" : "FALSE"); + for (const auto& att : t.attachments) { + icsContent += QString("ATTACH:%1\r\n").arg(att); + } + icsContent += "END:VTODO\r\n"; } @@ -561,6 +573,8 @@ void CalendarData::importCalendarFromIcs(const QString& calId, const QString& ic } else { currentEvent.end = parseIcsDateTime(val); } + } else if (key.compare("ATTACH", Qt::CaseInsensitive) == 0) { + currentEvent.attachments.append(val); } } } else if (inTask) { @@ -598,6 +612,8 @@ void CalendarData::importCalendarFromIcs(const QString& calId, const QString& ic currentTask.percentComplete = val.toInt(); } else if (key.compare("COMPLETED", Qt::CaseInsensitive) == 0) { currentTask.completed = (val.compare("TRUE", Qt::CaseInsensitive) == 0); + } else if (key.compare("ATTACH", Qt::CaseInsensitive) == 0) { + currentTask.attachments.append(val); } } } diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/msgs/CalendarData.h index d76e5637a..3e111c14c 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.h +++ b/retroshare-gui/src/gui/msgs/CalendarData.h @@ -62,6 +62,7 @@ struct CalendarEvent { QString description; QStringList attendees; // PGP IDs of contacts bool isPublic; + QStringList attachments; }; struct CalendarTask { @@ -80,6 +81,7 @@ struct CalendarTask { QString reminder; QString description; bool completed; + QStringList attachments; }; class CalendarData : public QObject { diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp index 5d302561e..97a107ed1 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/msgs/CalendarWidget.cpp @@ -104,6 +104,7 @@ void CalendarWidget::buildUi() { mEventTable->setSelectionBehavior(QAbstractItemView::SelectRows); mEventTable->setSelectionMode(QAbstractItemView::SingleSelection); mEventTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + mEventTable->setSortingEnabled(true); connect(mEventTable, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(onEventSelected(int,int))); mEventTable->setContextMenuPolicy(Qt::CustomContextMenu); connect(mEventTable, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(onEventTableContextMenu(const QPoint&))); @@ -333,6 +334,7 @@ void CalendarWidget::updateViews() { } void CalendarWidget::updateEventList() { + mEventTable->setSortingEnabled(false); mEventTable->setRowCount(0); const auto& events = CalendarData::instance()->getEvents(); @@ -384,6 +386,7 @@ void CalendarWidget::updateEventList() { mEventTable->setItem(row, 4, new QTableWidgetItem(calName)); row++; } + mEventTable->setSortingEnabled(true); } static QColor blendColors(const QColor& color1, const QColor& color2, qreal ratio) { @@ -706,9 +709,7 @@ void CalendarWidget::onEventSelected(int row, int col) { } } - if (!canEdit) return; - - EventDialog dlg(eventId, QDateTime::currentDateTime(), this); + EventDialog dlg(eventId, QDateTime::currentDateTime(), this, !canEdit); if (dlg.exec() == QDialog::Accepted) { refreshData(); } @@ -911,12 +912,18 @@ void CalendarWidget::onEventTableContextMenu(const QPoint& pos) { } QMenu menu(this); + QAction* viewAct = menu.addAction(tr("View Event")); QAction* editAct = menu.addAction(tr("Edit Event")); editAct->setEnabled(canEdit); QAction* selectedAct = menu.exec(mEventTable->viewport()->mapToGlobal(pos)); - if (selectedAct == editAct && canEdit) { - EventDialog dlg(eventId, QDateTime::currentDateTime(), this); + if (selectedAct == viewAct) { + EventDialog dlg(eventId, QDateTime::currentDateTime(), this, !canEdit); + if (dlg.exec() == QDialog::Accepted) { + refreshData(); + } + } else if (selectedAct == editAct && canEdit) { + EventDialog dlg(eventId, QDateTime::currentDateTime(), this, false); if (dlg.exec() == QDialog::Accepted) { refreshData(); } diff --git a/retroshare-gui/src/gui/msgs/EventDialog.cpp b/retroshare-gui/src/gui/msgs/EventDialog.cpp index 922ad18c5..bd93ff0e5 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.cpp +++ b/retroshare-gui/src/gui/msgs/EventDialog.cpp @@ -1,4 +1,6 @@ #include "gui/msgs/EventDialog.h" +#include +#include "retroshare/rsgxsflags.h" #include #include #include @@ -14,15 +16,23 @@ #include #include #include +#include +#include +#include +#include +#include +#include "gui/RetroShareLink.h" +#include "gui/common/FriendSelectionWidget.h" +#include -EventDialog::EventDialog(const QString& eventId, const QDateTime& startInfo, QWidget* parent) - : QDialog(parent), mEventId(eventId), mDefaultStart(startInfo) +EventDialog::EventDialog(const QString& eventId, const QDateTime& startInfo, QWidget* parent, bool readOnly) + : QDialog(parent), mEventId(eventId), mDefaultStart(startInfo), mReadOnly(readOnly) { - setWindowTitle(mEventId.isEmpty() ? tr("New Event") : tr("Edit Event")); setMinimumSize(500, 600); buildUi(); loadEvent(); + updateModeUi(); } EventDialog::~EventDialog() {} @@ -32,28 +42,31 @@ void EventDialog::buildUi() { mainLayout->setContentsMargins(15, 15, 15, 15); mainLayout->setSpacing(10); - // Top action bar (Save, Close, Delete) - QHBoxLayout* actionLayout = new QHBoxLayout(); - QPushButton* saveBtn = new QPushButton(tr("Save and Close"), this); - saveBtn->setIcon(QIcon(":/icons/mail/compose.png")); - connect(saveBtn, SIGNAL(clicked()), this, SLOT(onSaveAndClose())); - actionLayout->addWidget(saveBtn); + // Top action bar (Save, Invite, Delete) + mActionWidget = new QWidget(this); + QHBoxLayout* actionLayout = new QHBoxLayout(mActionWidget); + actionLayout->setContentsMargins(0, 0, 0, 0); - QPushButton* inviteBtn = new QPushButton(tr("Invite Attendees"), this); - connect(inviteBtn, SIGNAL(clicked()), this, SLOT(onInviteAttendees())); - actionLayout->addWidget(inviteBtn); + mSaveBtn = new QPushButton(tr("Save and Close"), this); + mSaveBtn->setIcon(QIcon(":/icons/mail/compose.png")); + connect(mSaveBtn, SIGNAL(clicked()), this, SLOT(onSaveAndClose())); + actionLayout->addWidget(mSaveBtn); - QPushButton* deleteBtn = new QPushButton(tr("Delete"), this); - deleteBtn->setIcon(QIcon(":/icons/mail/delete.png")); - connect(deleteBtn, SIGNAL(clicked()), this, SLOT(onDelete())); - actionLayout->addWidget(deleteBtn); + mInviteBtn = new QPushButton(tr("Invite Attendees"), this); + connect(mInviteBtn, SIGNAL(clicked()), this, SLOT(onInviteAttendees())); + actionLayout->addWidget(mInviteBtn); + + mDeleteBtn = new QPushButton(tr("Delete"), this); + mDeleteBtn->setIcon(QIcon(":/icons/mail/delete.png")); + connect(mDeleteBtn, SIGNAL(clicked()), this, SLOT(onDelete())); + actionLayout->addWidget(mDeleteBtn); if (mEventId.isEmpty()) { - deleteBtn->setEnabled(false); + mDeleteBtn->setEnabled(false); } actionLayout->addStretch(); - mainLayout->addLayout(actionLayout); + mainLayout->addWidget(mActionWidget); // Form inputs layout QFormLayout* formLayout = new QFormLayout(); @@ -110,28 +123,125 @@ void EventDialog::buildUi() { // Attendees Tab mAttendeesList = new QListWidget(this); - QMap contacts = CalendarData::getContacts(); - for (auto it = contacts.begin(); it != contacts.end(); ++it) { - QListWidgetItem* item = new QListWidgetItem(it.value(), mAttendeesList); - item->setData(Qt::UserRole, it.key()); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(Qt::Unchecked); - } tabWidget->addTab(mAttendeesList, tr("Attendees")); // Attachments Tab QWidget* attachTab = new QWidget(this); QVBoxLayout* attachLayout = new QVBoxLayout(attachTab); mAttachmentsList = new QListWidget(this); - attachLayout->addWidget(mAttachmentsList); - QPushButton* addAttachBtn = new QPushButton(tr("Attach File..."), this); - connect(addAttachBtn, &QPushButton::clicked, [this]() { - QString file = QFileDialog::getOpenFileName(this, tr("Select File")); - if (!file.isEmpty()) { - mAttachmentsList->addItem(QFileInfo(file).fileName()); + mAttachmentsList->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(mAttachmentsList, &QListWidget::customContextMenuRequested, [this](const QPoint& pos) { + QListWidgetItem* item = mAttachmentsList->itemAt(pos); + if (!item) return; + + QMenu menu(this); + QAction* downloadAction = menu.addAction(QIcon(":/icons/png/download.png"), tr("Download")); + QAction* downloadAllAction = menu.addAction(QIcon(":/icons/mail/downloadall.png"), tr("Download all")); + QAction* removeAction = nullptr; + + if (!mReadOnly) { + menu.addSeparator(); + removeAction = menu.addAction(QIcon(":/icons/mail/delete.png"), tr("Remove Attachment")); + } + + QAction* selectedAction = menu.exec(mAttachmentsList->mapToGlobal(pos)); + if (selectedAction == downloadAction) { + QString att = item->data(Qt::UserRole).toString(); + if (!att.isEmpty()) { + RetroShareLink link(att); + if (link.valid() && (link.type() == RetroShareLink::TYPE_FILE || link.type() == RetroShareLink::TYPE_FILE_TREE)) { + QList links; + links.append(link); + RetroShareLink::process(links); + } else if (QFileInfo::exists(att)) { + QString targetPath = QFileDialog::getSaveFileName(this, tr("Save Attachment As"), QFileInfo(att).fileName()); + if (!targetPath.isEmpty()) { + if (QFile::exists(targetPath)) { + QFile::remove(targetPath); + } + if (!QFile::copy(att, targetPath)) { + QMessageBox::warning(this, tr("Download Failed"), tr("Could not save the file to %1").arg(targetPath)); + } + } + } else { + QUrl url(att); + if (!url.scheme().isEmpty()) { + QDesktopServices::openUrl(url); + } + } + } + } else if (selectedAction == downloadAllAction) { + QList rsLinks; + QStringList localFiles; + for (int i = 0; i < mAttachmentsList->count(); ++i) { + QString att = mAttachmentsList->item(i)->data(Qt::UserRole).toString(); + if (att.isEmpty()) continue; + RetroShareLink link(att); + if (link.valid() && (link.type() == RetroShareLink::TYPE_FILE || link.type() == RetroShareLink::TYPE_FILE_TREE)) { + rsLinks.append(link); + } else if (QFileInfo::exists(att)) { + localFiles.append(att); + } else { + QUrl url(att); + if (!url.scheme().isEmpty()) { + QDesktopServices::openUrl(url); + } + } + } + if (!rsLinks.isEmpty()) { + RetroShareLink::process(rsLinks); + } + if (!localFiles.isEmpty()) { + QString targetDir = QFileDialog::getExistingDirectory(this, tr("Select Directory to Save Attachments")); + if (!targetDir.isEmpty()) { + bool success = true; + QStringList failedFiles; + for (const QString& file : localFiles) { + QFileInfo fi(file); + QString targetPath = QDir(targetDir).filePath(fi.fileName()); + if (QFile::exists(targetPath)) { + QFile::remove(targetPath); + } + if (!QFile::copy(file, targetPath)) { + success = false; + failedFiles.append(fi.fileName()); + } + } + if (!success) { + QMessageBox::warning(this, tr("Download Failed"), tr("Could not save the following files: %1").arg(failedFiles.join(", "))); + } + } + } + } else if (removeAction && selectedAction == removeAction) { + delete mAttachmentsList->takeItem(mAttachmentsList->row(item)); } }); - attachLayout->addWidget(addAttachBtn); + + connect(mAttachmentsList, &QListWidget::itemDoubleClicked, [](QListWidgetItem* item) { + QString pathOrUrl = item->data(Qt::UserRole).toString(); + if (!pathOrUrl.isEmpty()) { + QUrl url(pathOrUrl); + if (url.scheme().isEmpty()) { + url = QUrl::fromLocalFile(pathOrUrl); + } + QDesktopServices::openUrl(url); + } + }); + + attachLayout->addWidget(mAttachmentsList); + mAddAttachBtn = new QPushButton(tr("Attach File..."), this); + connect(mAddAttachBtn, &QPushButton::clicked, [this]() { + QStringList files = QFileDialog::getOpenFileNames(this, tr("Select File(s)")); + for (const QString& file : files) { + if (!file.isEmpty()) { + QListWidgetItem* item = new QListWidgetItem(QFileInfo(file).fileName(), mAttachmentsList); + item->setData(Qt::UserRole, file); + item->setToolTip(file); + } + } + }); + attachLayout->addWidget(mAddAttachBtn); tabWidget->addTab(attachTab, tr("Attachments")); mainLayout->addWidget(tabWidget); @@ -149,6 +259,23 @@ void EventDialog::buildUi() { bottomCheckLayout->addWidget(mDisallowCheck); mainLayout->addLayout(bottomCheckLayout); + + // Bottom Buttons (Close & Edit for read-only view mode) + mBottomButtonsWidget = new QWidget(this); + QHBoxLayout* bottomBtnLayout = new QHBoxLayout(mBottomButtonsWidget); + bottomBtnLayout->setContentsMargins(0, 0, 0, 0); + bottomBtnLayout->addStretch(); + + mEditBtn = new QPushButton(tr("Edit"), this); + mEditBtn->setStyleSheet("background-color: #0078d4; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold;"); + connect(mEditBtn, SIGNAL(clicked()), this, SLOT(onEditClicked())); + bottomBtnLayout->addWidget(mEditBtn); + + mCloseBtn = new QPushButton(tr("Close"), this); + connect(mCloseBtn, SIGNAL(clicked()), this, SLOT(reject())); + bottomBtnLayout->addWidget(mCloseBtn); + + mainLayout->addWidget(mBottomButtonsWidget); } void EventDialog::loadEvent() { @@ -174,14 +301,22 @@ void EventDialog::loadEvent() { mDescriptionEdit->setPlainText(ev.description); // Set attendees - for (int i = 0; i < mAttendeesList->count(); ++i) { - QListWidgetItem* item = mAttendeesList->item(i); - QString contactId = item->data(Qt::UserRole).toString(); - if (ev.attendees.contains(contactId)) { - item->setCheckState(Qt::Checked); - } else { - item->setCheckState(Qt::Unchecked); - } + mAttendeesList->clear(); + QMap contacts = CalendarData::getContacts(); + for (const auto& contactId : ev.attendees) { + QString name = contacts.value(contactId, contactId); + QListWidgetItem* item = new QListWidgetItem(name, mAttendeesList); + item->setData(Qt::UserRole, contactId); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Checked); + } + + // Load attachments + mAttachmentsList->clear(); + for (const auto& att : ev.attachments) { + QListWidgetItem* item = new QListWidgetItem(QFileInfo(att).fileName(), mAttachmentsList); + item->setData(Qt::UserRole, att); + item->setToolTip(att); } break; } @@ -199,7 +334,76 @@ void EventDialog::onAllDayToggled(bool checked) { } void EventDialog::onInviteAttendees() { - // Just switches to attendees tab + QDialog dialog(this); + dialog.setWindowTitle(tr("Invite Attendees")); + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QComboBox* filterCombo = new QComboBox(&dialog); + filterCombo->addItem(tr("All people")); + filterCombo->addItem(tr("My contacts")); +#ifdef RS_DIRECT_CHAT + filterCombo->addItem(tr("Friend Nodes")); +#endif + filterCombo->setCurrentIndex(0); + + FriendSelectionWidget* friendsWidget = new FriendSelectionWidget(&dialog); + friendsWidget->setHeaderText(tr("Select contacts to invite:")); + friendsWidget->setModus(FriendSelectionWidget::MODUS_MULTI); + friendsWidget->setShowType(FriendSelectionWidget::SHOW_GXS); + friendsWidget->start(); + + connect(filterCombo, static_cast(&QComboBox::currentIndexChanged), [friendsWidget](int index) { + switch (index) { + default: + case 0: + friendsWidget->setShowType(FriendSelectionWidget::SHOW_GXS); + break; + case 1: + friendsWidget->setShowType(FriendSelectionWidget::SHOW_CONTACTS); + break; +#ifdef RS_DIRECT_CHAT + case 2: + friendsWidget->setShowType(FriendSelectionWidget::SHOW_SSL); + break; +#endif + } + }); + + // Pre-select current attendees + std::set psids; + for (int i = 0; i < mAttendeesList->count(); ++i) { + QListWidgetItem* item = mAttendeesList->item(i); + if (item->checkState() == Qt::Checked) { + psids.insert(item->data(Qt::UserRole).toString().toStdString()); + } + } + friendsWidget->setSelectedIdsFromString(FriendSelectionWidget::IDTYPE_GPG, psids, false); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + layout->addWidget(filterCombo); + layout->addWidget(friendsWidget); + layout->addWidget(buttonBox); + + if (dialog.exec() == QDialog::Accepted) { + std::set selected; + friendsWidget->selectedIds(selected, false); + + mAttendeesList->clear(); + QMap contacts = CalendarData::getContacts(); + for (const auto& pgpId : selected) { + QString pgpIdStr = QString::fromStdString(pgpId.toStdString()); + QString name = contacts.value(pgpIdStr, pgpIdStr); + QListWidgetItem* item = new QListWidgetItem(name, mAttendeesList); + item->setData(Qt::UserRole, pgpIdStr); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Checked); + } + } + + // Switch to attendees tab QTabWidget* tabWidget = findChild(); if (tabWidget) { tabWidget->setCurrentIndex(1); // Attendees is index 1 @@ -236,6 +440,11 @@ void EventDialog::onSaveAndClose() { } } + // Get attachments + for (int i = 0; i < mAttachmentsList->count(); ++i) { + ev.attachments.append(mAttachmentsList->item(i)->data(Qt::UserRole).toString()); + } + if (mEventId.isEmpty()) { CalendarData::instance()->addEvent(ev); } else { @@ -259,3 +468,77 @@ void EventDialog::onDelete() { accept(); } } + +void EventDialog::onEditClicked() { + mReadOnly = false; + updateModeUi(); +} + +void EventDialog::updateModeUi() { + bool canEdit = false; + if (mEventId.isEmpty()) { + canEdit = true; + } else { + // Determine if user can edit this event (admin check for shared calendars) + QString calendarId; + const auto& events = CalendarData::instance()->getEvents(); + for (const auto& ev : events) { + if (ev.id == mEventId) { + calendarId = ev.calendarId; + break; + } + } + if (!calendarId.isEmpty()) { + const auto& cals = CalendarData::instance()->getCalendars(); + for (const auto& c : cals) { + if (c.id == calendarId) { + if (!c.onNetwork) { + canEdit = true; + } else if (rsGxsCalendar) { + std::list summaries; + if (rsGxsCalendar->getCalendarsSummaries(summaries)) { + RsGxsGroupId groupId(calendarId.toStdString()); + for (const auto& meta : summaries) { + if (meta.mGroupId == groupId) { + canEdit = IS_GROUP_ADMIN(meta.mSubscribeFlags); + break; + } + } + } + } + break; + } + } + } else { + canEdit = true; + } + } + + setWindowTitle(mReadOnly ? tr("View Event") : (mEventId.isEmpty() ? tr("New Event") : tr("Edit Event"))); + + // Top action bar is visible only in edit mode + mActionWidget->setVisible(!mReadOnly); + + // Bottom buttons are visible only in read-only mode + mBottomButtonsWidget->setVisible(mReadOnly); + mEditBtn->setVisible(canEdit); + + // Set read-only / enabled state of all fields + mCalendarCombo->setEnabled(!mReadOnly); + mTitleEdit->setReadOnly(mReadOnly); + mLocationEdit->setReadOnly(mReadOnly); + mCategoryCombo->setEnabled(!mReadOnly); + mAllDayCheck->setEnabled(!mReadOnly); + mStartEdit->setReadOnly(mReadOnly); + mEndEdit->setReadOnly(mReadOnly); + mRepeatCombo->setEnabled(!mReadOnly); + mReminderCombo->setEnabled(!mReadOnly); + mDescriptionEdit->setReadOnly(mReadOnly); + + mAttendeesList->setEnabled(!mReadOnly); + mAddAttachBtn->setVisible(!mReadOnly); + + mNotifyCheck->setEnabled(!mReadOnly); + mSeparateCheck->setEnabled(!mReadOnly); + mDisallowCheck->setEnabled(!mReadOnly); +} diff --git a/retroshare-gui/src/gui/msgs/EventDialog.h b/retroshare-gui/src/gui/msgs/EventDialog.h index 46c85adfa..0b29c79dd 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.h +++ b/retroshare-gui/src/gui/msgs/EventDialog.h @@ -17,7 +17,7 @@ class EventDialog : public QDialog { public: // Pass eventId to edit existing event, or empty string to create a new one. // If creating a new one, startInfo can specify the default start time. - EventDialog(const QString& eventId = "", const QDateTime& startInfo = QDateTime::currentDateTime(), QWidget* parent = nullptr); + EventDialog(const QString& eventId = "", const QDateTime& startInfo = QDateTime::currentDateTime(), QWidget* parent = nullptr, bool readOnly = false); ~EventDialog(); private slots: @@ -25,13 +25,21 @@ private slots: void onDelete(); void onAllDayToggled(bool checked); void onInviteAttendees(); + void onEditClicked(); private: void loadEvent(); void buildUi(); + void updateModeUi(); QString mEventId; QDateTime mDefaultStart; + bool mReadOnly; + + QWidget* mActionWidget; + QPushButton* mSaveBtn; + QPushButton* mInviteBtn; + QPushButton* mDeleteBtn; QComboBox* mCalendarCombo; QLineEdit* mTitleEdit; @@ -44,7 +52,13 @@ private: QComboBox* mReminderCombo; QTextEdit* mDescriptionEdit; QListWidget* mAttendeesList; + QListWidget* mAttachmentsList; + QPushButton* mAddAttachBtn; + + QWidget* mBottomButtonsWidget; + QPushButton* mEditBtn; + QPushButton* mCloseBtn; QCheckBox* mNotifyCheck; QCheckBox* mSeparateCheck; diff --git a/retroshare-gui/src/gui/msgs/TaskDialog.cpp b/retroshare-gui/src/gui/msgs/TaskDialog.cpp index fe795d88a..d8cfb4ae4 100644 --- a/retroshare-gui/src/gui/msgs/TaskDialog.cpp +++ b/retroshare-gui/src/gui/msgs/TaskDialog.cpp @@ -16,6 +16,11 @@ #include #include #include +#include +#include +#include +#include +#include "gui/RetroShareLink.h" TaskDialog::TaskDialog(const QString& taskId, QWidget* parent) : QDialog(parent), mTaskId(taskId) @@ -129,15 +134,114 @@ void TaskDialog::buildUi() { QWidget* attachTab = new QWidget(this); QVBoxLayout* attachLayout = new QVBoxLayout(attachTab); mAttachmentsList = new QListWidget(this); - attachLayout->addWidget(mAttachmentsList); - QPushButton* addAttachBtn = new QPushButton(tr("Attach File..."), this); - connect(addAttachBtn, &QPushButton::clicked, [this]() { - QString file = QFileDialog::getOpenFileName(this, tr("Select File")); - if (!file.isEmpty()) { - mAttachmentsList->addItem(QFileInfo(file).fileName()); + mAttachmentsList->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(mAttachmentsList, &QListWidget::customContextMenuRequested, [this](const QPoint& pos) { + QListWidgetItem* item = mAttachmentsList->itemAt(pos); + if (!item) return; + + QMenu menu(this); + QAction* downloadAction = menu.addAction(QIcon(":/icons/png/download.png"), tr("Download")); + QAction* downloadAllAction = menu.addAction(QIcon(":/icons/mail/downloadall.png"), tr("Download all")); + QAction* removeAction = menu.addAction(QIcon(":/icons/mail/delete.png"), tr("Remove Attachment")); + + QAction* selectedAction = menu.exec(mAttachmentsList->mapToGlobal(pos)); + if (selectedAction == downloadAction) { + QString att = item->data(Qt::UserRole).toString(); + if (!att.isEmpty()) { + RetroShareLink link(att); + if (link.valid() && (link.type() == RetroShareLink::TYPE_FILE || link.type() == RetroShareLink::TYPE_FILE_TREE)) { + QList links; + links.append(link); + RetroShareLink::process(links); + } else if (QFileInfo::exists(att)) { + QString targetPath = QFileDialog::getSaveFileName(this, tr("Save Attachment As"), QFileInfo(att).fileName()); + if (!targetPath.isEmpty()) { + if (QFile::exists(targetPath)) { + QFile::remove(targetPath); + } + if (!QFile::copy(att, targetPath)) { + QMessageBox::warning(this, tr("Download Failed"), tr("Could not save the file to %1").arg(targetPath)); + } + } + } else { + QUrl url(att); + if (!url.scheme().isEmpty()) { + QDesktopServices::openUrl(url); + } + } + } + } else if (selectedAction == downloadAllAction) { + QList rsLinks; + QStringList localFiles; + for (int i = 0; i < mAttachmentsList->count(); ++i) { + QString att = mAttachmentsList->item(i)->data(Qt::UserRole).toString(); + if (att.isEmpty()) continue; + RetroShareLink link(att); + if (link.valid() && (link.type() == RetroShareLink::TYPE_FILE || link.type() == RetroShareLink::TYPE_FILE_TREE)) { + rsLinks.append(link); + } else if (QFileInfo::exists(att)) { + localFiles.append(att); + } else { + QUrl url(att); + if (!url.scheme().isEmpty()) { + QDesktopServices::openUrl(url); + } + } + } + if (!rsLinks.isEmpty()) { + RetroShareLink::process(rsLinks); + } + if (!localFiles.isEmpty()) { + QString targetDir = QFileDialog::getExistingDirectory(this, tr("Select Directory to Save Attachments")); + if (!targetDir.isEmpty()) { + bool success = true; + QStringList failedFiles; + for (const QString& file : localFiles) { + QFileInfo fi(file); + QString targetPath = QDir(targetDir).filePath(fi.fileName()); + if (QFile::exists(targetPath)) { + QFile::remove(targetPath); + } + if (!QFile::copy(file, targetPath)) { + success = false; + failedFiles.append(fi.fileName()); + } + } + if (!success) { + QMessageBox::warning(this, tr("Download Failed"), tr("Could not save the following files: %1").arg(failedFiles.join(", "))); + } + } + } + } else if (selectedAction == removeAction) { + delete mAttachmentsList->takeItem(mAttachmentsList->row(item)); } }); - attachLayout->addWidget(addAttachBtn); + + connect(mAttachmentsList, &QListWidget::itemDoubleClicked, [](QListWidgetItem* item) { + QString pathOrUrl = item->data(Qt::UserRole).toString(); + if (!pathOrUrl.isEmpty()) { + QUrl url(pathOrUrl); + if (url.scheme().isEmpty()) { + url = QUrl::fromLocalFile(pathOrUrl); + } + QDesktopServices::openUrl(url); + } + }); + + attachLayout->addWidget(mAttachmentsList); + mAddAttachBtn = new QPushButton(tr("Attach File..."), this); + connect(mAddAttachBtn, &QPushButton::clicked, [this]() { + QStringList files = QFileDialog::getOpenFileNames(this, tr("Select File(s)")); + for (const QString& file : files) { + if (!file.isEmpty()) { + QListWidgetItem* item = new QListWidgetItem(QFileInfo(file).fileName(), mAttachmentsList); + item->setData(Qt::UserRole, file); + item->setToolTip(file); + } + } + }); + attachLayout->addWidget(mAddAttachBtn); tabWidget->addTab(attachTab, tr("Attachments")); mainLayout->addWidget(tabWidget); @@ -169,6 +273,14 @@ void TaskDialog::loadTask() { mRepeatCombo->setCurrentText(t.repeat); mReminderCombo->setCurrentText(t.reminder); mDescriptionEdit->setPlainText(t.description); + + // Load attachments + mAttachmentsList->clear(); + for (const auto& att : t.attachments) { + QListWidgetItem* item = new QListWidgetItem(QFileInfo(att).fileName(), mAttachmentsList); + item->setData(Qt::UserRole, att); + item->setToolTip(att); + } break; } } @@ -209,6 +321,11 @@ void TaskDialog::onSaveAndClose() { t.percentComplete = 100; } + // Get attachments + for (int i = 0; i < mAttachmentsList->count(); ++i) { + t.attachments.append(mAttachmentsList->item(i)->data(Qt::UserRole).toString()); + } + if (mTaskId.isEmpty()) { CalendarData::instance()->addTask(t); } else { diff --git a/retroshare-gui/src/gui/msgs/TaskDialog.h b/retroshare-gui/src/gui/msgs/TaskDialog.h index 774724479..7cae6074b 100644 --- a/retroshare-gui/src/gui/msgs/TaskDialog.h +++ b/retroshare-gui/src/gui/msgs/TaskDialog.h @@ -11,6 +11,7 @@ class QDateTimeEdit; class QTextEdit; class QSpinBox; class QListWidget; +class QPushButton; class TaskDialog : public QDialog { Q_OBJECT @@ -44,6 +45,7 @@ private: QComboBox* mReminderCombo; QTextEdit* mDescriptionEdit; QListWidget* mAttachmentsList; + QPushButton* mAddAttachBtn; }; #endif // TASKDIALOG_H From e5f7542ec3048d6d4c148dce7c1115c7765abdb6 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:17:12 +0200 Subject: [PATCH 23/26] Added invite feature --- retroshare-gui/src/gui/msgs/EventDialog.cpp | 311 ++++++++++++++++++-- retroshare-gui/src/gui/msgs/EventDialog.h | 4 +- 2 files changed, 283 insertions(+), 32 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/EventDialog.cpp b/retroshare-gui/src/gui/msgs/EventDialog.cpp index bd93ff0e5..df9582de4 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.cpp +++ b/retroshare-gui/src/gui/msgs/EventDialog.cpp @@ -11,6 +11,10 @@ #include #include #include +#include +#include +#include "gui/gxs/GxsIdTreeWidgetItem.h" +#include "gui/gxs/GxsIdDetails.h" #include #include #include @@ -24,6 +28,65 @@ #include "gui/RetroShareLink.h" #include "gui/common/FriendSelectionWidget.h" #include +#include +#include +#include +#include "gui/common/PeerDefs.h" +#include +#include "gui/common/AvatarDefs.h" +#include +#include + +namespace { +QString getContactName(const QString& idStr) { + std::string str = idStr.toStdString(); + if (str.length() == 16) { + RsPgpId pgpId(str); + QString name; + PeerDefs::rsidFromId(pgpId, &name); + return name; + } else if (str.length() == 32) { + RsGxsId gxsId(str); + RsIdentityDetails details; + if (rsIdentity && rsIdentity->getIdDetails(gxsId, details)) { + return QString::fromUtf8(details.mNickname.c_str()); + } + RsPeerId peerId(str); + std::string peerName = rsPeers ? rsPeers->getPeerName(peerId) : ""; + if (!peerName.empty()) { + return QString::fromUtf8(peerName.c_str()); + } + QString name; + PeerDefs::rsidFromId(peerId, &name); + if (name != QCoreApplication::translate("PeerDefs", "Unknown")) { + return name; + } + PeerDefs::rsidFromId(gxsId, &name); + return name; + } + return idStr; +} + +QIcon getContactAvatar(const QString& idStr) { + std::string str = idStr.toStdString(); + QPixmap pixmap; + if (str.length() == 16) { + AvatarDefs::getAvatarFromGpgId(RsPgpId(str), pixmap); + } else if (str.length() == 32) { + RsGxsId gxsId(str); + RsIdentityDetails details; + if (rsIdentity && rsIdentity->getIdDetails(gxsId, details)) { + AvatarDefs::getAvatarFromGxsId(gxsId, pixmap); + } else { + AvatarDefs::getAvatarFromSslId(RsPeerId(str), pixmap); + } + } + if (pixmap.isNull()) { + pixmap = QPixmap(AVATAR_DEFAULT_IMAGE_SQUARE); + } + return QIcon(pixmap); +} +} EventDialog::EventDialog(const QString& eventId, const QDateTime& startInfo, QWidget* parent, bool readOnly) : QDialog(parent), mEventId(eventId), mDefaultStart(startInfo), mReadOnly(readOnly) @@ -122,7 +185,10 @@ void EventDialog::buildUi() { tabWidget->addTab(mDescriptionEdit, tr("Description")); // Attendees Tab - mAttendeesList = new QListWidget(this); + mAttendeesList = new QTreeWidget(this); + mAttendeesList->setHeaderHidden(true); + mAttendeesList->setIconSize(QSize(32, 32)); + mAttendeesList->setRootIsDecorated(false); tabWidget->addTab(mAttendeesList, tr("Attendees")); // Attachments Tab @@ -302,13 +368,42 @@ void EventDialog::loadEvent() { // Set attendees mAttendeesList->clear(); - QMap contacts = CalendarData::getContacts(); for (const auto& contactId : ev.attendees) { - QString name = contacts.value(contactId, contactId); - QListWidgetItem* item = new QListWidgetItem(name, mAttendeesList); - item->setData(Qt::UserRole, contactId); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(Qt::Checked); + if (contactId.length() == 16) { + QString name = getContactName(contactId); + QTreeWidgetItem* item = new QTreeWidgetItem(mAttendeesList); + item->setIcon(0, getContactAvatar(contactId)); + item->setText(0, name); + item->setData(0, Qt::UserRole, contactId); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Checked); + } else if (contactId.length() == 32) { + RsGxsId gxsId(contactId.toStdString()); + RsPeerId peerId(contactId.toStdString()); + bool isSsl = false; + if (rsPeers) { + std::string peerName = rsPeers->getPeerName(peerId); + if (!peerName.empty() || rsPeers->isFriend(peerId)) { + isSsl = true; + } + } + if (isSsl) { + QString name = getContactName(contactId); + QTreeWidgetItem* item = new QTreeWidgetItem(mAttendeesList); + item->setIcon(0, getContactAvatar(contactId)); + item->setText(0, name); + item->setData(0, Qt::UserRole, contactId); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Checked); + } else { + // Treat as GXS ID + GxsIdRSTreeWidgetItem* item = new GxsIdRSTreeWidgetItem(nullptr, GxsIdDetails::ICON_TYPE_AVATAR, true, mAttendeesList); + item->setData(0, Qt::UserRole, contactId); + item->setId(gxsId, 0, true); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Checked); + } + } } // Load attachments @@ -336,6 +431,7 @@ void EventDialog::onAllDayToggled(bool checked) { void EventDialog::onInviteAttendees() { QDialog dialog(this); dialog.setWindowTitle(tr("Invite Attendees")); + dialog.resize(this->width(), 500); QVBoxLayout* layout = new QVBoxLayout(&dialog); QComboBox* filterCombo = new QComboBox(&dialog); @@ -348,7 +444,7 @@ void EventDialog::onInviteAttendees() { FriendSelectionWidget* friendsWidget = new FriendSelectionWidget(&dialog); friendsWidget->setHeaderText(tr("Select contacts to invite:")); - friendsWidget->setModus(FriendSelectionWidget::MODUS_MULTI); + friendsWidget->setModus(FriendSelectionWidget::MODUS_CHECK); friendsWidget->setShowType(FriendSelectionWidget::SHOW_GXS); friendsWidget->start(); @@ -370,14 +466,35 @@ void EventDialog::onInviteAttendees() { }); // Pre-select current attendees - std::set psids; - for (int i = 0; i < mAttendeesList->count(); ++i) { - QListWidgetItem* item = mAttendeesList->item(i); - if (item->checkState() == Qt::Checked) { - psids.insert(item->data(Qt::UserRole).toString().toStdString()); + std::set psidsGpg; + std::set psidsGxs; + std::set psidsSsl; + for (int i = 0; i < mAttendeesList->topLevelItemCount(); ++i) { + QTreeWidgetItem* item = mAttendeesList->topLevelItem(i); + if (item->checkState(0) == Qt::Checked) { + std::string idStr = item->data(0, Qt::UserRole).toString().toStdString(); + if (idStr.length() == 16) { + psidsGpg.insert(idStr); + } else if (idStr.length() == 32) { + RsPeerId peerId(idStr); + bool isSsl = false; + if (rsPeers) { + std::string peerName = rsPeers->getPeerName(peerId); + if (!peerName.empty() || rsPeers->isFriend(peerId)) { + isSsl = true; + } + } + if (isSsl) { + psidsSsl.insert(idStr); + } else { + psidsGxs.insert(idStr); + } + } } } - friendsWidget->setSelectedIdsFromString(FriendSelectionWidget::IDTYPE_GPG, psids, false); + friendsWidget->setSelectedIdsFromString(FriendSelectionWidget::IDTYPE_GPG, psidsGpg, false); + friendsWidget->setSelectedIdsFromString(FriendSelectionWidget::IDTYPE_GXS, psidsGxs, false); + friendsWidget->setSelectedIdsFromString(FriendSelectionWidget::IDTYPE_SSL, psidsSsl, false); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); @@ -387,20 +504,68 @@ void EventDialog::onInviteAttendees() { layout->addWidget(friendsWidget); layout->addWidget(buttonBox); - if (dialog.exec() == QDialog::Accepted) { - std::set selected; - friendsWidget->selectedIds(selected, false); + while (dialog.exec() == QDialog::Accepted) { + std::set selectedGpg; + friendsWidget->selectedIds(selectedGpg, false); + + std::set selectedGxs; + friendsWidget->selectedIds(selectedGxs, false); + + std::set selectedSsl; + friendsWidget->selectedIds(selectedSsl, false); + + int totalCount = 0; + for (const auto& id : selectedGpg) { + if (QString::fromStdString(id.toStdString()) != "0000000000000000") totalCount++; + } + for (const auto& id : selectedGxs) { + if (QString::fromStdString(id.toStdString()) != "00000000000000000000000000000000") totalCount++; + } + for (const auto& id : selectedSsl) { + if (QString::fromStdString(id.toStdString()) != "00000000000000000000000000000000") totalCount++; + } + + if (totalCount > 20) { + QMessageBox::warning(this, tr("Limit Exceeded"), tr("You can select a maximum of 20 attendees. Currently selected: %1").arg(totalCount)); + continue; + } mAttendeesList->clear(); - QMap contacts = CalendarData::getContacts(); - for (const auto& pgpId : selected) { + + for (const auto& pgpId : selectedGpg) { QString pgpIdStr = QString::fromStdString(pgpId.toStdString()); - QString name = contacts.value(pgpIdStr, pgpIdStr); - QListWidgetItem* item = new QListWidgetItem(name, mAttendeesList); - item->setData(Qt::UserRole, pgpIdStr); + if (pgpIdStr == "0000000000000000") continue; + QString name = getContactName(pgpIdStr); + QTreeWidgetItem* item = new QTreeWidgetItem(mAttendeesList); + item->setIcon(0, getContactAvatar(pgpIdStr)); + item->setText(0, name); + item->setData(0, Qt::UserRole, pgpIdStr); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(Qt::Checked); + item->setCheckState(0, Qt::Checked); } + + for (const auto& gxsId : selectedGxs) { + QString gxsIdStr = QString::fromStdString(gxsId.toStdString()); + if (gxsIdStr == "00000000000000000000000000000000") continue; + GxsIdRSTreeWidgetItem* item = new GxsIdRSTreeWidgetItem(nullptr, GxsIdDetails::ICON_TYPE_AVATAR, true, mAttendeesList); + item->setData(0, Qt::UserRole, gxsIdStr); + item->setId(gxsId, 0, true); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Checked); + } + + for (const auto& sslId : selectedSsl) { + QString sslIdStr = QString::fromStdString(sslId.toStdString()); + if (sslIdStr == "00000000000000000000000000000000") continue; + QString name = getContactName(sslIdStr); + QTreeWidgetItem* item = new QTreeWidgetItem(mAttendeesList); + item->setIcon(0, getContactAvatar(sslIdStr)); + item->setText(0, name); + item->setData(0, Qt::UserRole, sslIdStr); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, Qt::Checked); + } + break; } // Switch to attendees tab @@ -432,14 +597,19 @@ void EventDialog::onSaveAndClose() { // Get checked attendees QStringList invitedNames; - for (int i = 0; i < mAttendeesList->count(); ++i) { - QListWidgetItem* item = mAttendeesList->item(i); - if (item->checkState() == Qt::Checked) { - ev.attendees.append(item->data(Qt::UserRole).toString()); - invitedNames.append(item->text()); + for (int i = 0; i < mAttendeesList->topLevelItemCount(); ++i) { + QTreeWidgetItem* item = mAttendeesList->topLevelItem(i); + if (item->checkState(0) == Qt::Checked) { + ev.attendees.append(item->data(0, Qt::UserRole).toString()); + invitedNames.append(item->text(0)); } } + if (ev.attendees.size() > 20) { + QMessageBox::warning(this, tr("Limit Exceeded"), tr("You can select a maximum of 20 attendees. Please uncheck some.")); + return; + } + // Get attachments for (int i = 0; i < mAttachmentsList->count(); ++i) { ev.attachments.append(mAttachmentsList->item(i)->data(Qt::UserRole).toString()); @@ -451,10 +621,9 @@ void EventDialog::onSaveAndClose() { CalendarData::instance()->updateEvent(ev); } - // Mock invitation mailing + // Send actual invitations if (mNotifyCheck->isChecked() && !invitedNames.isEmpty()) { - QMessageBox::information(this, tr("Invitations Sent"), - tr("Invitations successfully sent to: %1").arg(invitedNames.join(", "))); + sendInvite(ev, invitedNames); } accept(); @@ -542,3 +711,83 @@ void EventDialog::updateModeUi() { mSeparateCheck->setEnabled(!mReadOnly); mDisallowCheck->setEnabled(!mReadOnly); } + +void EventDialog::sendInvite(const CalendarEvent& ev, const QStringList& invitedNames) { + bool at_least_one_gxsid = false; + std::set destinations; + + for (const auto& contactId : ev.attendees) { + std::string idStr = contactId.toStdString(); + if (idStr.length() == 16) { + RsPgpId pgpId(idStr); + std::list sslIds; + if (rsPeers) { + rsPeers->getAssociatedSSLIds(pgpId, sslIds); + for (const auto& sslId : sslIds) { + destinations.insert(Rs::Mail::MsgAddress(sslId, Rs::Mail::MsgAddress::AddressMode::MSG_ADDRESS_MODE_TO)); + } + } + } else if (idStr.length() == 32) { + RsPeerId peerId(idStr); + bool isSsl = false; + if (rsPeers) { + std::string peerName = rsPeers->getPeerName(peerId); + if (!peerName.empty() || rsPeers->isFriend(peerId)) { + isSsl = true; + } + } + if (isSsl) { + destinations.insert(Rs::Mail::MsgAddress(peerId, Rs::Mail::MsgAddress::AddressMode::MSG_ADDRESS_MODE_TO)); + } else { + destinations.insert(Rs::Mail::MsgAddress(RsGxsId(idStr), Rs::Mail::MsgAddress::AddressMode::MSG_ADDRESS_MODE_TO)); + at_least_one_gxsid = true; + } + } + } + + if (destinations.empty()) { + return; + } + + Rs::Mail::MessageInfo mi; + mi.destinations = destinations; + mi.title = (tr("Invitation: %1").arg(ev.title)).toUtf8().constData(); + + // Construct invitation HTML message body + QString body; + body += "

" + tr("You are invited to a calendar event:") + "

"; + body += ""; + body += ""; + if (!ev.location.isEmpty()) { + body += ""; + } + body += ""; + if (!ev.description.isEmpty()) { + body += ""; + } + body += "
" + tr("Title:") + "" + ev.title + "
" + tr("Location:") + "" + ev.location + "
" + tr("Time:") + "" + ev.start.toString("yyyy-MM-dd hh:mm") + " - " + ev.end.toString("yyyy-MM-dd hh:mm") + "
" + tr("Description:") + "" + QString(ev.description).replace("\n", "
") + "
"; + mi.msg = body.toUtf8().constData(); + + if (!at_least_one_gxsid) { + if (rsPeers) { + mi.from = Rs::Mail::MsgAddress(rsPeers->getOwnId(), Rs::Mail::MsgAddress::AddressMode::MSG_ADDRESS_MODE_TO); + } + } else { + std::list own_ids; + if (rsIdentity) { + rsIdentity->getOwnIds(own_ids); + } + if (own_ids.empty()) { + QMessageBox::warning(this, tr("RetroShare"), tr("Please create an identity to sign distant messages, or remove GXS contacts from the attendee list."), QMessageBox::Ok); + return; + } + mi.from = Rs::Mail::MsgAddress(own_ids.front(), Rs::Mail::MsgAddress::AddressMode::MSG_ADDRESS_MODE_TO); + } + + if (rsMail && rsMail->MessageSend(mi)) { + QMessageBox::information(this, tr("Invitations Sent"), + tr("Invitations successfully sent to: %1").arg(invitedNames.join(", "))); + } else { + QMessageBox::warning(this, tr("Sending Failed"), tr("Failed to send invitations.")); + } +} diff --git a/retroshare-gui/src/gui/msgs/EventDialog.h b/retroshare-gui/src/gui/msgs/EventDialog.h index 0b29c79dd..4e7064bba 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.h +++ b/retroshare-gui/src/gui/msgs/EventDialog.h @@ -10,6 +10,7 @@ class QCheckBox; class QDateTimeEdit; class QTextEdit; class QListWidget; +class QTreeWidget; class QTabWidget; class EventDialog : public QDialog { @@ -31,6 +32,7 @@ private: void loadEvent(); void buildUi(); void updateModeUi(); + void sendInvite(const CalendarEvent& ev, const QStringList& invitedNames); QString mEventId; QDateTime mDefaultStart; @@ -51,7 +53,7 @@ private: QComboBox* mRepeatCombo; QComboBox* mReminderCombo; QTextEdit* mDescriptionEdit; - QListWidget* mAttendeesList; + QTreeWidget* mAttendeesList; QListWidget* mAttachmentsList; QPushButton* mAddAttachBtn; From 437179b9f5d2c079b6ca8bea595206b97b7b49fa Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:46:12 +0200 Subject: [PATCH 24/26] Removed not needed checkboxes --- retroshare-gui/src/gui/msgs/EventDialog.cpp | 28 +++++++++++++++------ retroshare-gui/src/gui/msgs/EventDialog.h | 22 ++++++++++++++-- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/retroshare-gui/src/gui/msgs/EventDialog.cpp b/retroshare-gui/src/gui/msgs/EventDialog.cpp index df9582de4..ec13c5653 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.cpp +++ b/retroshare-gui/src/gui/msgs/EventDialog.cpp @@ -1,3 +1,23 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/EventDialog.cpp * + * * + * Copyright (C) 2026 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + #include "gui/msgs/EventDialog.h" #include #include "retroshare/rsgxsflags.h" @@ -318,12 +338,6 @@ void EventDialog::buildUi() { mNotifyCheck->setChecked(true); bottomCheckLayout->addWidget(mNotifyCheck); - mSeparateCheck = new QCheckBox(tr("Separate invitation per attendee"), this); - bottomCheckLayout->addWidget(mSeparateCheck); - - mDisallowCheck = new QCheckBox(tr("Disallow counter"), this); - bottomCheckLayout->addWidget(mDisallowCheck); - mainLayout->addLayout(bottomCheckLayout); // Bottom Buttons (Close & Edit for read-only view mode) @@ -708,8 +722,6 @@ void EventDialog::updateModeUi() { mAddAttachBtn->setVisible(!mReadOnly); mNotifyCheck->setEnabled(!mReadOnly); - mSeparateCheck->setEnabled(!mReadOnly); - mDisallowCheck->setEnabled(!mReadOnly); } void EventDialog::sendInvite(const CalendarEvent& ev, const QStringList& invitedNames) { diff --git a/retroshare-gui/src/gui/msgs/EventDialog.h b/retroshare-gui/src/gui/msgs/EventDialog.h index 4e7064bba..081f00bd0 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.h +++ b/retroshare-gui/src/gui/msgs/EventDialog.h @@ -1,3 +1,23 @@ +/******************************************************************************* + * retroshare-gui/src/gui/msgs/EventDialog.h * + * * + * Copyright (C) 2026 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + #ifndef EVENTDIALOG_H #define EVENTDIALOG_H @@ -63,8 +83,6 @@ private: QPushButton* mCloseBtn; QCheckBox* mNotifyCheck; - QCheckBox* mSeparateCheck; - QCheckBox* mDisallowCheck; }; #endif // EVENTDIALOG_H From 8900c9c9091e65512b32cdbdffc0bc6e51bb7a3f Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:19:17 +0200 Subject: [PATCH 25/26] Moved calendar sources into own folder Added calendar ifdefs --- retroshare-gui/CMakeLists.txt | 1 + retroshare-gui/src/CMakeLists.txt | 43 +++++++++++++------ .../gui/{msgs => calendar}/CalendarData.cpp | 2 +- .../src/gui/{msgs => calendar}/CalendarData.h | 0 .../CalendarPropertiesDialog.cpp | 2 +- .../CalendarPropertiesDialog.h | 2 +- .../gui/{msgs => calendar}/CalendarWidget.cpp | 6 +-- .../gui/{msgs => calendar}/CalendarWidget.h | 2 +- .../gui/{msgs => calendar}/CalendarWidget.ui | 0 .../gui/{msgs => calendar}/EventDialog.cpp | 2 +- .../src/gui/{msgs => calendar}/EventDialog.h | 2 +- .../src/gui/{msgs => calendar}/TaskDialog.cpp | 2 +- .../src/gui/{msgs => calendar}/TaskDialog.h | 2 +- .../gui/{msgs => calendar}/TasksWidget.cpp | 6 +-- .../src/gui/{msgs => calendar}/TasksWidget.h | 2 +- .../src/gui/{msgs => calendar}/TasksWidget.ui | 0 .../src/gui/msgs/MessagesDialog.cpp | 16 ++++++- retroshare-gui/src/gui/msgs/MessagesDialog.h | 6 +++ retroshare-gui/src/retroshare-gui.pro | 39 +++++++++++------ 19 files changed, 90 insertions(+), 45 deletions(-) rename retroshare-gui/src/gui/{msgs => calendar}/CalendarData.cpp (99%) rename retroshare-gui/src/gui/{msgs => calendar}/CalendarData.h (100%) rename retroshare-gui/src/gui/{msgs => calendar}/CalendarPropertiesDialog.cpp (99%) rename retroshare-gui/src/gui/{msgs => calendar}/CalendarPropertiesDialog.h (98%) rename retroshare-gui/src/gui/{msgs => calendar}/CalendarWidget.cpp (99%) rename retroshare-gui/src/gui/{msgs => calendar}/CalendarWidget.h (99%) rename retroshare-gui/src/gui/{msgs => calendar}/CalendarWidget.ui (100%) rename retroshare-gui/src/gui/{msgs => calendar}/EventDialog.cpp (99%) rename retroshare-gui/src/gui/{msgs => calendar}/EventDialog.h (98%) rename retroshare-gui/src/gui/{msgs => calendar}/TaskDialog.cpp (99%) rename retroshare-gui/src/gui/{msgs => calendar}/TaskDialog.h (96%) rename retroshare-gui/src/gui/{msgs => calendar}/TasksWidget.cpp (99%) rename retroshare-gui/src/gui/{msgs => calendar}/TasksWidget.h (98%) rename retroshare-gui/src/gui/{msgs => calendar}/TasksWidget.ui (100%) diff --git a/retroshare-gui/CMakeLists.txt b/retroshare-gui/CMakeLists.txt index dc4acfdec..904280042 100644 --- a/retroshare-gui/CMakeLists.txt +++ b/retroshare-gui/CMakeLists.txt @@ -62,6 +62,7 @@ option( RS_GXSCHANNELS "Enable GXS channels in GUI" ON ) option( RS_GXSFORUMS "Enable GXS forums in GUI" ON ) option( RS_GXSPOSTED "Enable GXS posted in GUI" ON ) option( RS_GXSCIRCLES "Enable GXS circles in GUI" ON ) +option( RS_USE_CALENDAR "Build with GXS Calendar support" ON) option( RS_GUI_CMARK "Enable CommonMark support in GUI" OFF ) set(RS_GXSIDENTITIES ON CACHE BOOL "Enable GXS identities in GUI" FORCE) set(RS_IDLE ON CACHE BOOL "Enable Idle support" FORCE) diff --git a/retroshare-gui/src/CMakeLists.txt b/retroshare-gui/src/CMakeLists.txt index 907e21f2c..ce3f02260 100644 --- a/retroshare-gui/src/CMakeLists.txt +++ b/retroshare-gui/src/CMakeLists.txt @@ -115,12 +115,6 @@ list( src/gui/connect/FriendRecommendDialog.cpp src/gui/msgs/MessagesDialog.cpp - src/gui/msgs/CalendarData.cpp - src/gui/msgs/CalendarWidget.cpp - src/gui/msgs/TasksWidget.cpp - src/gui/msgs/CalendarPropertiesDialog.cpp - src/gui/msgs/EventDialog.cpp - src/gui/msgs/TaskDialog.cpp src/gui/msgs/MessageComposer.cpp src/gui/msgs/MessageWidget.cpp src/gui/msgs/MessageWindow.cpp @@ -336,8 +330,6 @@ list( src/gui/msgs/MessageComposer.ui src/gui/msgs/MessageWindow.ui src/gui/msgs/MessageWidget.ui - src/gui/msgs/CalendarWidget.ui - src/gui/msgs/TasksWidget.ui src/gui/settings/settingsw.ui src/gui/settings/GeneralPage.ui @@ -552,12 +544,6 @@ list( src/gui/connect/FriendRecommendDialog.h src/gui/msgs/MessagesDialog.h - src/gui/msgs/CalendarData.h - src/gui/msgs/CalendarWidget.h - src/gui/msgs/TasksWidget.h - src/gui/msgs/CalendarPropertiesDialog.h - src/gui/msgs/EventDialog.h - src/gui/msgs/TaskDialog.h src/gui/msgs/MessageInterface.h src/gui/msgs/MessageComposer.h src/gui/msgs/MessageWindow.h @@ -725,6 +711,35 @@ if(RS_JSON_API) ) endif(RS_JSON_API) +if(RS_USE_CALENDAR) + add_definitions(-DRS_USE_CALENDAR) + list( + APPEND RS_GUI_SOURCES + src/gui/calendar/CalendarData.cpp + src/gui/calendar/CalendarWidget.cpp + src/gui/calendar/TasksWidget.cpp + src/gui/calendar/CalendarPropertiesDialog.cpp + src/gui/calendar/EventDialog.cpp + src/gui/calendar/TaskDialog.cpp + ) + + list( + APPEND RS_IMPLEMENTATION_HEADERS + src/gui/calendar/CalendarData.h + src/gui/calendar/CalendarWidget.h + src/gui/calendar/TasksWidget.h + src/gui/calendar/CalendarPropertiesDialog.h + src/gui/calendar/EventDialog.h + src/gui/calendar/TaskDialog.h + ) + + list( + APPEND RS_GUI_FORMS + src/gui/calendar/CalendarWidget.ui + src/gui/calendar/TasksWidget.ui + ) +endif(RS_USE_CALENDAR) + if(RS_WEBUI) list( APPEND RS_GUI_SOURCES diff --git a/retroshare-gui/src/gui/msgs/CalendarData.cpp b/retroshare-gui/src/gui/calendar/CalendarData.cpp similarity index 99% rename from retroshare-gui/src/gui/msgs/CalendarData.cpp rename to retroshare-gui/src/gui/calendar/CalendarData.cpp index 45c9effc2..36329c270 100644 --- a/retroshare-gui/src/gui/msgs/CalendarData.cpp +++ b/retroshare-gui/src/gui/calendar/CalendarData.cpp @@ -18,7 +18,7 @@ * * *******************************************************************************/ -#include "gui/msgs/CalendarData.h" +#include "gui/calendar/CalendarData.h" #include #include #include diff --git a/retroshare-gui/src/gui/msgs/CalendarData.h b/retroshare-gui/src/gui/calendar/CalendarData.h similarity index 100% rename from retroshare-gui/src/gui/msgs/CalendarData.h rename to retroshare-gui/src/gui/calendar/CalendarData.h diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp b/retroshare-gui/src/gui/calendar/CalendarPropertiesDialog.cpp similarity index 99% rename from retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp rename to retroshare-gui/src/gui/calendar/CalendarPropertiesDialog.cpp index e5821b209..f957fb38f 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp +++ b/retroshare-gui/src/gui/calendar/CalendarPropertiesDialog.cpp @@ -18,7 +18,7 @@ * * *******************************************************************************/ -#include "gui/msgs/CalendarPropertiesDialog.h" +#include "gui/calendar/CalendarPropertiesDialog.h" #include #include #include diff --git a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h b/retroshare-gui/src/gui/calendar/CalendarPropertiesDialog.h similarity index 98% rename from retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h rename to retroshare-gui/src/gui/calendar/CalendarPropertiesDialog.h index 5d3933445..ec3339774 100644 --- a/retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h +++ b/retroshare-gui/src/gui/calendar/CalendarPropertiesDialog.h @@ -23,7 +23,7 @@ #include #include -#include "gui/msgs/CalendarData.h" +#include "gui/calendar/CalendarData.h" class QStackedWidget; class QRadioButton; diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp b/retroshare-gui/src/gui/calendar/CalendarWidget.cpp similarity index 99% rename from retroshare-gui/src/gui/msgs/CalendarWidget.cpp rename to retroshare-gui/src/gui/calendar/CalendarWidget.cpp index 97a107ed1..3ce301dfe 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.cpp +++ b/retroshare-gui/src/gui/calendar/CalendarWidget.cpp @@ -18,9 +18,9 @@ * * *******************************************************************************/ -#include "gui/msgs/CalendarWidget.h" -#include "gui/msgs/EventDialog.h" -#include "gui/msgs/CalendarPropertiesDialog.h" +#include "gui/calendar/CalendarWidget.h" +#include "gui/calendar/EventDialog.h" +#include "gui/calendar/CalendarPropertiesDialog.h" #include #include #include "retroshare/rsgxsflags.h" diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.h b/retroshare-gui/src/gui/calendar/CalendarWidget.h similarity index 99% rename from retroshare-gui/src/gui/msgs/CalendarWidget.h rename to retroshare-gui/src/gui/calendar/CalendarWidget.h index 70beaecfe..4c2d3cba7 100644 --- a/retroshare-gui/src/gui/msgs/CalendarWidget.h +++ b/retroshare-gui/src/gui/calendar/CalendarWidget.h @@ -26,7 +26,7 @@ #include #include #include -#include "gui/msgs/CalendarData.h" +#include "gui/calendar/CalendarData.h" #include "ui_CalendarWidget.h" class QListWidgetItem; diff --git a/retroshare-gui/src/gui/msgs/CalendarWidget.ui b/retroshare-gui/src/gui/calendar/CalendarWidget.ui similarity index 100% rename from retroshare-gui/src/gui/msgs/CalendarWidget.ui rename to retroshare-gui/src/gui/calendar/CalendarWidget.ui diff --git a/retroshare-gui/src/gui/msgs/EventDialog.cpp b/retroshare-gui/src/gui/calendar/EventDialog.cpp similarity index 99% rename from retroshare-gui/src/gui/msgs/EventDialog.cpp rename to retroshare-gui/src/gui/calendar/EventDialog.cpp index ec13c5653..8461d9ade 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.cpp +++ b/retroshare-gui/src/gui/calendar/EventDialog.cpp @@ -18,7 +18,7 @@ * * *******************************************************************************/ -#include "gui/msgs/EventDialog.h" +#include "gui/calendar/EventDialog.h" #include #include "retroshare/rsgxsflags.h" #include diff --git a/retroshare-gui/src/gui/msgs/EventDialog.h b/retroshare-gui/src/gui/calendar/EventDialog.h similarity index 98% rename from retroshare-gui/src/gui/msgs/EventDialog.h rename to retroshare-gui/src/gui/calendar/EventDialog.h index 081f00bd0..0cee88292 100644 --- a/retroshare-gui/src/gui/msgs/EventDialog.h +++ b/retroshare-gui/src/gui/calendar/EventDialog.h @@ -22,7 +22,7 @@ #define EVENTDIALOG_H #include -#include "gui/msgs/CalendarData.h" +#include "gui/calendar/CalendarData.h" class QComboBox; class QLineEdit; diff --git a/retroshare-gui/src/gui/msgs/TaskDialog.cpp b/retroshare-gui/src/gui/calendar/TaskDialog.cpp similarity index 99% rename from retroshare-gui/src/gui/msgs/TaskDialog.cpp rename to retroshare-gui/src/gui/calendar/TaskDialog.cpp index d8cfb4ae4..d589d4b58 100644 --- a/retroshare-gui/src/gui/msgs/TaskDialog.cpp +++ b/retroshare-gui/src/gui/calendar/TaskDialog.cpp @@ -1,4 +1,4 @@ -#include "gui/msgs/TaskDialog.h" +#include "gui/calendar/TaskDialog.h" #include #include #include diff --git a/retroshare-gui/src/gui/msgs/TaskDialog.h b/retroshare-gui/src/gui/calendar/TaskDialog.h similarity index 96% rename from retroshare-gui/src/gui/msgs/TaskDialog.h rename to retroshare-gui/src/gui/calendar/TaskDialog.h index 7cae6074b..9df6d2cc2 100644 --- a/retroshare-gui/src/gui/msgs/TaskDialog.h +++ b/retroshare-gui/src/gui/calendar/TaskDialog.h @@ -2,7 +2,7 @@ #define TASKDIALOG_H #include -#include "gui/msgs/CalendarData.h" +#include "gui/calendar/CalendarData.h" class QComboBox; class QLineEdit; diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.cpp b/retroshare-gui/src/gui/calendar/TasksWidget.cpp similarity index 99% rename from retroshare-gui/src/gui/msgs/TasksWidget.cpp rename to retroshare-gui/src/gui/calendar/TasksWidget.cpp index 7265bf3c1..8e34f17af 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.cpp +++ b/retroshare-gui/src/gui/calendar/TasksWidget.cpp @@ -18,9 +18,9 @@ * * *******************************************************************************/ -#include "gui/msgs/TasksWidget.h" -#include "gui/msgs/TaskDialog.h" -#include "gui/msgs/CalendarPropertiesDialog.h" +#include "gui/calendar/TasksWidget.h" +#include "gui/calendar/TaskDialog.h" +#include "gui/calendar/CalendarPropertiesDialog.h" #include #include #include diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.h b/retroshare-gui/src/gui/calendar/TasksWidget.h similarity index 98% rename from retroshare-gui/src/gui/msgs/TasksWidget.h rename to retroshare-gui/src/gui/calendar/TasksWidget.h index a3154c122..7fde06edb 100644 --- a/retroshare-gui/src/gui/msgs/TasksWidget.h +++ b/retroshare-gui/src/gui/calendar/TasksWidget.h @@ -23,7 +23,7 @@ #include #include -#include "gui/msgs/CalendarData.h" +#include "gui/calendar/CalendarData.h" #include "ui_TasksWidget.h" class QListWidgetItem; diff --git a/retroshare-gui/src/gui/msgs/TasksWidget.ui b/retroshare-gui/src/gui/calendar/TasksWidget.ui similarity index 100% rename from retroshare-gui/src/gui/msgs/TasksWidget.ui rename to retroshare-gui/src/gui/calendar/TasksWidget.ui diff --git a/retroshare-gui/src/gui/msgs/MessagesDialog.cpp b/retroshare-gui/src/gui/msgs/MessagesDialog.cpp index e39616803..de62366ae 100644 --- a/retroshare-gui/src/gui/msgs/MessagesDialog.cpp +++ b/retroshare-gui/src/gui/msgs/MessagesDialog.cpp @@ -26,8 +26,10 @@ #include #include "MessagesDialog.h" -#include "gui/msgs/CalendarWidget.h" -#include "gui/msgs/TasksWidget.h" +#ifdef RS_USE_CALENDAR +#include "gui/calendar/CalendarWidget.h" +#include "gui/calendar/TasksWidget.h" +#endif #include "gui/common/TagDefs.h" #include "gui/common/PeerDefs.h" @@ -149,8 +151,10 @@ MessagesDialog::MessagesDialog(QWidget *parent) lockUpdate = 0; lastSelectedIndex = QModelIndex(); mLastCurrentQuickViewRow = -1; +#ifdef RS_USE_CALENDAR mCalendarWidget = nullptr; mTasksWidget = nullptr; +#endif msgWidget = new MessageWidget(true, this); ui.msgLayout->addWidget(msgWidget); @@ -275,6 +279,7 @@ MessagesDialog::MessagesDialog(QWidget *parent) tagIndex = 0; } +#ifdef RS_USE_CALENDAR QToolButton *calendarBtn = new QToolButton(this); calendarBtn->setIcon(FilesDefs::getIconFromQtResourcePath(":/icons/svg/calendar-month.svg")); calendarBtn->setIconSize(QSize(24, 24)); @@ -295,6 +300,7 @@ MessagesDialog::MessagesDialog(QWidget *parent) ui.msgsButtons_HL->insertWidget(tagIndex, calendarBtn); ui.msgsButtons_HL->insertWidget(tagIndex + 1, tasksBtn); +#endif int H = misc::getFontSizeFactor("HelpButton").height(); QString help_str = tr( @@ -1608,11 +1614,13 @@ void MessagesDialog::emptyTrash() void MessagesDialog::tabChanged(int tab) { QWidget *widget = ui.tabWidget->widget(tab); +#ifdef RS_USE_CALENDAR if (widget == mCalendarWidget && mCalendarWidget) { mCalendarWidget->refreshData(); } else if (widget == mTasksWidget && mTasksWidget) { mTasksWidget->refreshData(); } +#endif connectActions(); updateInterface(); } @@ -1626,16 +1634,19 @@ void MessagesDialog::tabCloseRequested(int tab) QWidget *widget = ui.tabWidget->widget(tab); if (widget) { +#ifdef RS_USE_CALENDAR if (widget == mCalendarWidget) { mCalendarWidget = nullptr; } else if (widget == mTasksWidget) { mTasksWidget = nullptr; } +#endif ui.tabWidget->removeTab(tab); widget->deleteLater(); } } +#ifdef RS_USE_CALENDAR void MessagesDialog::showCalendarTab() { if (!mCalendarWidget) { @@ -1653,6 +1664,7 @@ void MessagesDialog::showTasksTab() } ui.tabWidget->setCurrentWidget(mTasksWidget); } +#endif void MessagesDialog::closeTab(const std::string &msgId) { diff --git a/retroshare-gui/src/gui/msgs/MessagesDialog.h b/retroshare-gui/src/gui/msgs/MessagesDialog.h index 2201184eb..8d44b0dbb 100644 --- a/retroshare-gui/src/gui/msgs/MessagesDialog.h +++ b/retroshare-gui/src/gui/msgs/MessagesDialog.h @@ -36,8 +36,10 @@ class MessageWidget; class QTreeWidgetItem; class RsMessageModel; class MessageSortFilterProxyModel ; +#ifdef RS_USE_CALENDAR class CalendarWidget; class TasksWidget; +#endif class MessagesDialog : public MainPage { @@ -112,8 +114,10 @@ private slots: void tabChanged(int tab); void tabCloseRequested(int tab); +#ifdef RS_USE_CALENDAR void showCalendarTab(); void showTasksTab(); +#endif private: void handleEvent_main_thread(std::shared_ptr event); @@ -156,8 +160,10 @@ private: //RSTreeWidgetItemCompareRole *mMessageCompareRole; MessageWidget *msgWidget; +#ifdef RS_USE_CALENDAR CalendarWidget *mCalendarWidget; TasksWidget *mTasksWidget; +#endif RsMessageModel *mMessageModel; MessageSortFilterProxyModel *mMessageProxyModel; diff --git a/retroshare-gui/src/retroshare-gui.pro b/retroshare-gui/src/retroshare-gui.pro index fdba34c9b..c97278e4d 100644 --- a/retroshare-gui/src/retroshare-gui.pro +++ b/retroshare-gui/src/retroshare-gui.pro @@ -111,6 +111,7 @@ CONFIG += gxschannels CONFIG += posted CONFIG += gxsgui CONFIG += gxscircles +#CONFIG += gxscalendar # Other Disabled Bits. #CONFIG += framecatcher @@ -477,12 +478,6 @@ HEADERS += rshare.h \ gui/connect/PGPKeyDialog.h \ gui/connect/FriendRecommendDialog.h \ gui/msgs/MessagesDialog.h \ - gui/msgs/CalendarData.h \ - gui/msgs/CalendarWidget.h \ - gui/msgs/TasksWidget.h \ - gui/msgs/CalendarPropertiesDialog.h \ - gui/msgs/EventDialog.h \ - gui/msgs/TaskDialog.h \ gui/msgs/MessageInterface.h \ gui/msgs/MessageComposer.h \ gui/msgs/MessageWindow.h \ @@ -672,8 +667,6 @@ FORMS += gui/StartDialog.ui \ gui/msgs/MessageComposer.ui \ gui/msgs/MessageWindow.ui\ gui/msgs/MessageWidget.ui\ - gui/msgs/CalendarWidget.ui \ - gui/msgs/TasksWidget.ui \ gui/settings/settingsw.ui \ gui/settings/GeneralPage.ui \ gui/settings/ServerPage.ui \ @@ -843,12 +836,6 @@ SOURCES += main.cpp \ gui/connect/ConfCertDialog.cpp \ gui/connect/PGPKeyDialog.cpp \ gui/msgs/MessagesDialog.cpp \ - gui/msgs/CalendarData.cpp \ - gui/msgs/CalendarWidget.cpp \ - gui/msgs/TasksWidget.cpp \ - gui/msgs/CalendarPropertiesDialog.cpp \ - gui/msgs/EventDialog.cpp \ - gui/msgs/TaskDialog.cpp \ gui/msgs/MessageComposer.cpp \ gui/msgs/MessageWidget.cpp \ gui/msgs/MessageWindow.cpp \ @@ -1512,6 +1499,30 @@ gxsgui { } +gxscalendar { + DEFINES += RS_USE_CALENDAR + + HEADERS += \ + gui/calendar/CalendarData.h \ + gui/calendar/CalendarWidget.h \ + gui/calendar/TasksWidget.h \ + gui/calendar/CalendarPropertiesDialog.h \ + gui/calendar/EventDialog.h \ + gui/calendar/TaskDialog.h + + FORMS += \ + gui/calendar/CalendarWidget.ui \ + gui/calendar/TasksWidget.ui + + SOURCES += \ + gui/calendar/CalendarData.cpp \ + gui/calendar/CalendarWidget.cpp \ + gui/calendar/TasksWidget.cpp \ + gui/calendar/CalendarPropertiesDialog.cpp \ + gui/calendar/EventDialog.cpp \ + gui/calendar/TaskDialog.cpp +} + ################################################################ #Define qmake_info.h file so GUI can get wath was used to compil From 0b012f66858446b6cb64b852773249ac6a2d0559 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:22:18 +0200 Subject: [PATCH 26/26] Disabled calendar on cmake file by default to off --- retroshare-gui/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retroshare-gui/CMakeLists.txt b/retroshare-gui/CMakeLists.txt index 904280042..be5951a42 100644 --- a/retroshare-gui/CMakeLists.txt +++ b/retroshare-gui/CMakeLists.txt @@ -62,7 +62,7 @@ option( RS_GXSCHANNELS "Enable GXS channels in GUI" ON ) option( RS_GXSFORUMS "Enable GXS forums in GUI" ON ) option( RS_GXSPOSTED "Enable GXS posted in GUI" ON ) option( RS_GXSCIRCLES "Enable GXS circles in GUI" ON ) -option( RS_USE_CALENDAR "Build with GXS Calendar support" ON) +option( RS_USE_CALENDAR "Build with Calendar support" OFF) option( RS_GUI_CMARK "Enable CommonMark support in GUI" OFF ) set(RS_GXSIDENTITIES ON CACHE BOOL "Enable GXS identities in GUI" FORCE) set(RS_IDLE ON CACHE BOOL "Enable Idle support" FORCE)