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.
This commit is contained in:
Monroe Clinton 2023-06-28 19:17:31 -06:00 committed by Andrew Kaster
parent db3e1b128c
commit 1b5b1e4153
Notes: sideshowbarker 2024-07-17 09:56:35 +09:00
17 changed files with 696 additions and 176 deletions

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

View file

@ -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 {};
}
}

View file

@ -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;
};
}

View file

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

View file

@ -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());
};
}
}

View file

@ -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;
};
}

View file

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

View file

@ -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());
}
});
}
}
}

View file

@ -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;
};
}

View file

@ -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 {};
}
}

View file

@ -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 };
};
}

View file

@ -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 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));
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)));
auto calendar_widget = TRY(Calendar::CalendarWidget::create(window));
window->set_main_widget(calendar_widget);
window->show();
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());
}
}
app->exec();
return 0;

View file

@ -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();
paint_tile(painter, m_tiles[0][i], tile_rect, x_offset, y_offset, k);
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);
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;

View file

@ -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 };