Explorar el Código

Calendar: Implement saving, loading, and displaying of calendars

The user can now save, load, and view calendars. A calendar is made up
of an array of events which are saved in a JSON file. In the future we
should implement the iCalendar standard instead of using a custom
format.
Monroe Clinton hace 2 años
padre
commit
1b5b1e4153

+ 1 - 0
Base/etc/FileIconProvider.ini

@@ -30,6 +30,7 @@ ruby=*.rb
 shell=*.sh,*.bash,*.zsh
 sound=*.wav,*.flac,*.mp3,*.qoa
 spreadsheet=*.sheets,*.csv
+calendar=*.cal
 text=*.txt
 truetype=*.ttf
 pixelpaint=*.pp

+ 3 - 0
Base/res/apps/Calendar.af

@@ -2,3 +2,6 @@
 Name=Calendar
 Executable=/bin/Calendar
 Category=Office
+
+[Launcher]
+FileTypes=cal

BIN
Base/res/icons/16x16/filetype-calendar.png


BIN
Base/res/icons/32x32/filetype-calendar.png


+ 24 - 4
Userland/Applications/Calendar/AddEventDialog.cpp

@@ -1,6 +1,6 @@
 /*
  * Copyright (c) 2019-2020, Ryan Grieb <ryan.m.grieb@gmail.com>
- * Copyright (c) 2022, the SerenityOS developers.
+ * Copyright (c) 2022-2023, the SerenityOS developers.
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -19,9 +19,12 @@
 #include <LibGfx/Color.h>
 #include <LibGfx/Font/FontDatabase.h>
 
-AddEventDialog::AddEventDialog(Core::DateTime date_time, Window* parent_window)
+namespace Calendar {
+
+AddEventDialog::AddEventDialog(Core::DateTime date_time, EventManager& event_manager, Window* parent_window)
     : Dialog(parent_window)
     , m_date_time(date_time)
+    , m_event_manager(event_manager)
 {
     resize(158, 130);
     set_title("Add Event");
@@ -42,6 +45,7 @@ AddEventDialog::AddEventDialog(Core::DateTime date_time, Window* parent_window)
     add_label.set_font(Gfx::FontDatabase::default_font().bold_variant());
 
     auto& event_title_textbox = top_container.add<GUI::TextBox>();
+    event_title_textbox.set_name("event_title_textbox");
     event_title_textbox.set_fixed_height(20);
 
     auto& middle_container = widget->add<GUI::Widget>();
@@ -92,8 +96,9 @@ AddEventDialog::AddEventDialog(Core::DateTime date_time, Window* parent_window)
     button_container.add_spacer().release_value_but_fixme_should_propagate_errors();
     auto& ok_button = button_container.add<GUI::Button>("OK"_short_string);
     ok_button.set_fixed_size(80, 20);
-    ok_button.on_click = [this](auto) {
-        dbgln("TODO: Add event icon on specific tile");
+    ok_button.on_click = [&](auto) {
+        add_event_to_calendar().release_value_but_fixme_should_propagate_errors();
+
         done(ExecResult::OK);
     };
 
@@ -110,6 +115,19 @@ AddEventDialog::AddEventDialog(Core::DateTime date_time, Window* parent_window)
     event_title_textbox.set_focus(true);
 }
 
+ErrorOr<void> AddEventDialog::add_event_to_calendar()
+{
+    JsonObject event;
+    auto start_date = TRY(String::formatted("{}-{:0>2d}-{:0>2d}", m_date_time.year(), m_date_time.month(), m_date_time.day()));
+    auto summary = find_descendant_of_type_named<GUI::TextBox>("event_title_textbox")->get_text();
+    event.set("start_date", JsonValue(start_date));
+    event.set("summary", JsonValue(summary));
+    TRY(m_event_manager.add_event(event));
+    m_event_manager.set_dirty(true);
+
+    return {};
+}
+
 int AddEventDialog::MonthListModel::row_count(const GUI::ModelIndex&) const
 {
     return 12;
@@ -176,3 +194,5 @@ GUI::Variant AddEventDialog::MeridiemListModel::data(const GUI::ModelIndex& inde
     }
     return {};
 }
+
+}

+ 12 - 4
Userland/Applications/Calendar/AddEventDialog.h

@@ -1,30 +1,35 @@
 /*
  * Copyright (c) 2019-2020, Ryan Grieb <ryan.m.grieb@gmail.com>
- * Copyright (c) 2022, the SerenityOS developers.
+ * Copyright (c) 2022-2023, the SerenityOS developers.
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
 #pragma once
 
+#include "EventManager.h"
 #include <LibGUI/Calendar.h>
 #include <LibGUI/Dialog.h>
 #include <LibGUI/Model.h>
 #include <LibGUI/Window.h>
 
+namespace Calendar {
+
 class AddEventDialog final : public GUI::Dialog {
     C_OBJECT(AddEventDialog)
 public:
     virtual ~AddEventDialog() override = default;
 
-    static void show(Core::DateTime date_time, Window* parent_window = nullptr)
+    static void show(Core::DateTime date_time, EventManager& event_manager, Window* parent_window = nullptr)
     {
-        auto dialog = AddEventDialog::construct(date_time, parent_window);
+        auto dialog = AddEventDialog::construct(date_time, event_manager, parent_window);
         dialog->exec();
     }
 
 private:
-    AddEventDialog(Core::DateTime date_time, Window* parent_window = nullptr);
+    AddEventDialog(Core::DateTime date_time, EventManager& event_manager, Window* parent_window = nullptr);
+
+    ErrorOr<void> add_event_to_calendar();
 
     class MonthListModel final : public GUI::Model {
     public:
@@ -65,4 +70,7 @@ private:
     };
 
     Core::DateTime m_date_time;
+    EventManager& m_event_manager;
 };
+
+}

+ 4 - 1
Userland/Applications/Calendar/CMakeLists.txt

@@ -7,6 +7,9 @@ compile_gml(CalendarWindow.gml CalendarWindowGML.h calendar_window_gml)
 
 set(SOURCES
     AddEventDialog.cpp
+    CalendarWidget.cpp
+    EventCalendar.cpp
+    EventManager.cpp
     main.cpp
 )
 
@@ -15,4 +18,4 @@ set(GENERATED_SOURCES
 )
 
 serenity_app(Calendar ICON app-calendar)
-target_link_libraries(Calendar PRIVATE LibConfig LibCore LibGfx LibGUI LibMain)
+target_link_libraries(Calendar PRIVATE LibConfig LibCore LibFileSystem LibFileSystemAccessClient LibGfx LibGUI LibMain)

+ 275 - 0
Userland/Applications/Calendar/CalendarWidget.cpp

@@ -0,0 +1,275 @@
+/*
+ * Copyright (c) 2023, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CalendarWidget.h"
+#include "AddEventDialog.h"
+#include <AK/JsonParser.h>
+#include <AK/LexicalPath.h>
+#include <Applications/Calendar/CalendarWindowGML.h>
+#include <LibConfig/Client.h>
+#include <LibCore/System.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Calendar.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Menubar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Process.h>
+#include <LibGUI/Toolbar.h>
+#include <LibGUI/Window.h>
+#include <LibMain/Main.h>
+
+namespace Calendar {
+
+ErrorOr<NonnullRefPtr<CalendarWidget>> CalendarWidget::create(GUI::Window* parent_window)
+{
+    auto widget = TRY(AK::adopt_nonnull_ref_or_enomem(new (nothrow) CalendarWidget));
+    TRY(widget->load_from_gml(calendar_window_gml));
+
+    widget->m_event_calendar = widget->find_descendant_of_type_named<EventCalendar>("calendar");
+    widget->create_on_events_change();
+
+    auto toolbar = widget->find_descendant_of_type_named<GUI::Toolbar>("toolbar");
+    auto calendar = widget->m_event_calendar;
+
+    auto prev_date_action = TRY(widget->create_prev_date_action());
+    auto next_date_action = TRY(widget->create_next_date_action());
+
+    auto add_event_action = TRY(widget->create_add_event_action());
+
+    auto jump_to_action = TRY(widget->create_jump_to_action());
+
+    auto view_month_action = TRY(widget->create_view_month_action());
+    view_month_action->set_checked(true);
+
+    auto view_year_action = TRY(widget->create_view_year_action());
+    auto view_type_action_group = make<GUI::ActionGroup>();
+
+    view_type_action_group->set_exclusive(true);
+    view_type_action_group->add_action(*view_month_action);
+    view_type_action_group->add_action(*view_year_action);
+    auto default_view = Config::read_string("Calendar"sv, "View"sv, "DefaultView"sv, "Month"sv);
+    if (default_view == "Year")
+        view_year_action->set_checked(true);
+
+    auto open_settings_action = TRY(widget->create_open_settings_action());
+
+    (void)TRY(toolbar->try_add_action(prev_date_action));
+    (void)TRY(toolbar->try_add_action(next_date_action));
+    TRY(toolbar->try_add_separator());
+    (void)TRY(toolbar->try_add_action(jump_to_action));
+    (void)TRY(toolbar->try_add_action(add_event_action));
+    TRY(toolbar->try_add_separator());
+    (void)TRY(toolbar->try_add_action(view_month_action));
+    (void)TRY(toolbar->try_add_action(view_year_action));
+    (void)TRY(toolbar->try_add_action(open_settings_action));
+
+    widget->create_on_tile_doubleclick();
+
+    calendar->on_month_click = [&] {
+        view_month_action->set_checked(true);
+    };
+
+    auto new_calendar_action = TRY(widget->create_new_calendar_action());
+    auto open_calendar_action = widget->create_open_calendar_action();
+
+    auto save_as_action = widget->create_save_as_action();
+    auto save_action = widget->create_save_action(save_as_action);
+
+    auto& file_menu = parent_window->add_menu("&File"_short_string);
+    file_menu.add_action(open_settings_action);
+    file_menu.add_action(new_calendar_action);
+    file_menu.add_action(open_calendar_action);
+    file_menu.add_action(save_as_action);
+    file_menu.add_action(save_action);
+
+    TRY(file_menu.try_add_separator());
+
+    TRY(file_menu.try_add_action(GUI::CommonActions::make_quit_action([](auto&) {
+        GUI::Application::the()->quit();
+    })));
+
+    auto& event_menu = parent_window->add_menu("&Event"_short_string);
+    event_menu.add_action(add_event_action);
+
+    auto view_menu = TRY(parent_window->try_add_menu("&View"_short_string));
+    TRY(view_menu->try_add_action(*view_month_action));
+    TRY(view_menu->try_add_action(*view_year_action));
+
+    auto help_menu = TRY(parent_window->try_add_menu("&Help"_short_string));
+    TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(parent_window)));
+    TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Calendar", TRY(GUI::Icon::try_create_default_icon("app-calendar"sv)), parent_window)));
+
+    return widget;
+}
+
+void CalendarWidget::create_on_events_change()
+{
+    m_event_calendar->event_manager().on_events_change = [&]() {
+        m_event_calendar->repaint();
+        window()->set_modified(true);
+        update_window_title();
+    };
+}
+
+void CalendarWidget::load_file(FileSystemAccessClient::File file)
+{
+    auto result = m_event_calendar->event_manager().load_file(file);
+    if (result.is_error()) {
+        GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Cannot load file: {}", result.error()));
+        return;
+    }
+
+    window()->set_modified(false);
+    update_window_title();
+}
+
+NonnullRefPtr<GUI::Action> CalendarWidget::create_save_action(GUI::Action& save_as_action)
+{
+    return GUI::CommonActions::make_save_action([&](auto&) {
+        if (current_filename().is_empty()) {
+            save_as_action.activate();
+            return;
+        }
+
+        auto response = FileSystemAccessClient::Client::the().request_file(window(), current_filename().to_deprecated_string(), Core::File::OpenMode::Write);
+        if (response.is_error())
+            return;
+
+        auto result = m_event_calendar->event_manager().save(response.value());
+        if (result.is_error()) {
+            GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Cannot save file: {}", result.error()));
+            return;
+        }
+
+        window()->set_modified(false);
+        update_window_title();
+    });
+}
+
+NonnullRefPtr<GUI::Action> CalendarWidget::create_save_as_action()
+{
+    return GUI::CommonActions::make_save_as_action([&](auto&) {
+        auto response = FileSystemAccessClient::Client::the().save_file(window(), "calendar", "cal");
+        if (response.is_error())
+            return;
+
+        auto result = m_event_calendar->event_manager().save(response.value());
+        if (result.is_error()) {
+            GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Cannot save file: {}", result.error()));
+            return;
+        }
+
+        window()->set_modified(false);
+        update_window_title();
+    });
+}
+
+ErrorOr<NonnullRefPtr<GUI::Action>> CalendarWidget::create_new_calendar_action()
+{
+    return GUI::Action::create("&New Calendar", {}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-calendar.png"sv)), [&](const GUI::Action&) {
+        auto response = FileSystemAccessClient::Client::the().save_file(window(), "calendar", "cal", Core::File::OpenMode::Write);
+
+        if (response.is_error())
+            return;
+
+        m_event_calendar->event_manager().clear();
+
+        auto result = m_event_calendar->event_manager().save(response.value());
+        if (result.is_error()) {
+            GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Cannot save file: {}", result.error()));
+            return;
+        }
+
+        update_window_title();
+    });
+}
+
+NonnullRefPtr<GUI::Action> CalendarWidget::create_open_calendar_action()
+{
+    return GUI::CommonActions::make_open_action([&](auto&) {
+        auto response = FileSystemAccessClient::Client::the().open_file(window());
+        if (response.is_error())
+            return;
+        (void)load_file(response.release_value());
+    });
+}
+
+ErrorOr<NonnullRefPtr<GUI::Action>> CalendarWidget::create_prev_date_action()
+{
+    return GUI::Action::create({}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png"sv)), [&](const GUI::Action&) {
+        m_event_calendar->show_previous_date();
+    });
+}
+
+ErrorOr<NonnullRefPtr<GUI::Action>> CalendarWidget::create_next_date_action()
+{
+    return GUI::Action::create({}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"sv)), [&](const GUI::Action&) {
+        m_event_calendar->show_next_date();
+    });
+}
+
+void CalendarWidget::update_window_title()
+{
+    StringBuilder builder;
+    if (current_filename().is_empty())
+        builder.append("Untitled"sv);
+    else
+        builder.append(current_filename());
+    builder.append("[*] - Calendar"sv);
+
+    window()->set_title(builder.to_deprecated_string());
+}
+
+ErrorOr<NonnullRefPtr<GUI::Action>> CalendarWidget::create_add_event_action()
+{
+    return GUI::Action::create("&Add Event", {}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/add-event.png"sv)), [&](const GUI::Action&) {
+        AddEventDialog::show(m_event_calendar->selected_date(), m_event_calendar->event_manager(), window());
+    });
+}
+
+ErrorOr<NonnullRefPtr<GUI::Action>> CalendarWidget::create_jump_to_action()
+{
+    return GUI::Action::create("Jump to &Today", {}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/calendar-date.png"sv)), [&](const GUI::Action&) {
+        m_event_calendar->set_selected_date(Core::DateTime::now());
+        m_event_calendar->update_tiles(Core::DateTime::now().year(), Core::DateTime::now().month());
+    });
+}
+
+ErrorOr<NonnullRefPtr<GUI::Action>> CalendarWidget::create_view_month_action()
+{
+    return GUI::Action::create_checkable("&Month View", { Mod_Ctrl, KeyCode::Key_1 }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/calendar-month-view.png"sv)), [&](const GUI::Action&) {
+        if (m_event_calendar->mode() == GUI::Calendar::Year)
+            m_event_calendar->toggle_mode();
+    });
+}
+
+ErrorOr<NonnullRefPtr<GUI::Action>> CalendarWidget::create_view_year_action()
+{
+    return GUI::Action::create_checkable("&Year View", { Mod_Ctrl, KeyCode::Key_2 }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/icon-view.png"sv)), [&](const GUI::Action&) {
+        if (m_event_calendar->mode() == GUI::Calendar::Month)
+            m_event_calendar->toggle_mode();
+    });
+}
+
+ErrorOr<NonnullRefPtr<GUI::Action>> CalendarWidget::create_open_settings_action()
+{
+    return GUI::Action::create("Calendar &Settings", {}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-settings.png"sv)), [&](GUI::Action const&) {
+        GUI::Process::spawn_or_show_error(window(), "/bin/CalendarSettings"sv);
+    });
+}
+
+void CalendarWidget::create_on_tile_doubleclick()
+{
+    m_event_calendar->on_tile_doubleclick = [&] {
+        AddEventDialog::show(m_event_calendar->selected_date(), m_event_calendar->event_manager(), window());
+    };
+}
+
+}

+ 48 - 0
Userland/Applications/Calendar/CalendarWidget.h

@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2023, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include "EventCalendar.h"
+#include <AK/NonnullRefPtr.h>
+#include <LibFileSystemAccessClient/Client.h>
+#include <LibGUI/Calendar.h>
+#include <LibGUI/Widget.h>
+
+namespace Calendar {
+
+class CalendarWidget final : public GUI::Widget {
+    C_OBJECT(CalendarWidget);
+
+public:
+    static ErrorOr<NonnullRefPtr<CalendarWidget>> create(GUI::Window*);
+    virtual ~CalendarWidget() override = default;
+
+    void update_window_title();
+    void load_file(FileSystemAccessClient::File file);
+
+private:
+    void create_on_tile_doubleclick();
+
+    String const& current_filename() const { return m_event_calendar->event_manager().current_filename(); }
+
+    void create_on_events_change();
+    NonnullRefPtr<GUI::Action> create_save_as_action();
+    NonnullRefPtr<GUI::Action> create_save_action(GUI::Action& save_as_action);
+    ErrorOr<NonnullRefPtr<GUI::Action>> create_new_calendar_action();
+    NonnullRefPtr<GUI::Action> create_open_calendar_action();
+    ErrorOr<NonnullRefPtr<GUI::Action>> create_prev_date_action();
+    ErrorOr<NonnullRefPtr<GUI::Action>> create_next_date_action();
+    ErrorOr<NonnullRefPtr<GUI::Action>> create_add_event_action();
+    ErrorOr<NonnullRefPtr<GUI::Action>> create_jump_to_action();
+    ErrorOr<NonnullRefPtr<GUI::Action>> create_view_month_action();
+    ErrorOr<NonnullRefPtr<GUI::Action>> create_view_year_action();
+    ErrorOr<NonnullRefPtr<GUI::Action>> create_open_settings_action();
+
+    RefPtr<EventCalendar> m_event_calendar;
+};
+
+}

+ 1 - 1
Userland/Applications/Calendar/CalendarWindow.gml

@@ -10,7 +10,7 @@
         }
     }
 
-    @GUI::Calendar {
+    @::Calendar::EventCalendar {
         name: "calendar"
     }
 }

+ 52 - 0
Userland/Applications/Calendar/EventCalendar.cpp

@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2023, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "EventCalendar.h"
+#include <LibGUI/Painter.h>
+#include <LibGfx/Font/FontDatabase.h>
+#include <LibGfx/Palette.h>
+
+REGISTER_WIDGET(::Calendar, EventCalendar);
+
+namespace Calendar {
+
+static constexpr int tile_breakpoint = 50;
+
+EventCalendar::EventCalendar(Core::DateTime date_time, Mode mode)
+    : Calendar(date_time, mode)
+    , m_event_manager(EventManager::create())
+{
+}
+
+void EventCalendar::paint_tile(GUI::Painter& painter, GUI::Calendar::Tile& tile, Gfx::IntRect& tile_rect, int x_offset, int y_offset, int day_offset)
+{
+    Calendar::paint_tile(painter, tile, tile_rect, x_offset, y_offset, day_offset);
+
+    auto events = m_event_manager->events();
+
+    if (tile.width > tile_breakpoint && tile.height > tile_breakpoint) {
+        auto index = 0;
+        auto font_height = font().x_height();
+        events.for_each([&](JsonValue const& value) {
+            auto const& event = value.as_object();
+
+            if (!event.has("start_date"sv) || !event.has("summary"sv))
+                return;
+
+            auto start_date = event.get("start_date"sv).value().to_deprecated_string();
+            auto summary = event.get("summary"sv).value().to_deprecated_string();
+
+            if (start_date == DeprecatedString::formatted("{}-{:0>2d}-{:0>2d}", tile.year, tile.month, tile.day)) {
+
+                auto text_rect = tile.rect.translated(4, 4 + (font_height + 8) * ++index);
+
+                painter.draw_text(text_rect, summary, Gfx::FontDatabase::default_font(), Gfx::TextAlignment::TopLeft, palette().base_text());
+            }
+        });
+    }
+}
+
+}

+ 34 - 0
Userland/Applications/Calendar/EventCalendar.h

@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include "EventManager.h"
+#include <LibFileSystemAccessClient/Client.h>
+#include <LibGUI/Calendar.h>
+
+namespace Calendar {
+
+class EventCalendar final : public GUI::Calendar {
+    C_OBJECT(EventCalendar);
+
+public:
+    virtual ~EventCalendar() override = default;
+
+    EventManager& event_manager() const { return *m_event_manager; }
+
+private:
+    EventCalendar(Core::DateTime date_time = Core::DateTime::now(), Mode mode = Month);
+
+    ErrorOr<void> save(FileSystemAccessClient::File& file);
+    ErrorOr<void> load_file(FileSystemAccessClient::File& file);
+
+    virtual void paint_tile(GUI::Painter&, GUI::Calendar::Tile&, Gfx::IntRect&, int x_offset, int y_offset, int day_offset) override;
+
+    OwnPtr<EventManager> m_event_manager;
+};
+
+}

+ 63 - 0
Userland/Applications/Calendar/EventManager.cpp

@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2023, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "EventManager.h"
+#include <AK/JsonParser.h>
+#include <LibConfig/Client.h>
+#include <LibFileSystemAccessClient/Client.h>
+
+namespace Calendar {
+
+EventManager::EventManager()
+{
+}
+
+OwnPtr<EventManager> EventManager::create()
+{
+    return adopt_own(*new EventManager());
+}
+
+ErrorOr<void> EventManager::add_event(JsonObject event)
+{
+    TRY(m_events.append(move(event)));
+    set_dirty(true);
+    on_events_change();
+
+    return {};
+}
+
+void EventManager::set_events(JsonArray events)
+{
+    m_events = move(events);
+    on_events_change();
+}
+
+ErrorOr<void> EventManager::save(FileSystemAccessClient::File& file)
+{
+    set_filename(file.filename());
+    set_dirty(false);
+
+    auto stream = file.release_stream();
+    TRY(stream->write_some(m_events.to_deprecated_string().bytes()));
+    stream->close();
+
+    return {};
+}
+
+ErrorOr<void> EventManager::load_file(FileSystemAccessClient::File& file)
+{
+    set_filename(file.filename());
+    set_dirty(false);
+
+    auto content = TRY(file.stream().read_until_eof());
+    auto events = TRY(AK::JsonParser(content).parse());
+
+    set_events(events.as_array());
+
+    return {};
+}
+
+}

+ 49 - 0
Userland/Applications/Calendar/EventManager.h

@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2023, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/JsonObject.h>
+#include <AK/JsonValue.h>
+#include <AK/Noncopyable.h>
+#include <AK/OwnPtr.h>
+#include <LibFileSystemAccessClient/Client.h>
+#include <LibGUI/Window.h>
+
+namespace Calendar {
+
+class EventManager {
+    AK_MAKE_NONCOPYABLE(EventManager);
+    AK_MAKE_NONMOVABLE(EventManager);
+
+public:
+    static OwnPtr<EventManager> create();
+
+    String const& current_filename() const { return m_current_filename; }
+    void set_filename(String const& filename) { m_current_filename = filename; }
+    bool dirty() const { return m_dirty; }
+    void set_dirty(bool dirty) { m_dirty = dirty; }
+
+    ErrorOr<void> save(FileSystemAccessClient::File& file);
+    ErrorOr<void> load_file(FileSystemAccessClient::File& file);
+    ErrorOr<void> add_event(JsonObject);
+    void set_events(JsonArray events);
+    void clear() { m_events.clear(); }
+
+    JsonArray const& events() const { return m_events; }
+
+    Function<void()> on_events_change;
+
+private:
+    explicit EventManager();
+
+    JsonArray m_events;
+
+    String m_current_filename;
+    bool m_dirty { false };
+};
+
+}

+ 33 - 86
Userland/Applications/Calendar/main.cpp

@@ -5,9 +5,12 @@
  */
 
 #include "AddEventDialog.h"
-#include <Applications/Calendar/CalendarWindowGML.h>
+#include "CalendarWidget.h"
 #include <LibConfig/Client.h>
+#include <LibCore/ArgsParser.h>
 #include <LibCore/System.h>
+#include <LibFileSystem/FileSystem.h>
+#include <LibFileSystemAccessClient/Client.h>
 #include <LibGUI/Action.h>
 #include <LibGUI/ActionGroup.h>
 #include <LibGUI/Application.h>
@@ -16,24 +19,42 @@
 #include <LibGUI/Icon.h>
 #include <LibGUI/Menu.h>
 #include <LibGUI/Menubar.h>
+#include <LibGUI/Painter.h>
 #include <LibGUI/Process.h>
 #include <LibGUI/Toolbar.h>
 #include <LibGUI/Window.h>
+#include <LibGfx/Font/FontDatabase.h>
+#include <LibGfx/Palette.h>
 #include <LibMain/Main.h>
 
 ErrorOr<int> serenity_main(Main::Arguments arguments)
 {
-    TRY(Core::System::pledge("stdio recvfd sendfd rpath proc exec unix"));
+    TRY(Core::System::pledge("stdio recvfd sendfd rpath wpath cpath proc exec unix"));
 
     auto app = TRY(GUI::Application::create(arguments));
 
+    StringView filename;
+
+    Core::ArgsParser args_parser;
+    args_parser.add_positional_argument(filename, "File to read from", "file", Core::ArgsParser::Required::No);
+
+    args_parser.parse(arguments);
+
+    if (!filename.is_empty()) {
+        if (!FileSystem::exists(filename) || FileSystem::is_directory(filename)) {
+            warnln("File does not exist or is a directory: {}", filename);
+            return 1;
+        }
+    }
+
     Config::pledge_domain("Calendar");
     Config::monitor_domain("Calendar");
 
-    TRY(Core::System::pledge("stdio recvfd sendfd rpath proc exec"));
+    TRY(Core::System::pledge("stdio recvfd sendfd rpath wpath cpath proc exec unix"));
     TRY(Core::System::unveil("/etc/timezone", "r"));
     TRY(Core::System::unveil("/res", "r"));
     TRY(Core::System::unveil("/bin/CalendarSettings", "x"));
+    TRY(Core::System::unveil("/tmp/session/%sid/portal/filesystemaccess", "rw"));
     TRY(Core::System::unveil(nullptr, nullptr));
 
     auto app_icon = TRY(GUI::Icon::try_create_default_icon("app-calendar"sv));
@@ -42,92 +63,18 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
     window->resize(600, 480);
     window->set_icon(app_icon.bitmap_for_size(16));
 
-    auto main_widget = TRY(window->set_main_widget<GUI::Widget>());
-    TRY(main_widget->load_from_gml(calendar_window_gml));
-
-    auto toolbar = main_widget->find_descendant_of_type_named<GUI::Toolbar>("toolbar");
-    auto calendar = main_widget->find_descendant_of_type_named<GUI::Calendar>("calendar");
-
-    auto prev_date_action = GUI::Action::create({}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png"sv)), [&](const GUI::Action&) {
-        calendar->show_previous_date();
-    });
-
-    auto next_date_action = GUI::Action::create({}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"sv)), [&](const GUI::Action&) {
-        calendar->show_next_date();
-    });
-
-    auto add_event_action = GUI::Action::create("&Add Event", {}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/add-event.png"sv)), [&](const GUI::Action&) {
-        AddEventDialog::show(calendar->selected_date(), window);
-    });
-
-    auto jump_to_action = GUI::Action::create("Jump to &Today", {}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/calendar-date.png"sv)), [&](const GUI::Action&) {
-        calendar->set_selected_date(Core::DateTime::now());
-        calendar->update_tiles(Core::DateTime::now().year(), Core::DateTime::now().month());
-    });
-
-    auto view_month_action = GUI::Action::create_checkable("&Month View", { Mod_Ctrl, KeyCode::Key_1 }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/calendar-month-view.png"sv)), [&](const GUI::Action&) {
-        if (calendar->mode() == GUI::Calendar::Year)
-            calendar->toggle_mode();
-    });
-    view_month_action->set_checked(true);
-
-    auto view_year_action = GUI::Action::create_checkable("&Year View", { Mod_Ctrl, KeyCode::Key_2 }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/icon-view.png"sv)), [&](const GUI::Action&) {
-        if (calendar->mode() == GUI::Calendar::Month)
-            calendar->toggle_mode();
-    });
+    auto calendar_widget = TRY(Calendar::CalendarWidget::create(window));
+    window->set_main_widget(calendar_widget);
 
-    auto view_type_action_group = make<GUI::ActionGroup>();
-    view_type_action_group->set_exclusive(true);
-    view_type_action_group->add_action(*view_month_action);
-    view_type_action_group->add_action(*view_year_action);
-    auto default_view = Config::read_string("Calendar"sv, "View"sv, "DefaultView"sv, "Month"sv);
-    if (default_view == "Year")
-        view_year_action->set_checked(true);
-
-    auto open_settings_action = GUI::Action::create("Calendar &Settings", {}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-settings.png"sv)), [&](GUI::Action const&) {
-        GUI::Process::spawn_or_show_error(window, "/bin/CalendarSettings"sv);
-    });
-
-    (void)TRY(toolbar->try_add_action(prev_date_action));
-    (void)TRY(toolbar->try_add_action(next_date_action));
-    TRY(toolbar->try_add_separator());
-    (void)TRY(toolbar->try_add_action(jump_to_action));
-    (void)TRY(toolbar->try_add_action(add_event_action));
-    TRY(toolbar->try_add_separator());
-    (void)TRY(toolbar->try_add_action(view_month_action));
-    (void)TRY(toolbar->try_add_action(view_year_action));
-    (void)TRY(toolbar->try_add_action(open_settings_action));
-
-    calendar->on_tile_doubleclick = [&] {
-        AddEventDialog::show(calendar->selected_date(), window);
-    };
-
-    calendar->on_month_click = [&] {
-        view_month_action->set_checked(true);
-    };
-
-    auto& file_menu = window->add_menu("&File"_short_string);
-    file_menu.add_action(GUI::Action::create("&Add Event", { Mod_Ctrl | Mod_Shift, Key_E }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/add-event.png"sv)),
-        [&](const GUI::Action&) {
-            AddEventDialog::show(calendar->selected_date(), window);
-        }));
-    file_menu.add_action(open_settings_action);
-
-    TRY(file_menu.try_add_separator());
-
-    TRY(file_menu.try_add_action(GUI::CommonActions::make_quit_action([](auto&) {
-        GUI::Application::the()->quit();
-    })));
-
-    auto view_menu = TRY(window->try_add_menu("&View"_short_string));
-    TRY(view_menu->try_add_action(*view_month_action));
-    TRY(view_menu->try_add_action(*view_year_action));
+    window->show();
 
-    auto help_menu = TRY(window->try_add_menu("&Help"_short_string));
-    TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(window)));
-    TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Calendar", app_icon, window)));
+    if (!filename.is_empty()) {
+        auto response = FileSystemAccessClient::Client::the().request_file_read_only_approved(window, filename);
+        if (!response.is_error()) {
+            calendar_widget->load_file(response.release_value());
+        }
+    }
 
-    window->show();
     app->exec();
 
     return 0;

+ 76 - 63
Userland/Libraries/LibGUI/Calendar.cpp

@@ -396,8 +396,6 @@ void Calendar::paint_event(GUI::PaintEvent& event)
 
     painter.translate(frame_thickness(), frame_thickness());
 
-    int width = unadjusted_tile_size().width();
-    int height = unadjusted_tile_size().height();
     int x_offset = 0;
     int y_offset = 0;
 
@@ -492,7 +490,6 @@ void Calendar::paint_event(GUI::PaintEvent& event)
             if (j > 0)
                 y_offset += m_tiles[0][(j - 1) * 7].height + 1;
             for (int k = 0; k < 7; k++) {
-                bool is_weekend = is_day_in_weekend((DayOfWeek)((k + to_underlying(m_first_day_of_week)) % 7));
                 if (k > 0)
                     x_offset += m_tiles[0][k - 1].width + 1;
                 auto tile_rect = Gfx::IntRect(
@@ -502,48 +499,8 @@ void Calendar::paint_event(GUI::PaintEvent& event)
                     m_tiles[0][i].height);
                 m_tiles[0][i].rect = tile_rect.translated(frame_thickness(), frame_thickness());
 
-                Color background_color = palette().base();
-
-                if (m_tiles[0][i].is_hovered || m_tiles[0][i].is_selected) {
-                    background_color = palette().hover_highlight();
-                } else if (is_weekend) {
-                    background_color = palette().gutter();
-                }
-
-                painter.fill_rect(tile_rect, background_color);
+                paint_tile(painter, m_tiles[0][i], tile_rect, x_offset, y_offset, k);
 
-                auto text_alignment = Gfx::TextAlignment::TopRight;
-                auto text_rect = Gfx::IntRect(
-                    x_offset,
-                    y_offset + 4,
-                    m_tiles[0][i].width - 4,
-                    font().pixel_size_rounded_up() + 4);
-
-                if (width > 150 && height > 150) {
-                    set_font(extra_large_font);
-                } else if (width > 100 && height > 100) {
-                    set_font(large_font);
-                } else if (width > 50 && height > 50) {
-                    set_font(medium_font);
-                } else if (width >= 30 && height >= 30) {
-                    set_font(small_font);
-                } else {
-                    set_font(small_font);
-                    text_alignment = Gfx::TextAlignment::Center;
-                    text_rect = Gfx::IntRect(tile_rect);
-                }
-
-                auto display_date = DeprecatedString::number(m_tiles[0][i].day);
-                if (m_tiles[0][i].is_selected && (width < 30 || height < 30))
-                    painter.draw_rect(tile_rect, palette().base_text());
-
-                if (m_tiles[0][i].is_today && !m_tiles[0][i].is_outside_selected_month) {
-                    painter.draw_text(text_rect, display_date, font().bold_variant(), text_alignment, palette().base_text());
-                } else if (m_tiles[0][i].is_outside_selected_month) {
-                    painter.draw_text(text_rect, display_date, m_tiles[0][i].is_today ? font().bold_variant() : font(), text_alignment, Color::LightGray);
-                } else {
-                    painter.draw_text(text_rect, display_date, font(), text_alignment, palette().base_text());
-                }
                 i++;
             }
         }
@@ -631,26 +588,8 @@ void Calendar::paint_event(GUI::PaintEvent& event)
                         m_tiles[l][i].height);
                     m_tiles[l][i].rect = tile_rect.translated(frame_thickness(), frame_thickness());
 
-                    if (m_tiles[l][i].is_hovered || m_tiles[l][i].is_selected)
-                        painter.fill_rect(tile_rect, palette().hover_highlight());
-                    else
-                        painter.fill_rect(tile_rect, palette().base());
+                    paint_tile(painter, m_tiles[0][i], tile_rect, x_offset, y_offset, k);
 
-                    if (width > 50 && height > 50) {
-                        set_font(medium_font);
-                    } else {
-                        set_font(small_font);
-                    }
-
-                    auto display_date = DeprecatedString::number(m_tiles[l][i].day);
-                    if (m_tiles[l][i].is_selected)
-                        painter.draw_rect(tile_rect, palette().base_text());
-
-                    if (m_tiles[l][i].is_today && !m_tiles[l][i].is_outside_selected_month) {
-                        painter.draw_text(tile_rect, display_date, font().bold_variant(), Gfx::TextAlignment::Center, palette().base_text());
-                    } else if (!m_tiles[l][i].is_outside_selected_month) {
-                        painter.draw_text(tile_rect, display_date, font(), Gfx::TextAlignment::Center, palette().base_text());
-                    }
                     i++;
                 }
             }
@@ -658,6 +597,80 @@ void Calendar::paint_event(GUI::PaintEvent& event)
     }
 }
 
+void Calendar::paint_tile(GUI::Painter& painter, GUI::Calendar::Tile& tile, Gfx::IntRect& tile_rect, int x_offset, int y_offset, int day_offset)
+{
+    int width = unadjusted_tile_size().width();
+    int height = unadjusted_tile_size().height();
+
+    if (mode() == Month) {
+        bool is_weekend = is_day_in_weekend((DayOfWeek)((day_offset + to_underlying(m_first_day_of_week)) % 7));
+
+        Color background_color = palette().base();
+
+        if (tile.is_hovered || tile.is_selected) {
+            background_color = palette().hover_highlight();
+        } else if (is_weekend) {
+            background_color = palette().gutter();
+        }
+
+        painter.fill_rect(tile_rect, background_color);
+
+        auto text_alignment = Gfx::TextAlignment::TopRight;
+        auto text_rect = Gfx::IntRect(
+            x_offset,
+            y_offset + 4,
+            tile.width - 4,
+            font().pixel_size_rounded_up() + 4);
+
+        if (width > 150 && height > 150) {
+            set_font(extra_large_font);
+        } else if (width > 100 && height > 100) {
+            set_font(large_font);
+        } else if (width > 50 && height > 50) {
+            set_font(medium_font);
+        } else if (width >= 30 && height >= 30) {
+            set_font(small_font);
+        } else {
+            set_font(small_font);
+            text_alignment = Gfx::TextAlignment::Center;
+            text_rect = Gfx::IntRect(tile_rect);
+        }
+
+        auto display_date = DeprecatedString::number(tile.day);
+        if (tile.is_selected && (width < 30 || height < 30))
+            painter.draw_rect(tile_rect, palette().base_text());
+
+        if (tile.is_today && !tile.is_outside_selected_month) {
+            painter.draw_text(text_rect, display_date, font().bold_variant(), text_alignment, palette().base_text());
+        } else if (tile.is_outside_selected_month) {
+            painter.draw_text(text_rect, display_date, tile.is_today ? font().bold_variant() : font(), text_alignment, Color::LightGray);
+        } else {
+            painter.draw_text(text_rect, display_date, font(), text_alignment, palette().base_text());
+        }
+    } else {
+        if (tile.is_hovered || tile.is_selected)
+            painter.fill_rect(tile_rect, palette().hover_highlight());
+        else
+            painter.fill_rect(tile_rect, palette().base());
+
+        if (width > 50 && height > 50) {
+            set_font(medium_font);
+        } else {
+            set_font(small_font);
+        }
+
+        auto display_date = DeprecatedString::number(tile.day);
+        if (tile.is_selected)
+            painter.draw_rect(tile_rect, palette().base_text());
+
+        if (tile.is_today && !tile.is_outside_selected_month) {
+            painter.draw_text(tile_rect, display_date, font().bold_variant(), Gfx::TextAlignment::Center, palette().base_text());
+        } else if (!tile.is_outside_selected_month) {
+            painter.draw_text(tile_rect, display_date, font(), Gfx::TextAlignment::Center, palette().base_text());
+        }
+    }
+}
+
 void Calendar::leave_event(Core::Event&)
 {
     int months;

+ 20 - 16
Userland/Libraries/LibGUI/Calendar.h

@@ -17,12 +17,25 @@
 
 namespace GUI {
 
-class Calendar final
+class Calendar
     : public GUI::AbstractScrollableWidget
     , public Config::Listener {
     C_OBJECT(Calendar)
 
 public:
+    struct Tile {
+        unsigned year;
+        unsigned month;
+        unsigned day;
+        Gfx::IntRect rect;
+        int width { 0 };
+        int height { 0 };
+        bool is_today { false };
+        bool is_selected { false };
+        bool is_hovered { false };
+        bool is_outside_selected_month { false };
+    };
+
     enum Mode {
         Month,
         Year
@@ -35,6 +48,8 @@ public:
         YearOnly
     };
 
+    virtual ~Calendar() override = default;
+
     void set_selected_date(Core::DateTime date_time) { m_selected_date = date_time; }
     Core::DateTime selected_date() const { return m_selected_date; }
 
@@ -77,20 +92,21 @@ public:
 
     virtual void config_string_did_change(StringView, StringView, StringView, StringView) override;
     virtual void config_i32_did_change(StringView, StringView, StringView, i32 value) override;
+    virtual void paint_event(GUI::PaintEvent&) override;
+    virtual void paint_tile(GUI::Painter&, GUI::Calendar::Tile&, Gfx::IntRect&, int x_offset, int y_offset, int day_offset);
 
     Function<void()> on_scroll;
     Function<void()> on_tile_click;
     Function<void()> on_tile_doubleclick;
     Function<void()> on_month_click;
 
-private:
+protected:
     Calendar(Core::DateTime date_time = Core::DateTime::now(), Mode mode = Month);
-    virtual ~Calendar() override = default;
 
+private:
     static size_t day_of_week_index(DeprecatedString const&);
 
     virtual void resize_event(GUI::ResizeEvent&) override;
-    virtual void paint_event(GUI::PaintEvent&) override;
     virtual void mousemove_event(GUI::MouseEvent&) override;
     virtual void mousedown_event(MouseEvent&) override;
     virtual void mouseup_event(MouseEvent&) override;
@@ -127,18 +143,6 @@ private:
     };
     Vector<MonthTile> m_months;
 
-    struct Tile {
-        unsigned year;
-        unsigned month;
-        unsigned day;
-        Gfx::IntRect rect;
-        int width { 0 };
-        int height { 0 };
-        bool is_today { false };
-        bool is_selected { false };
-        bool is_hovered { false };
-        bool is_outside_selected_month { false };
-    };
     Vector<Tile> m_tiles[12];
 
     bool m_grid { true };