diff options
author | Sergey Bugaev <bugaevc@gmail.com> | 2019-09-21 00:47:31 +0300 |
---|---|---|
committer | Andreas Kling <awesomekling@gmail.com> | 2019-09-28 18:29:42 +0200 |
commit | 02ee8cbbe2f16d7da15c9b6454e5217fc2e7438a (patch) | |
tree | 75bb65ed9c8cdb86af346ef23a037e50df31d57d | |
parent | 6ec625d6f38d93751be44afc79ab713b1cec8f1b (diff) | |
download | serenity-02ee8cbbe2f16d7da15c9b6454e5217fc2e7438a.zip |
Applications: Add a new Help app
This is a neat simple app that can display the Serenity manual ^)
-rw-r--r-- | Applications/Help/Makefile | 11 | ||||
-rw-r--r-- | Applications/Help/ManualModel.cpp | 105 | ||||
-rw-r--r-- | Applications/Help/ManualModel.h | 31 | ||||
-rw-r--r-- | Applications/Help/ManualNode.h | 14 | ||||
-rw-r--r-- | Applications/Help/ManualPageNode.cpp | 18 | ||||
-rw-r--r-- | Applications/Help/ManualPageNode.h | 27 | ||||
-rw-r--r-- | Applications/Help/ManualSectionNode.cpp | 26 | ||||
-rw-r--r-- | Applications/Help/ManualSectionNode.h | 34 | ||||
-rw-r--r-- | Applications/Help/main.cpp | 79 | ||||
-rwxr-xr-x | Applications/Makefile.common | 2 | ||||
-rwxr-xr-x | Kernel/build-root-filesystem.sh | 2 | ||||
-rwxr-xr-x | Kernel/makeall.sh | 1 |
12 files changed, 349 insertions, 1 deletions
diff --git a/Applications/Help/Makefile b/Applications/Help/Makefile new file mode 100644 index 0000000000..b85c197770 --- /dev/null +++ b/Applications/Help/Makefile @@ -0,0 +1,11 @@ +include ../../Makefile.common + +OBJS = \ + ManualModel.o \ + ManualSectionNode.o \ + ManualPageNode.o \ + main.o + +APP = Help + +include ../Makefile.common diff --git a/Applications/Help/ManualModel.cpp b/Applications/Help/ManualModel.cpp new file mode 100644 index 0000000000..bf56fc29b3 --- /dev/null +++ b/Applications/Help/ManualModel.cpp @@ -0,0 +1,105 @@ +#include "ManualModel.h" +#include "ManualNode.h" +#include "ManualPageNode.h" +#include "ManualSectionNode.h" +#include <LibDraw/PNGLoader.h> + +static ManualSectionNode s_sections[] = { + { "1", "Command-line programs" }, + { "2", "System calls" } +}; + +ManualModel::ManualModel() +{ + // FIXME: need some help from the icon fairy ^) + m_section_icon.set_bitmap_for_size(16, load_png("/res/icons/16x16/book.png")); + m_page_icon.set_bitmap_for_size(16, load_png("/res/icons/16x16/filetype-unknown.png")); +} + +String ManualModel::page_path(const GModelIndex& index) const +{ + if (!index.is_valid()) + return {}; + auto* node = static_cast<const ManualNode*>(index.internal_data()); + if (!node->is_page()) + return {}; + auto* page = static_cast<const ManualPageNode*>(node); + return page->path(); +} + +String ManualModel::page_and_section(const GModelIndex& index) const +{ + if (!index.is_valid()) + return {}; + auto* node = static_cast<const ManualNode*>(index.internal_data()); + if (!node->is_page()) + return {}; + auto* page = static_cast<const ManualPageNode*>(node); + auto* section = static_cast<const ManualSectionNode*>(page->parent()); + return String::format("%s(%s)", page->name().characters(), section->section_name().characters()); +} + +GModelIndex ManualModel::index(int row, int column, const GModelIndex& parent_index) const +{ + if (!parent_index.is_valid()) + return create_index(row, column, &s_sections[row]); + auto* parent = static_cast<const ManualNode*>(parent_index.internal_data()); + auto* child = &parent->children()[row]; + return create_index(row, column, child); +} + +GModelIndex ManualModel::parent_index(const GModelIndex& index) const +{ + if (!index.is_valid()) + return {}; + auto* child = static_cast<const ManualNode*>(index.internal_data()); + auto* parent = child->parent(); + if (parent == nullptr) + return {}; + + if (parent->parent() == nullptr) { + for (size_t row = 0; row < sizeof(s_sections) / sizeof(s_sections[0]); row++) + if (&s_sections[row] == parent) + return create_index(row, 0, parent); + ASSERT_NOT_REACHED(); + } + for (int row = 0; row < parent->parent()->children().size(); row++) { + ManualNode* child_at_row = &parent->parent()->children()[row]; + if (child_at_row == parent) + return create_index(row, 0, parent); + } + ASSERT_NOT_REACHED(); +} + +int ManualModel::row_count(const GModelIndex& index) const +{ + if (!index.is_valid()) + return sizeof(s_sections) / sizeof(s_sections[0]); + auto* node = static_cast<const ManualNode*>(index.internal_data()); + return node->children().size(); +} + +int ManualModel::column_count(const GModelIndex&) const +{ + return 1; +} + +GVariant ManualModel::data(const GModelIndex& index, Role role) const +{ + auto* node = static_cast<const ManualNode*>(index.internal_data()); + switch (role) { + case Role::Display: + return node->name(); + case Role::Icon: + if (node->is_page()) + return m_page_icon; + return m_section_icon; + default: + return {}; + } +} + +void ManualModel::update() +{ + did_update(); +} diff --git a/Applications/Help/ManualModel.h b/Applications/Help/ManualModel.h new file mode 100644 index 0000000000..d0ecb255fb --- /dev/null +++ b/Applications/Help/ManualModel.h @@ -0,0 +1,31 @@ +#pragma once + +#include <AK/NonnullRefPtr.h> +#include <AK/String.h> +#include <LibGUI/GModel.h> + +class ManualModel final : public GModel { +public: + static NonnullRefPtr<ManualModel> create() + { + return adopt(*new ManualModel); + } + + virtual ~ManualModel() override {}; + + String page_path(const GModelIndex&) const; + String page_and_section(const GModelIndex&) const; + + virtual int row_count(const GModelIndex& = GModelIndex()) const override; + virtual int column_count(const GModelIndex& = GModelIndex()) const override; + virtual GVariant data(const GModelIndex&, Role = Role::Display) const override; + virtual void update() override; + virtual GModelIndex parent_index(const GModelIndex&) const override; + virtual GModelIndex index(int row, int column = 0, const GModelIndex& parent = GModelIndex()) const override; + +private: + ManualModel(); + + GIcon m_section_icon; + GIcon m_page_icon; +}; diff --git a/Applications/Help/ManualNode.h b/Applications/Help/ManualNode.h new file mode 100644 index 0000000000..44609d5088 --- /dev/null +++ b/Applications/Help/ManualNode.h @@ -0,0 +1,14 @@ +#pragma once + +#include <AK/NonnullOwnPtrVector.h> +#include <AK/String.h> + +class ManualNode { +public: + virtual ~ManualNode() {} + + virtual NonnullOwnPtrVector<ManualNode>& children() const = 0; + virtual const ManualNode* parent() const = 0; + virtual String name() const = 0; + virtual bool is_page() const { return false; } +}; diff --git a/Applications/Help/ManualPageNode.cpp b/Applications/Help/ManualPageNode.cpp new file mode 100644 index 0000000000..7aec825586 --- /dev/null +++ b/Applications/Help/ManualPageNode.cpp @@ -0,0 +1,18 @@ +#include "ManualPageNode.h" +#include "ManualSectionNode.h" + +const ManualNode* ManualPageNode::parent() const +{ + return &m_section; +} + +NonnullOwnPtrVector<ManualNode>& ManualPageNode::children() const +{ + static NonnullOwnPtrVector<ManualNode> empty_vector; + return empty_vector; +} + +String ManualPageNode::path() const +{ + return String::format("%s/%s.md", m_section.path().characters(), m_page.characters()); +} diff --git a/Applications/Help/ManualPageNode.h b/Applications/Help/ManualPageNode.h new file mode 100644 index 0000000000..79dc1e8fce --- /dev/null +++ b/Applications/Help/ManualPageNode.h @@ -0,0 +1,27 @@ +#pragma once + +#include "ManualNode.h" + +class ManualSectionNode; + +class ManualPageNode : public ManualNode { +public: + virtual ~ManualPageNode() override {} + + ManualPageNode(const ManualSectionNode& section, const StringView& page) + : m_section(section) + , m_page(page) + { + } + + virtual NonnullOwnPtrVector<ManualNode>& children() const override; + virtual const ManualNode* parent() const override; + virtual String name() const override { return m_page; }; + virtual bool is_page() const override { return true; } + + String path() const; + +private: + const ManualSectionNode& m_section; + String m_page; +}; diff --git a/Applications/Help/ManualSectionNode.cpp b/Applications/Help/ManualSectionNode.cpp new file mode 100644 index 0000000000..17e1ed3a51 --- /dev/null +++ b/Applications/Help/ManualSectionNode.cpp @@ -0,0 +1,26 @@ +#include "ManualSectionNode.h" +#include "ManualPageNode.h" +#include <AK/String.h> +#include <LibCore/CDirIterator.h> + +String ManualSectionNode::path() const +{ + return String::format("/usr/share/man/man%s", m_section.characters()); +} + +void ManualSectionNode::reify_if_needed() const +{ + if (m_reified) + return; + m_reified = true; + + CDirIterator dir_iter { path(), CDirIterator::Flags::SkipDots }; + + while (dir_iter.has_next()) { + String file_name = dir_iter.next_path(); + ASSERT(file_name.ends_with(".md")); + String page_name = file_name.substring(0, file_name.length() - 3); + NonnullOwnPtr<ManualNode> child = make<ManualPageNode>(*this, move(page_name)); + m_children.append(move(child)); + } +} diff --git a/Applications/Help/ManualSectionNode.h b/Applications/Help/ManualSectionNode.h new file mode 100644 index 0000000000..1d6b50d830 --- /dev/null +++ b/Applications/Help/ManualSectionNode.h @@ -0,0 +1,34 @@ +#pragma once + +#include "ManualNode.h" + +class ManualSectionNode : public ManualNode { +public: + virtual ~ManualSectionNode() override {} + + ManualSectionNode(String section, String name) + : m_section(section) + , m_full_name(String::format("%s. %s", section.characters(), name.characters())) + { + } + + virtual NonnullOwnPtrVector<ManualNode>& children() const override + { + reify_if_needed(); + return m_children; + } + + virtual const ManualNode* parent() const override { return nullptr; } + virtual String name() const override { return m_full_name; } + + const String& section_name() const { return m_section; } + String path() const; + +private: + void reify_if_needed() const; + + String m_section; + String m_full_name; + mutable NonnullOwnPtrVector<ManualNode> m_children; + mutable bool m_reified { false }; +}; diff --git a/Applications/Help/main.cpp b/Applications/Help/main.cpp new file mode 100644 index 0000000000..dd20b51205 --- /dev/null +++ b/Applications/Help/main.cpp @@ -0,0 +1,79 @@ +#include "ManualModel.h" +#include <LibCore/CFile.h> +#include <LibDraw/PNGLoader.h> +#include <LibGUI/GApplication.h> +#include <LibGUI/GMessageBox.h> +#include <LibGUI/GSplitter.h> +#include <LibGUI/GTextEditor.h> +#include <LibGUI/GTreeView.h> +#include <LibGUI/GWindow.h> +#include <LibHTML/HtmlView.h> +#include <LibHTML/Layout/LayoutNode.h> +#include <LibHTML/Parser/CSSParser.h> +#include <LibHTML/Parser/HTMLParser.h> +#include <LibMarkdown/MDDocument.h> + +int main(int argc, char* argv[]) +{ + GApplication app(argc, argv); + + auto window = GWindow::construct(); + window->set_title("Help"); + window->set_rect(300, 200, 570, 500); + + auto splitter = GSplitter::construct(Orientation::Horizontal, nullptr); + + auto model = ManualModel::create(); + + auto tree_view = GTreeView::construct(splitter); + tree_view->set_model(model); + tree_view->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); + tree_view->set_preferred_size(200, 500); + + auto html_view = HtmlView::construct(splitter); + + extern const char default_stylesheet_source[]; + String css = default_stylesheet_source; + auto sheet = parse_css(css); + + tree_view->on_selection_change = [&] { + String path = model->page_path(tree_view->selection().first()); + if (path.is_null()) { + html_view->set_document(nullptr); + return; + } + + dbg() << "Opening page at " << path; + + auto file = CFile::construct(); + file->set_filename(path); + + if (!file->open(CIODevice::OpenMode::ReadOnly)) { + int saved_errno = errno; + GMessageBox::show(strerror(saved_errno), "Failed to open man page", GMessageBox::Type::Error, GMessageBox::InputType::OK, window); + return; + } + auto buffer = file->read_all(); + StringView source { (char*)buffer.data(), buffer.size() }; + + MDDocument md_document; + bool success = md_document.parse(source); + ASSERT(success); + + String html = md_document.render_to_html(); + auto html_document = parse_html(html); + html_document->normalize(); + html_document->add_sheet(sheet); + html_view->set_document(html_document); + + String page_and_section = model->page_and_section(tree_view->selection().first()); + window->set_title(String::format("Help: %s", page_and_section.characters())); + }; + + window->set_main_widget(splitter); + window->show(); + + window->set_icon(load_png("/res/icons/16x16/book.png")); + + return app.exec(); +} diff --git a/Applications/Makefile.common b/Applications/Makefile.common index 6707d00010..8514e4bf09 100755 --- a/Applications/Makefile.common +++ b/Applications/Makefile.common @@ -3,7 +3,7 @@ DEFINES += -DUSERLAND all: $(APP) $(APP): $(OBJS) - $(LD) -o $(APP) $(LDFLAGS) $(OBJS) -lgui -ldraw -laudio -lipc -lthread -lvt -lpcidb -lcore -lc + $(LD) -o $(APP) $(LDFLAGS) $(OBJS) -lmarkdown -lhtml -lgui -ldraw -laudio -lipc -lthread -lvt -lpcidb -lcore -lc .cpp.o: @echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $< diff --git a/Kernel/build-root-filesystem.sh b/Kernel/build-root-filesystem.sh index 703f303ce1..62a0c1b772 100755 --- a/Kernel/build-root-filesystem.sh +++ b/Kernel/build-root-filesystem.sh @@ -88,6 +88,7 @@ cp ../Applications/Calculator/Calculator mnt/bin/Calculator cp ../Applications/SoundPlayer/SoundPlayer mnt/bin/SoundPlayer cp ../Applications/DisplayProperties/DisplayProperties mnt/bin/DisplayProperties cp ../Applications/Welcome/Welcome mnt/bin/Welcome +cp ../Applications/Help/Help mnt/bin/Help cp ../Demos/HelloWorld/HelloWorld mnt/bin/HelloWorld cp ../Demos/HelloWorld2/HelloWorld2 mnt/bin/HelloWorld2 cp ../Demos/RetroFetch/RetroFetch mnt/bin/RetroFetch @@ -127,6 +128,7 @@ ln -s ChanViewer mnt/bin/cv ln -s Calculator mnt/bin/calc ln -s Inspector mnt/bin/ins ln -s SoundPlayer mnt/bin/sp +ln -s Help mnt/bin/help echo "done" mkdir -p mnt/boot/ diff --git a/Kernel/makeall.sh b/Kernel/makeall.sh index d4713cf6ac..2e9e2b5996 100755 --- a/Kernel/makeall.sh +++ b/Kernel/makeall.sh @@ -59,6 +59,7 @@ build_targets="$build_targets ../Applications/Terminal" build_targets="$build_targets ../Applications/TextEditor" build_targets="$build_targets ../Applications/SoundPlayer" build_targets="$build_targets ../Applications/Welcome" +build_targets="$build_targets ../Applications/Help" build_targets="$build_targets ../Demos/Fire" build_targets="$build_targets ../Demos/HelloWorld" |