Serenity Operating System
1/*
2 * Copyright (c) 2021, Nick Vella <nick@nxk.io>
3 * Copyright (c) 2021, sin-ack <sin-ack@protonmail.com>
4 * Copyright (c) 2022, the SerenityOS developers.
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include "ProjectTemplatesModel.h"
10
11#include <AK/LexicalPath.h>
12#include <AK/QuickSort.h>
13#include <LibCore/DirIterator.h>
14#include <LibGUI/Variant.h>
15#include <LibGfx/TextAlignment.h>
16#include <stdio.h>
17
18namespace HackStudio {
19
20ProjectTemplatesModel::ProjectTemplatesModel()
21 : m_templates()
22 , m_mapping()
23{
24 auto watcher_or_error = Core::FileWatcher::create();
25 if (!watcher_or_error.is_error()) {
26 m_file_watcher = watcher_or_error.release_value();
27 m_file_watcher->on_change = [&](auto) {
28 invalidate();
29 };
30
31 auto watch_result = m_file_watcher->add_watch(
32 ProjectTemplate::templates_path(),
33 Core::FileWatcherEvent::Type::ChildCreated
34 | Core::FileWatcherEvent::Type::ChildDeleted);
35
36 if (watch_result.is_error()) {
37 warnln("Unable to watch templates directory, templates will not automatically refresh. Error: {}", watch_result.error());
38 }
39 } else {
40 warnln("Unable to watch templates directory, templates will not automatically refresh. Error: {}", watcher_or_error.error());
41 }
42
43 rescan_templates();
44}
45
46int ProjectTemplatesModel::row_count(const GUI::ModelIndex&) const
47{
48 return m_mapping.size();
49}
50
51int ProjectTemplatesModel::column_count(const GUI::ModelIndex&) const
52{
53 return Column::__Count;
54}
55
56DeprecatedString ProjectTemplatesModel::column_name(int column) const
57{
58 switch (column) {
59 case Column::Icon:
60 return "Icon";
61 case Column::Id:
62 return "ID";
63 case Column::Name:
64 return "Name";
65 }
66 VERIFY_NOT_REACHED();
67}
68
69GUI::Variant ProjectTemplatesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
70{
71 if (static_cast<size_t>(index.row()) >= m_mapping.size())
72 return {};
73
74 if (role == GUI::ModelRole::TextAlignment)
75 return Gfx::TextAlignment::CenterLeft;
76
77 if (role == GUI::ModelRole::Display) {
78 switch (index.column()) {
79 case Column::Name:
80 return m_mapping[index.row()]->name();
81 case Column::Id:
82 return m_mapping[index.row()]->id();
83 }
84 }
85
86 if (role == GUI::ModelRole::Icon) {
87 return m_mapping[index.row()]->icon();
88 }
89
90 return {};
91}
92
93RefPtr<ProjectTemplate> ProjectTemplatesModel::template_for_index(const GUI::ModelIndex& index)
94{
95 if (static_cast<size_t>(index.row()) >= m_mapping.size())
96 return {};
97
98 return m_mapping[index.row()];
99}
100
101void ProjectTemplatesModel::update()
102{
103 rescan_templates();
104 did_update();
105}
106
107void ProjectTemplatesModel::rescan_templates()
108{
109 m_templates.clear();
110
111 // Iterate over template manifest INI files in the templates path
112 Core::DirIterator di(ProjectTemplate::templates_path(), Core::DirIterator::SkipDots);
113 if (di.has_error()) {
114 warnln("DirIterator: {}", di.error());
115 return;
116 }
117
118 while (di.has_next()) {
119 auto full_path = LexicalPath(di.next_full_path());
120 if (!full_path.has_extension(".ini"sv))
121 continue;
122
123 auto project_template = ProjectTemplate::load_from_manifest(full_path.string());
124 if (!project_template) {
125 warnln("Template manifest {} is invalid.", full_path.string());
126 continue;
127 }
128
129 m_templates.append(project_template.release_nonnull());
130 }
131
132 // Enumerate the loaded projects into a sorted mapping, by priority value descending.
133 m_mapping.clear();
134 for (auto& project_template : m_templates)
135 m_mapping.append(project_template);
136 quick_sort(m_mapping, [](auto a, auto b) {
137 return a->priority() > b->priority();
138 });
139}
140
141}