Serenity Operating System
1/*
2 * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "ManualModel.h"
8#include <AK/Try.h>
9#include <AK/Utf8View.h>
10#include <LibManual/Node.h>
11#include <LibManual/PageNode.h>
12#include <LibManual/Path.h>
13#include <LibManual/SectionNode.h>
14
15ManualModel::ManualModel()
16{
17 m_section_open_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/book-open.png"sv).release_value_but_fixme_should_propagate_errors());
18 m_section_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/book.png"sv).release_value_but_fixme_should_propagate_errors());
19 m_page_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-unknown.png"sv).release_value_but_fixme_should_propagate_errors());
20}
21
22Optional<GUI::ModelIndex> ManualModel::index_from_path(StringView path) const
23{
24 // The first slice removes the man pages base path plus the `/man` from the main section subdirectory.
25 // The second slice removes the trailing `.md`.
26 auto path_without_base = path.substring_view(Manual::manual_base_path.string().length() + 4);
27 auto url = URL::create_with_help_scheme(path_without_base.substring_view(0, path_without_base.length() - 3), {}, "man");
28
29 auto maybe_page = Manual::Node::try_find_from_help_url(url);
30 if (maybe_page.is_error())
31 return {};
32
33 auto page = maybe_page.release_value();
34 // Main section
35 if (page->parent() == nullptr) {
36 for (size_t section = 0; section < Manual::number_of_sections; ++section) {
37 auto main_section_index = index(static_cast<int>(section), 0);
38 if (main_section_index.internal_data() == page.ptr())
39 return main_section_index;
40 }
41 return {};
42 }
43 auto maybe_siblings = page->parent()->children();
44 if (maybe_siblings.is_error())
45 return {};
46 auto siblings = maybe_siblings.release_value();
47 for (size_t row = 0; row < siblings.size(); ++row) {
48 if (siblings[row] == page)
49 return create_index(static_cast<int>(row), 0, page.ptr());
50 }
51
52 return {};
53}
54
55Optional<String> ManualModel::page_name(const GUI::ModelIndex& index) const
56{
57 if (!index.is_valid())
58 return {};
59 auto* node = static_cast<Manual::Node const*>(index.internal_data());
60 if (!node->is_page())
61 return {};
62 auto* page = static_cast<Manual::PageNode const*>(node);
63 auto path = page->name();
64 if (path.is_error())
65 return {};
66 return path.release_value();
67}
68
69Optional<String> ManualModel::page_path(const GUI::ModelIndex& index) const
70{
71 if (!index.is_valid())
72 return {};
73 auto* node = static_cast<Manual::Node const*>(index.internal_data());
74 auto page = node->document();
75 if (!page)
76 return {};
77 auto path = page->path();
78 if (path.is_error())
79 return {};
80 return path.release_value();
81}
82
83ErrorOr<StringView> ManualModel::page_view(String const& path) const
84{
85 if (path.is_empty())
86 return StringView {};
87
88 {
89 // Check if we've got it cached already.
90 auto mapped_file = m_mapped_files.get(path);
91 if (mapped_file.has_value())
92 return StringView { mapped_file.value()->bytes() };
93 }
94
95 auto file = TRY(Core::MappedFile::map(path));
96
97 StringView view { file->bytes() };
98 m_mapped_files.set(path, move(file));
99 return view;
100}
101
102Optional<String> ManualModel::page_and_section(const GUI::ModelIndex& index) const
103{
104 if (!index.is_valid())
105 return {};
106 auto* node = static_cast<Manual::Node const*>(index.internal_data());
107 if (!node->is_page())
108 return {};
109 auto* page = static_cast<Manual::PageNode const*>(node);
110 auto* section = static_cast<Manual::SectionNode const*>(page->parent());
111 auto page_name = page->name();
112 if (page_name.is_error())
113 return {};
114 auto name = String::formatted("{}({})", page_name.release_value(), section->section_name());
115 if (name.is_error())
116 return {};
117 return name.release_value();
118}
119
120GUI::ModelIndex ManualModel::index(int row, int column, const GUI::ModelIndex& parent_index) const
121{
122 if (!parent_index.is_valid())
123 return create_index(row, column, Manual::sections[row].ptr());
124 auto* parent = static_cast<Manual::Node const*>(parent_index.internal_data());
125 auto const children = parent->children();
126 if (children.is_error())
127 return {};
128 auto child = children.value()[row];
129 return create_index(row, column, child.ptr());
130}
131
132GUI::ModelIndex ManualModel::parent_index(const GUI::ModelIndex& index) const
133{
134 if (!index.is_valid())
135 return {};
136 auto* child = static_cast<Manual::Node const*>(index.internal_data());
137 auto* parent = child->parent();
138 if (parent == nullptr)
139 return {};
140
141 if (parent->parent() == nullptr) {
142 for (size_t row = 0; row < Manual::sections.size(); row++)
143 if (Manual::sections[row].ptr() == parent)
144 return create_index(row, 0, parent);
145 VERIFY_NOT_REACHED();
146 }
147 auto maybe_children = parent->parent()->children();
148 if (maybe_children.is_error())
149 return {};
150 auto children = maybe_children.release_value();
151 for (size_t row = 0; row < children.size(); row++) {
152 Manual::Node const* child_at_row = children[row];
153 if (child_at_row == parent)
154 return create_index(row, 0, parent);
155 }
156 VERIFY_NOT_REACHED();
157}
158
159int ManualModel::row_count(const GUI::ModelIndex& index) const
160{
161 if (!index.is_valid())
162 return static_cast<int>(Manual::sections.size());
163 auto* node = static_cast<Manual::Node const*>(index.internal_data());
164 auto maybe_children = node->children();
165 if (maybe_children.is_error())
166 return 0;
167 return static_cast<int>(maybe_children.value().size());
168}
169
170int ManualModel::column_count(const GUI::ModelIndex&) const
171{
172 return 1;
173}
174
175GUI::Variant ManualModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
176{
177 auto* node = static_cast<Manual::Node const*>(index.internal_data());
178 switch (role) {
179 case GUI::ModelRole::Search:
180 if (!node->is_page())
181 return {};
182 if (auto path = page_path(index); path.has_value())
183 if (auto page = page_view(path.release_value()); !page.is_error())
184 // FIXME: We already provide String, but GUI::Variant still needs DeprecatedString.
185 return DeprecatedString(page.release_value());
186 return {};
187 case GUI::ModelRole::Display:
188 if (auto name = node->name(); !name.is_error())
189 return name.release_value();
190 return {};
191 case GUI::ModelRole::Icon:
192 if (node->is_page())
193 return m_page_icon;
194 if (node->is_open())
195 return m_section_open_icon;
196 return m_section_icon;
197 default:
198 return {};
199 }
200}
201
202void ManualModel::update_section_node_on_toggle(const GUI::ModelIndex& index, bool const open)
203{
204 auto* node = static_cast<Manual::Node*>(index.internal_data());
205 if (is<Manual::SectionNode>(*node))
206 static_cast<Manual::SectionNode*>(node)->set_open(open);
207}
208
209TriState ManualModel::data_matches(const GUI::ModelIndex& index, const GUI::Variant& term) const
210{
211 auto name = page_name(index);
212 if (!name.has_value())
213 return TriState::False;
214
215 if (name.value().bytes_as_string_view().contains(term.as_string(), CaseSensitivity::CaseInsensitive))
216 return TriState::True;
217
218 auto path = page_path(index);
219 // NOTE: This is slightly inaccurate, as page_path can also fail due to OOM. We consider it acceptable to have a data mismatch in that case.
220 if (!path.has_value())
221 return TriState::False;
222 auto view_result = page_view(path.release_value());
223 if (view_result.is_error() || view_result.value().is_empty())
224 return TriState::False;
225
226 return view_result.value().contains(term.as_string(), CaseSensitivity::CaseInsensitive) ? TriState::True : TriState::False;
227}