Serenity Operating System
at master 227 lines 8.1 kB view raw
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}