This commit is contained in:
defnax 2026-09-11 07:54:59 +02:00 committed by GitHub
commit 03d9cc1371
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 5559 additions and 3 deletions

View File

@ -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 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)

View File

@ -711,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

View File

@ -0,0 +1,860 @@
/*******************************************************************************
* retroshare-gui/src/gui/msgs/CalendarData.cpp *
* *
* Copyright (C) 2011 by Retroshare Team <retroshare.project@gmail.com> *
* *
* 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 <https://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
#include "gui/calendar/CalendarData.h"
#include <retroshare/rsinit.h>
#include <retroshare/rspeers.h>
#include <retroshare/rsgxscalendar.h>
#include <retroshare/rsgxscircles.h>
#include <util/qtthreadsutils.h>
#include <QSettings>
#include <QDir>
#include <QUuid>
CalendarData* CalendarData::mInstance = nullptr;
CalendarData* CalendarData::instance() {
if (!mInstance) {
mInstance = new CalendarData();
}
return mInstance;
}
CalendarData::CalendarData() : QObject(), mEventHandlerId(0) {
loadData();
if (rsEvents && rsGxsCalendar) {
RsEventType calendarEventType = (RsEventType)rsEvents->getDynamicEventType("GXS_CALENDAR");
rsEvents->registerEventsHandler(
[this](std::shared_ptr<const RsEvent> event) {
RsQThreadUtils::postToObject([=]() { handleGxsEvent(event); }, this);
},
mEventHandlerId, calendarEventType
);
}
}
CalendarData::~CalendarData() {
saveData();
if (rsEvents && mEventHandlerId != 0) {
rsEvents->unregisterEventsHandler(mEventHandlerId);
}
}
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();
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();
// Ensure we have at least one default calendar
if (mCalendars.isEmpty()) {
CalendarInfo defaultCal;
defaultCal.id = "personal";
defaultCal.name = "Private";
defaultCal.color = QColor("#4a90e2");
defaultCal.isPublic = false;
defaultCal.owner = "local";
defaultCal.showReminders = true;
defaultCal.email = "retroshare <retroshare@GXSID>";
defaultCal.onNetwork = false;
mCalendars.append(defaultCal);
}
// 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();
ev.attachments = settings.value("attachments").toStringList();
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();
task.attachments = settings.value("attachments").toStringList();
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.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();
// 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.setValue("attachments", mEvents[i].attachments);
}
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.setValue("attachments", mTasks[i].attachments);
}
settings.endArray();
settings.sync();
}
void CalendarData::addCalendar(const CalendarInfo& cal) {
mCalendars.append(cal);
saveData();
emit calendarDataChanged();
}
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();
emit calendarDataChanged();
}
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;
}
}
// 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();
}
void CalendarData::addEvent(const CalendarEvent& ev) {
mEvents.append(ev);
saveData();
publishCalendarUpdates(ev.calendarId);
}
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();
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) {
for (int i = 0; i < mTasks.size(); ++i) {
if (mTasks[i].id == task.id) {
mTasks[i] = task;
break;
}
}
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<QString, QString> CalendarData::getContacts() {
QMap<QString, QString> contacts;
if (!rsPeers) {
return contacts;
}
std::list<RsPgpId> 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;
}
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'"));
}
for (const auto& att : ev.attachments) {
icsContent += QString("ATTACH:%1\r\n").arg(att);
}
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");
for (const auto& att : t.attachments) {
icsContent += QString("ATTACH:%1\r\n").arg(att);
}
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 (key.compare("ATTACH", Qt::CaseInsensitive) == 0) {
currentEvent.attachments.append(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);
} else if (key.compare("ATTACH", Qt::CaseInsensitive) == 0) {
currentTask.attachments.append(val);
}
}
}
}
}
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, cal.circleType, RsGxsCircleId(cal.circleId.toStdString()), RsGxsCircleId(cal.internalCircle.toStdString()), cal.groupFlags, 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";
localCal.circleType = 1;
localCal.circleId = "";
localCal.internalCircle = "";
localCal.groupFlags = 4;
localCal.description = "";
mCalendars.append(localCal);
saveData();
}
emit calendarDataChanged();
// Trigger sync to fetch contents
updateCalendars();
} 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::updateCalendars() {
if (!rsGxsCalendar) return;
std::list<RsGroupMetaData> 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";
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<RsGxsCalendarMessage> 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();
}
// 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();
}
}
void CalendarData::handleGxsEvent(std::shared_ptr<const RsEvent> event) {
const RsGxsCalendarEvent *e = dynamic_cast<const RsGxsCalendarEvent*>(event.get());
if (e) {
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:
updateCalendars();
break;
default:
break;
}
}
}

View File

@ -0,0 +1,143 @@
/*******************************************************************************
* retroshare-gui/src/gui/msgs/CalendarData.h *
* *
* Copyright (C) 2011 by Retroshare Team <retroshare.project@gmail.com> *
* *
* 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 <https://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
#ifndef CALENDARDATA_H
#define CALENDARDATA_H
#include <QString>
#include <QDateTime>
#include <QList>
#include <QMap>
#include <QColor>
#include <QStringList>
#include <QObject>
#include <memory>
#include <retroshare/rsevents.h>
struct CalendarInfo {
QString id;
QString name;
QColor color;
bool isPublic;
QString owner; // contact PGP ID or "local"
bool showReminders;
QString email;
bool onNetwork;
uint32_t circleType;
QString circleId;
QString internalCircle;
uint32_t groupFlags;
QString description;
};
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;
QStringList attachments;
};
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;
QStringList attachments;
};
class CalendarData : public QObject {
Q_OBJECT
public:
static CalendarData* instance();
void loadData();
void saveData();
const QList<CalendarInfo>& getCalendars() const { return mCalendars; }
const QList<CalendarEvent>& getEvents() const { return mEvents; }
const QList<CalendarTask>& 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<QString, QString> 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 updateCalendars();
private slots:
void handleGxsEvent(std::shared_ptr<const RsEvent> event);
private:
CalendarData();
~CalendarData() override;
QList<CalendarInfo> mCalendars;
QList<CalendarEvent> mEvents;
QList<CalendarTask> mTasks;
QMap<QString, QString> mLastMsgIds;
static CalendarData* mInstance;
uint32_t mEventHandlerId;
};
#endif // CALENDARDATA_H

View File

@ -0,0 +1,450 @@
/*******************************************************************************
* retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.cpp *
* *
* Copyright (C) 2011 by Retroshare Team <retroshare.project@gmail.com> *
* *
* 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 <https://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
#include "gui/calendar/CalendarPropertiesDialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QRadioButton>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <QStackedWidget>
#include <QColorDialog>
#include <QMessageBox>
#include <QUuid>
#include <retroshare/rspeers.h>
#include <retroshare/rsidentity.h>
#include <retroshare/rsgxscalendar.h>
#include "gui/gxs/GxsIdChooser.h"
#include "gui/gxs/GxsCircleChooser.h"
#include "gui/common/GroupChooser.h"
#include <retroshare/rsgxscircles.h>
#include <QGroupBox>
#include <QPlainTextEdit>
CalendarPropertiesDialog::CalendarPropertiesDialog(const QString& calId, QWidget* parent)
: QDialog(parent), mCalId(calId), mEditMode(!calId.isEmpty()), mSelectedColor(QColor("#4a90e2"))
{
setupUi();
// 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"));
// 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;
mRadioNetwork->setChecked(existingCal.onNetwork);
mRadioComputer->setChecked(!existingCal.onNetwork);
// 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();
// 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);
updatePage2Layout();
} 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 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);
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);
// ================= PAGE 2 (Wizard Step 2) =================
mPage2 = new QWidget(this);
QVBoxLayout* page2Layout = new QVBoxLayout(mPage2);
page2Layout->setContentsMargins(5, 5, 5, 5);
page2Layout->setSpacing(15);
mFormLayout = new QFormLayout();
mFormLayout->setSpacing(12);
mFormLayout->setLabelAlignment(Qt::AlignRight);
mNameEdit = new QLineEdit(mPage2);
mNameEdit->setMinimumHeight(26);
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()));
mFormLayout->addRow(tr("Colour:"), mColorBtn);
mIdChooser = new GxsIdChooser(mPage2);
mFormLayout->addRow(tr("Owner:"), mIdChooser);
page2Layout->addLayout(mFormLayout);
// Message Distribution group box
mDistribGroupBox = new QGroupBox(tr("Calendar 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->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(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()));
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::onNext() {
if (mRadioImport && mRadioImport->isChecked()) {
accept();
return;
}
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 (info.onNetwork && rsGxsCalendar && info.owner == "local") {
// 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;
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(), 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)));
return;
}
} else {
RsGxsGroupId groupId;
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);
} else {
QMessageBox::critical(this, tr("GXS Error"), tr("Failed to create network calendar: %1").arg(QString::fromStdString(errMsg)));
return;
}
}
} else {
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;
// 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();
}

View File

@ -0,0 +1,99 @@
/*******************************************************************************
* retroshare-gui/src/gui/msgs/CalendarPropertiesDialog.h *
* *
* Copyright (C) 2011 by Retroshare Team <retroshare.project@gmail.com> *
* *
* 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 <https://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
#ifndef CALENDARPROPERTIESDIALOG_H
#define CALENDARPROPERTIESDIALOG_H
#include <QDialog>
#include <QColor>
#include "gui/calendar/CalendarData.h"
class QStackedWidget;
class QRadioButton;
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
public:
CalendarPropertiesDialog(const QString& calId = "", QWidget* parent = nullptr);
~CalendarPropertiesDialog();
CalendarInfo getCalendarInfo() const;
bool isImportMode() const;
private slots:
void onNext();
void onBack();
void onSelectColor();
void onAccept();
void updateCircleOptions();
private:
void setupUi();
void updateColorButton();
void updatePage2Layout();
QString mCalId;
bool mEditMode;
QColor mSelectedColor;
QStackedWidget* mStackedWidget;
QWidget* mPage1;
QWidget* mPage2;
// Page 1 widgets
QRadioButton* mRadioComputer;
QRadioButton* mRadioNetwork;
QRadioButton* mRadioImport;
// Page 2 widgets
QFormLayout* mFormLayout;
QLineEdit* mNameEdit;
QPushButton* mColorBtn;
// 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;
QPushButton* mBackBtn;
QPushButton* mCreateOrSaveBtn;
QPushButton* mCancelBtn;
};
#endif // CALENDARPROPERTIESDIALOG_H

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,120 @@
/*******************************************************************************
* retroshare-gui/src/gui/msgs/CalendarWidget.h *
* *
* Copyright (C) 2011 by Retroshare Team <retroshare.project@gmail.com> *
* *
* 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 <https://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
#ifndef CALENDARWIDGET_H
#define CALENDARWIDGET_H
#include <QWidget>
#include <QDate>
#include <QMap>
#include <QStyledItemDelegate>
#include "gui/calendar/CalendarData.h"
#include "ui_CalendarWidget.h"
class QListWidgetItem;
class QLabel;
class QComboBox;
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
public:
CalendarWidget(QWidget* parent = nullptr);
~CalendarWidget();
QDate selectedDate() const { return mSelectedDate; }
public slots:
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 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);
private:
void buildUi();
void updateViews();
void updateDayView();
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
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;
QListWidget* mSharedCalendarList;
QLabel* mPeriodLabel;
QLabel* mCwLabel;
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<QString, QString> mCellEventMap; // "viewMode_row_col" -> Event ID
Ui::CalendarWidget ui;
};
#endif // CALENDARWIDGET_H

View File

@ -0,0 +1,322 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalendarWidget</class>
<widget class="QWidget" name="CalendarWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>833</width>
<height>600</height>
</rect>
</property>
<layout class="QHBoxLayout" name="mainLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="handleWidth">
<number>1</number>
</property>
<widget class="QWidget" name="sidebar" native="true">
<layout class="QVBoxLayout" name="sidebarLayout">
<property name="spacing">
<number>12</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QPushButton" name="newEventBtn">
<property name="styleSheet">
<string notr="true">font-weight: bold; background-color: #4a90e2; color: white; border-radius: 4px; padding: 6px;</string>
</property>
<property name="text">
<string>+ New Event</string>
</property>
</widget>
</item>
<item>
<widget class="QCalendarWidget" name="sidebarCalendar">
<property name="gridVisible">
<bool>true</bool>
</property>
<property name="horizontalHeaderFormat">
<enum>QCalendarWidget::SingleLetterDayNames</enum>
</property>
<property name="verticalHeaderFormat">
<enum>QCalendarWidget::NoVerticalHeader</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="calendarsLabel">
<property name="styleSheet">
<string notr="true">font-weight: bold; font-size: 14px;</string>
</property>
<property name="text">
<string>My Calendars</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="calendarList">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="sharedCalendarsLabel">
<property name="styleSheet">
<string notr="true">font-weight: bold; font-size: 14px; margin-top: 10px;</string>
</property>
<property name="text">
<string>Shared Calendars</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="sharedCalendarList">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="newCalBtn">
<property name="text">
<string>New Calendar...</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="mainArea" native="true">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="topControlLayout">
<item>
<widget class="QPushButton" name="prevBtn">
<property name="text">
<string>&lt;</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="todayBtn">
<property name="text">
<string>Today</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="nextBtn">
<property name="text">
<string>&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="periodLabel">
<property name="styleSheet">
<string notr="true">font-weight: bold; font-size: 16px; margin-left: 10px;</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="topControlSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="placeholderText">
<string>Search events...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="dayViewBtn">
<property name="text">
<string>Day</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="weekViewBtn">
<property name="text">
<string>Week</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="monthViewBtn">
<property name="text">
<string>Month</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSplitter" name="splitter_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QTableWidget" name="eventTable">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectItems</enum>
</property>
</widget>
<widget class="QStackedWidget" name="viewStack">
<widget class="QWidget" name="dayPage">
<layout class="QVBoxLayout" name="dayPageLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="dayTable">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="weekPage">
<layout class="QVBoxLayout" name="weekPageLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="weekTable">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="monthPage">
<layout class="QVBoxLayout" name="monthPageLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="monthTable">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,805 @@
/*******************************************************************************
* retroshare-gui/src/gui/msgs/EventDialog.cpp *
* *
* Copyright (C) 2026 by Retroshare Team <retroshare.project@gmail.com> *
* *
* 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 <https://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
#include "gui/calendar/EventDialog.h"
#include <retroshare/rsgxscalendar.h>
#include "retroshare/rsgxsflags.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include <QComboBox>
#include <QCheckBox>
#include <QDateTimeEdit>
#include <QTextEdit>
#include <QListWidget>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include "gui/gxs/GxsIdTreeWidgetItem.h"
#include "gui/gxs/GxsIdDetails.h"
#include <QPushButton>
#include <QTabWidget>
#include <QMessageBox>
#include <QUuid>
#include <QFileDialog>
#include <QDesktopServices>
#include <QUrl>
#include <QFileInfo>
#include <QMenu>
#include <QDir>
#include "gui/RetroShareLink.h"
#include "gui/common/FriendSelectionWidget.h"
#include <QDialogButtonBox>
#include <retroshare/rsidentity.h>
#include <retroshare/rspeers.h>
#include <retroshare/rsmail.h>
#include "gui/common/PeerDefs.h"
#include <QCoreApplication>
#include "gui/common/AvatarDefs.h"
#include <QPixmap>
#include <QIcon>
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)
{
setMinimumSize(500, 600);
buildUi();
loadEvent();
updateModeUi();
}
EventDialog::~EventDialog() {}
void EventDialog::buildUi() {
QVBoxLayout* mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(15, 15, 15, 15);
mainLayout->setSpacing(10);
// Top action bar (Save, Invite, Delete)
mActionWidget = new QWidget(this);
QHBoxLayout* actionLayout = new QHBoxLayout(mActionWidget);
actionLayout->setContentsMargins(0, 0, 0, 0);
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);
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()) {
mDeleteBtn->setEnabled(false);
}
actionLayout->addStretch();
mainLayout->addWidget(mActionWidget);
// Form inputs layout
QFormLayout* formLayout = new QFormLayout();
formLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
formLayout->setSpacing(8);
mCalendarCombo = new QComboBox(this);
const QList<CalendarInfo>& 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 QTreeWidget(this);
mAttendeesList->setHeaderHidden(true);
mAttendeesList->setIconSize(QSize(32, 32));
mAttendeesList->setRootIsDecorated(false);
tabWidget->addTab(mAttendeesList, tr("Attendees"));
// Attachments Tab
QWidget* attachTab = new QWidget(this);
QVBoxLayout* attachLayout = new QVBoxLayout(attachTab);
mAttachmentsList = new QListWidget(this);
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<RetroShareLink> 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<RetroShareLink> 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));
}
});
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);
// Bottom Options Checkboxes
QHBoxLayout* bottomCheckLayout = new QHBoxLayout();
mNotifyCheck = new QCheckBox(tr("Notify attendees"), this);
mNotifyCheck->setChecked(true);
bottomCheckLayout->addWidget(mNotifyCheck);
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() {
if (mEventId.isEmpty()) {
return;
}
const QList<CalendarEvent>& 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
mAttendeesList->clear();
for (const auto& contactId : ev.attendees) {
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
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;
}
}
}
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() {
QDialog dialog(this);
dialog.setWindowTitle(tr("Invite Attendees"));
dialog.resize(this->width(), 500);
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_CHECK);
friendsWidget->setShowType(FriendSelectionWidget::SHOW_GXS);
friendsWidget->start();
connect(filterCombo, static_cast<void (QComboBox::*)(int)>(&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<std::string> psidsGpg;
std::set<std::string> psidsGxs;
std::set<std::string> 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, 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);
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
layout->addWidget(filterCombo);
layout->addWidget(friendsWidget);
layout->addWidget(buttonBox);
while (dialog.exec() == QDialog::Accepted) {
std::set<RsPgpId> selectedGpg;
friendsWidget->selectedIds<RsPgpId, FriendSelectionWidget::IDTYPE_GPG>(selectedGpg, false);
std::set<RsGxsId> selectedGxs;
friendsWidget->selectedIds<RsGxsId, FriendSelectionWidget::IDTYPE_GXS>(selectedGxs, false);
std::set<RsPeerId> selectedSsl;
friendsWidget->selectedIds<RsPeerId, FriendSelectionWidget::IDTYPE_SSL>(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();
for (const auto& pgpId : selectedGpg) {
QString pgpIdStr = QString::fromStdString(pgpId.toStdString());
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(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
QTabWidget* tabWidget = findChild<QTabWidget*>();
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->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());
}
if (mEventId.isEmpty()) {
CalendarData::instance()->addEvent(ev);
} else {
CalendarData::instance()->updateEvent(ev);
}
// Send actual invitations
if (mNotifyCheck->isChecked() && !invitedNames.isEmpty()) {
sendInvite(ev, invitedNames);
}
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();
}
}
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<RsGroupMetaData> 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);
}
void EventDialog::sendInvite(const CalendarEvent& ev, const QStringList& invitedNames) {
bool at_least_one_gxsid = false;
std::set<Rs::Mail::MsgAddress> destinations;
for (const auto& contactId : ev.attendees) {
std::string idStr = contactId.toStdString();
if (idStr.length() == 16) {
RsPgpId pgpId(idStr);
std::list<RsPeerId> 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 += "<h3>" + tr("You are invited to a calendar event:") + "</h3>";
body += "<table>";
body += "<tr><td><b>" + tr("Title:") + "</b></td><td>" + ev.title + "</td></tr>";
if (!ev.location.isEmpty()) {
body += "<tr><td><b>" + tr("Location:") + "</b></td><td>" + ev.location + "</td></tr>";
}
body += "<tr><td><b>" + tr("Time:") + "</b></td><td>" + ev.start.toString("yyyy-MM-dd hh:mm") + " - " + ev.end.toString("yyyy-MM-dd hh:mm") + "</td></tr>";
if (!ev.description.isEmpty()) {
body += "<tr><td><b>" + tr("Description:") + "</b></td><td>" + QString(ev.description).replace("\n", "<br>") + "</td></tr>";
}
body += "</table>";
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<RsGxsId> 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."));
}
}

View File

@ -0,0 +1,88 @@
/*******************************************************************************
* retroshare-gui/src/gui/msgs/EventDialog.h *
* *
* Copyright (C) 2026 by Retroshare Team <retroshare.project@gmail.com> *
* *
* 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 <https://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
#ifndef EVENTDIALOG_H
#define EVENTDIALOG_H
#include <QDialog>
#include "gui/calendar/CalendarData.h"
class QComboBox;
class QLineEdit;
class QCheckBox;
class QDateTimeEdit;
class QTextEdit;
class QListWidget;
class QTreeWidget;
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, bool readOnly = false);
~EventDialog();
private slots:
void onSaveAndClose();
void onDelete();
void onAllDayToggled(bool checked);
void onInviteAttendees();
void onEditClicked();
private:
void loadEvent();
void buildUi();
void updateModeUi();
void sendInvite(const CalendarEvent& ev, const QStringList& invitedNames);
QString mEventId;
QDateTime mDefaultStart;
bool mReadOnly;
QWidget* mActionWidget;
QPushButton* mSaveBtn;
QPushButton* mInviteBtn;
QPushButton* mDeleteBtn;
QComboBox* mCalendarCombo;
QLineEdit* mTitleEdit;
QLineEdit* mLocationEdit;
QComboBox* mCategoryCombo;
QCheckBox* mAllDayCheck;
QDateTimeEdit* mStartEdit;
QDateTimeEdit* mEndEdit;
QComboBox* mRepeatCombo;
QComboBox* mReminderCombo;
QTextEdit* mDescriptionEdit;
QTreeWidget* mAttendeesList;
QListWidget* mAttachmentsList;
QPushButton* mAddAttachBtn;
QWidget* mBottomButtonsWidget;
QPushButton* mEditBtn;
QPushButton* mCloseBtn;
QCheckBox* mNotifyCheck;
};
#endif // EVENTDIALOG_H

View File

@ -0,0 +1,345 @@
#include "gui/calendar/TaskDialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include <QComboBox>
#include <QCheckBox>
#include <QDateTimeEdit>
#include <QTextEdit>
#include <QSpinBox>
#include <QPushButton>
#include <QTabWidget>
#include <QMessageBox>
#include <QUuid>
#include <QFileDialog>
#include <QListWidget>
#include <QFileInfo>
#include <QDesktopServices>
#include <QUrl>
#include <QMenu>
#include <QDir>
#include "gui/RetroShareLink.h"
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<CalendarInfo>& 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);
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<RetroShareLink> 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<RetroShareLink> 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));
}
});
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);
}
void TaskDialog::loadTask() {
if (mTaskId.isEmpty()) {
return;
}
const QList<CalendarTask>& 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);
// 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;
}
}
}
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;
}
// 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 {
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();
}
}

View File

@ -0,0 +1,51 @@
#ifndef TASKDIALOG_H
#define TASKDIALOG_H
#include <QDialog>
#include "gui/calendar/CalendarData.h"
class QComboBox;
class QLineEdit;
class QCheckBox;
class QDateTimeEdit;
class QTextEdit;
class QSpinBox;
class QListWidget;
class QPushButton;
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;
QPushButton* mAddAttachBtn;
};
#endif // TASKDIALOG_H

View File

@ -0,0 +1,544 @@
/*******************************************************************************
* retroshare-gui/src/gui/msgs/TasksWidget.cpp *
* *
* Copyright (C) 2011 by Retroshare Team <retroshare.project@gmail.com> *
* *
* 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 <https://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
#include "gui/calendar/TasksWidget.h"
#include "gui/calendar/TaskDialog.h"
#include "gui/calendar/CalendarPropertiesDialog.h"
#include <retroshare/rsidentity.h>
#include <retroshare/rsgxscalendar.h>
#include <QVBoxLayout>
#include <QMenu>
#include <QComboBox>
#include <QInputDialog>
#include <QColorDialog>
#include <QMessageBox>
#include <QFileDialog>
#include <QFile>
#include <QTextStream>
#include <QDir>
#include <QHBoxLayout>
#include <QSplitter>
#include <QPushButton>
#include <QCalendarWidget>
#include <QListWidget>
#include <QTableWidget>
#include <QLabel>
#include <QLineEdit>
#include <QHeaderView>
#include <QCheckBox>
#include <QDateTime>
#include <QShowEvent>
#include <QUuid>
TasksWidget::TasksWidget(QWidget* parent)
: QWidget(parent), mCurrentFilterMode(0), 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()->updateCalendars();
}
}
void TasksWidget::buildUi() {
ui.setupUi(this);
// Initialize UI pointers
mSidebarCalendar = ui.sidebarCalendar;
mFilterList = ui.filterList;
mCalendarList = ui.calendarList;
mSharedCalendarList = ui.sharedCalendarList;
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(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)));
}
void TasksWidget::refreshData() {
const auto& cals = CalendarData::instance()->getCalendars();
// 1. Populate My Calendars (owned by us, i.e. owner == "local")
{
// Save current check states
QMap<QString, Qt::CheckState> checkedStates;
for (int i = 0; i < mCalendarList->count(); ++i) {
QListWidgetItem* item = mCalendarList->item(i);
checkedStates[item->data(Qt::UserRole).toString()] = item->checkState();
}
mCalendarList->blockSignals(true);
mCalendarList->clear();
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);
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);
}
// 2. Populate Shared Calendars (not owned by us)
{
// Save current check states and subscription states
QMap<QString, Qt::CheckState> sharedCheckedStates;
QMap<QString, bool> sharedSubscribedStates;
for (int i = 0; i < mSharedCalendarList->count(); ++i) {
QListWidgetItem* item = mSharedCalendarList->item(i);
QString calId = item->data(Qt::UserRole).toString();
sharedCheckedStates[calId] = item->checkState();
sharedSubscribedStates[calId] = item->data(Qt::UserRole + 1).toBool();
}
mSharedCalendarList->blockSignals(true);
mSharedCalendarList->clear();
if (rsGxsCalendar) {
std::list<RsGroupMetaData> 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());
// 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);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
// Render blue bullet for subscribed, grey for unsubscribed
QPixmap pix(12, 12);
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 and subscription status did not change.
// If subscription state changed, set checked state based on new subscription status.
if (sharedCheckedStates.contains(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);
}
}
}
}
mSharedCalendarList->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());
}
}
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();
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";
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::onSharedCalendarSelectionChanged(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"));
// 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();
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);
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);
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) {
exportCalendar(calId, calName);
} else if (selectedAct == propertiesAct) {
CalendarPropertiesDialog dlg(calId, this);
if (dlg.exec() == QDialog::Accepted) {
refreshData();
}
}
}
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);
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))
);
}
}
}

View File

@ -0,0 +1,79 @@
/*******************************************************************************
* retroshare-gui/src/gui/msgs/TasksWidget.h *
* *
* Copyright (C) 2011 by Retroshare Team <retroshare.project@gmail.com> *
* *
* 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 <https://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
#ifndef TASKSWIDGET_H
#define TASKSWIDGET_H
#include <QWidget>
#include <QDate>
#include "gui/calendar/CalendarData.h"
#include "ui_TasksWidget.h"
class QListWidgetItem;
class QComboBox;
class TasksWidget : public QWidget {
Q_OBJECT
public:
TasksWidget(QWidget* parent = nullptr);
~TasksWidget();
public slots:
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 onSharedCalendarSelectionChanged(QListWidgetItem* item);
void onSearchChanged(const QString& text);
void onCalendarContextMenu(const QPoint& pos);
void onSharedCalendarContextMenu(const QPoint& pos);
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;
QListWidget* mSharedCalendarList;
QLineEdit* mQuickTaskEdit;
QLineEdit* mSearchEdit;
QTableWidget* mTaskTable;
Ui::TasksWidget ui;
};
#endif // TASKSWIDGET_H

View File

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TasksWidget</class>
<widget class="QWidget" name="TasksWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<layout class="QHBoxLayout" name="mainLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="handleWidth">
<number>1</number>
</property>
<widget class="QWidget" name="sidebar" native="true">
<layout class="QVBoxLayout" name="sidebarLayout">
<property name="spacing">
<number>12</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QPushButton" name="newTaskBtn">
<property name="styleSheet">
<string notr="true">font-weight: bold; background-color: #4a90e2; color: white; border-radius: 4px; padding: 6px;</string>
</property>
<property name="text">
<string>+ New Task</string>
</property>
</widget>
</item>
<item>
<widget class="QCalendarWidget" name="sidebarCalendar">
<property name="gridVisible">
<bool>true</bool>
</property>
<property name="horizontalHeaderFormat">
<enum>QCalendarWidget::SingleLetterDayNames</enum>
</property>
<property name="verticalHeaderFormat">
<enum>QCalendarWidget::NoVerticalHeader</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="filterLabel">
<property name="styleSheet">
<string notr="true">font-weight: bold; font-size: 14px;</string>
</property>
<property name="text">
<string>Filter Tasks</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="filterList"/>
</item>
<item>
<widget class="QLabel" name="calendarsLabel">
<property name="styleSheet">
<string notr="true">font-weight: bold; font-size: 14px;</string>
</property>
<property name="text">
<string>My Calendars</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="calendarList">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="sharedCalendarsLabel">
<property name="styleSheet">
<string notr="true">font-weight: bold; font-size: 14px; margin-top: 10px;</string>
</property>
<property name="text">
<string>Shared Calendars</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="sharedCalendarList">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="mainArea" native="true">
<layout class="QVBoxLayout" name="mainAreaLayout">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<layout class="QHBoxLayout" name="topLayout">
<item>
<widget class="QLineEdit" name="quickTaskEdit">
<property name="placeholderText">
<string>Click here to add a new task</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="placeholderText">
<string>Search tasks...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="taskTable">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -26,6 +26,10 @@
#include <QTimer>
#include "MessagesDialog.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"
@ -147,6 +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);
@ -266,6 +274,34 @@ 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;
}
#ifdef RS_USE_CALENDAR
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);
#endif
int H = misc::getFontSizeFactor("HelpButton").height();
QString help_str = tr(
"<h1><img width=\"%1\" src=\":/icons/help_64.png\">&nbsp;&nbsp;Messages</h1>"
@ -1575,8 +1611,16 @@ void MessagesDialog::emptyTrash()
rsMail->MessageDelete(it->msgId);
}
void MessagesDialog::tabChanged(int /*tab*/)
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();
}
@ -1590,15 +1634,43 @@ 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) {
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);
}
#endif
void MessagesDialog::closeTab(const std::string &msgId)
{
QList<MessageWidget*> msgWidgets;
for (int tab = 1; tab < ui.tabWidget->count(); ++tab) {
for (int tab = 3; tab < ui.tabWidget->count(); ++tab) {
MessageWidget *msgWidget = dynamic_cast<MessageWidget*>(ui.tabWidget->widget(tab));
if (msgWidget && msgWidget->msgId() == msgId) {
msgWidgets.append(msgWidget);
@ -1626,7 +1698,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);

View File

@ -36,6 +36,10 @@ class MessageWidget;
class QTreeWidgetItem;
class RsMessageModel;
class MessageSortFilterProxyModel ;
#ifdef RS_USE_CALENDAR
class CalendarWidget;
class TasksWidget;
#endif
class MessagesDialog : public MainPage
{
@ -110,6 +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<const RsEvent> event);
@ -152,6 +160,10 @@ private:
//RSTreeWidgetItemCompareRole *mMessageCompareRole;
MessageWidget *msgWidget;
#ifdef RS_USE_CALENDAR
CalendarWidget *mCalendarWidget;
TasksWidget *mTasksWidget;
#endif
RsMessageModel *mMessageModel;
MessageSortFilterProxyModel *mMessageProxyModel;

View File

@ -111,6 +111,7 @@ CONFIG += gxschannels
CONFIG += posted
CONFIG += gxsgui
CONFIG += gxscircles
#CONFIG += gxscalendar
# Other Disabled Bits.
#CONFIG += framecatcher
@ -1498,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