aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-24 17:28:14 +0200
committerDanilo M. <danix@danix.xyz>2026-09-24 17:28:14 +0200
commit516b2daaeab00bb4d4c28055a434dc5ee53aeb60 (patch)
tree5e75f82d4b6022ef66544a0ad8b361b99dd0acb3
parentbf1d7ecf687adeadc98d60395a4efc7cb306e13f (diff)
downloadqtmaildir-516b2daaeab00bb4d4c28055a434dc5ee53aeb60.tar.gz
qtmaildir-516b2daaeab00bb4d4c28055a434dc5ee53aeb60.zip
feat: open the calendar from the View menu and the toolbar
-rw-r--r--src/keymap.cpp3
-rw-r--r--src/mainwindow.cpp44
-rw-r--r--src/mainwindow.h14
-rw-r--r--tests/test_mainwindow.cpp26
4 files changed, 87 insertions, 0 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp
index 97399e2..67617d3 100644
--- a/src/keymap.cpp
+++ b/src/keymap.cpp
@@ -63,6 +63,9 @@ QStringList KeyMap::knownActions()
QStringLiteral("mark_all_read"),
QStringLiteral("edit_tags"),
QStringLiteral("tag_rules"),
+ // Item 206. No default chord: the calendar is opened from the View
+ // menu and the toolbar, and a shortcut is a chosen subset (item 132).
+ QStringLiteral("calendar"),
QStringLiteral("flag"),
// Compose and send (item 123). save_message deliberately carries no
// default chord: since item 132 a shortcut is a chosen subset rather
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 4c37a83..26643f4 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -53,6 +53,7 @@
#include <QToolButton>
#include <QVBoxLayout>
+#include "calendarwindow.h"
#include "composecontext.h"
#include "composewindow.h"
#include "mailsync.h"
@@ -246,6 +247,19 @@ void MainWindow::closeEvent(QCloseEvent *event)
return;
}
+ // The calendar is parentless, so Qt would leave it open with nothing
+ // behind it (the composer precedent below). Its own closeEvent asks about
+ // an unsaved event; a Cancel there cancels the quit. Done before the first
+ // prompt so the whole quit stops here rather than after the user has
+ // answered something else.
+ if (m_calendar) {
+ m_calendar->close();
+ if (m_calendar && m_calendar->isVisible()) {
+ event->ignore();
+ return;
+ }
+ }
+
// Case 3 FIRST, because it is the one where saving is what is already not
// working: in case 2 nothing is lost by saving, here quitting loses that
// text, so the dialog must say so plainly rather than offering a save that
@@ -1035,6 +1049,28 @@ void MainWindow::buildUi()
setWindowTitle(QStringLiteral("qtmaildir %1").arg(QTMAILDIR_VERSION));
}
+void MainWindow::openCalendar()
+{
+ if (m_config.calendarsDir().isEmpty()) {
+ showTransientStatus(
+ tr("No calendar is configured: set calendars_dir under [general]."));
+ return;
+ }
+ if (m_calendar) {
+ m_calendar->raise();
+ m_calendar->activateWindow();
+ return;
+ }
+ QStringList own;
+ for (const Account &account : m_config.accounts())
+ if (!account.address.isEmpty())
+ own << account.address;
+ // Parentless, like a composer: its own entry in the task switcher.
+ m_calendar = new CalendarWindow(m_config, own, uiStatePath());
+ m_calendar->setAttribute(Qt::WA_DeleteOnClose);
+ m_calendar->show();
+}
+
void MainWindow::composeNew()
{
// m_accountBox->currentData() is how the selected account is read
@@ -1913,6 +1949,10 @@ void MainWindow::registerActions()
tr("Edit the rules that tag mail as it arrives"), [this]() {
showTagRulesDialog();
});
+ addAction(QStringLiteral("calendar"), tr("&Calendar"),
+ tr("Open the calendar"), [this]() {
+ openCalendar();
+ });
addAction(QStringLiteral("save_query"), tr("&Save query..."),
tr("Keep the current query as a saved query"), [this]() {
saveCurrentQuery();
@@ -2168,6 +2208,7 @@ const QHash<QString, QPair<QString, QString>> kThemeIcons = {
// is the whole control, and editing the standing rules is not editing
// the selection's tags.
{ QStringLiteral("tag_rules"), { QStringLiteral("configure"), QString() } },
+ { QStringLiteral("calendar"), { QStringLiteral("x-office-calendar"), QString() } },
{ QStringLiteral("complete_query"), { QStringLiteral("edit-find-replace"), QString() } },
// NOT "document-save": that is the floppy/disk shape, which reads as
// "write a file somewhere" and asks the user to guess what is being
@@ -2292,6 +2333,8 @@ void MainWindow::buildMenus()
viewMenu->addAction(m_actions.value(QStringLiteral("zoom_in")));
viewMenu->addAction(m_actions.value(QStringLiteral("zoom_out")));
viewMenu->addAction(m_actions.value(QStringLiteral("zoom_reset")));
+ viewMenu->addSeparator();
+ viewMenu->addAction(m_actions.value(QStringLiteral("calendar")));
auto *helpMenu = menuBar()->addMenu(tr("&Help"));
auto *shortcuts = helpMenu->addAction(tr("&Keyboard shortcuts"));
@@ -2400,6 +2443,7 @@ void MainWindow::buildMenus()
// message is exactly where it must not be: that is how a user marks a
// thousand threads read meaning to mark one.
toolBar->addAction(m_actions.value(QStringLiteral("mark_all_read")));
+ toolBar->addAction(m_actions.value(QStringLiteral("calendar")));
toolBar->addSeparator();
toolBar->addAction(m_actions.value(QStringLiteral("undo")));
}
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 4fb19e4..e965de2 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -76,6 +76,7 @@ class NotmuchWorker;
class QueryCompleter;
class TagRulesDialog;
class ComposeWindow;
+class CalendarWindow;
class MainWindow : public QMainWindow
{
@@ -888,6 +889,15 @@ private:
/// that populates.
void showMaildirOverview();
+ /// Opens the calendar window (item 206), or raises the one already open.
+ ///
+ /// A single instance: the window is parentless, like a composer, so it
+ /// gets its own task-switcher entry, and WA_DeleteOnClose frees it when the
+ /// user closes it. Does nothing but hint when no calendars_dir is
+ /// configured, rather than opening an empty window over a feature that is
+ /// off.
+ void openCalendar();
+
/// Opens a composer on a blank message (item 123).
void composeNew();
@@ -1948,6 +1958,10 @@ private:
/// signal leaks entries; removing the QPointer crashes.
QList<QPointer<ComposeWindow>> m_composers;
+ /// The open calendar window, or null. Parentless and WA_DeleteOnClose, like
+ /// a composer, so the QPointer is what nulls it when the user closes it.
+ QPointer<CalendarWindow> m_calendar;
+
/// Confirmed tag mutations not yet known to have reached the mail store.
///
/// A count of its own rather than QUndoStack::isClean(), which cannot serve
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index e87843d..5446780 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -16,6 +16,8 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
+#include <algorithm>
+
#include <QProcess>
#include <QtTest>
@@ -44,6 +46,7 @@
#include <QTreeView>
#include <QTimer>
+#include "calendarwindow.h"
#include "config.h"
#include "keymap.h"
#include "mainwindow.h"
@@ -618,6 +621,10 @@ private slots:
void aConversationStaysWhileAnyMessageMatches();
void aConversationLeavesWhenItsUnionEmpties();
+ // Item 206, task 15: the calendar opens from the View menu and toolbar,
+ // and does nothing at all when no calendars_dir is configured.
+ void theCalendarActionWithNoCalendarOpensNothing();
+
private:
/// Owns the throwaway lock table init() points every test at. A pointer
/// rather than a value because it is rebuilt per test, and QTemporaryDir
@@ -8817,6 +8824,25 @@ void TestMainWindow::theSpamActionCarriesTheBugIconWithAFallback()
QCOMPARE(names.second, QStringLiteral("mail-mark-junk"));
}
+void TestMainWindow::theCalendarActionWithNoCalendarOpensNothing()
+{
+ // Item 206, task 15. The default test config leaves calendars_dir empty, so
+ // the feature is off: the action must exist (it is registered like any
+ // other) but triggering it may not crash or open an empty window.
+ const Config config;
+ MainWindow window(config);
+ auto *action = window.findChild<QAction *>(QStringLiteral("calendar"));
+ QVERIFY2(action, "no action named calendar");
+
+ action->trigger();
+
+ const QWidgetList tops = QApplication::topLevelWidgets();
+ QVERIFY2(std::none_of(tops.cbegin(), tops.cend(), [](QWidget *w) {
+ return qobject_cast<CalendarWindow *>(w) != nullptr;
+ }),
+ "a calendar window opened with no calendars_dir configured");
+}
+
// Constructing a MainWindow needs a QApplication and a platform plugin. The
// test has no display under ctest, so it runs offscreen unless the caller
// asked for something else.