summaryrefslogtreecommitdiff
path: root/Applications/Help
diff options
context:
space:
mode:
authorSergey Bugaev <bugaevc@gmail.com>2019-09-21 00:47:31 +0300
committerAndreas Kling <awesomekling@gmail.com>2019-09-28 18:29:42 +0200
commit02ee8cbbe2f16d7da15c9b6454e5217fc2e7438a (patch)
tree75bb65ed9c8cdb86af346ef23a037e50df31d57d /Applications/Help
parent6ec625d6f38d93751be44afc79ab713b1cec8f1b (diff)
downloadserenity-02ee8cbbe2f16d7da15c9b6454e5217fc2e7438a.zip
Applications: Add a new Help app
This is a neat simple app that can display the Serenity manual ^)
Diffstat (limited to 'Applications/Help')
-rw-r--r--Applications/Help/Makefile11
-rw-r--r--Applications/Help/ManualModel.cpp105
-rw-r--r--Applications/Help/ManualModel.h31
-rw-r--r--Applications/Help/ManualNode.h14
-rw-r--r--Applications/Help/ManualPageNode.cpp18
-rw-r--r--Applications/Help/ManualPageNode.h27
-rw-r--r--Applications/Help/ManualSectionNode.cpp26
-rw-r--r--Applications/Help/ManualSectionNode.h34
-rw-r--r--Applications/Help/main.cpp79
9 files changed, 345 insertions, 0 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();
+}