Quellcode durchsuchen

Applications: Add a new Help app

This is a neat simple app that can display the Serenity manual ^)
Sergey Bugaev vor 5 Jahren
Ursprung
Commit
02ee8cbbe2

+ 11 - 0
Applications/Help/Makefile

@@ -0,0 +1,11 @@
+include ../../Makefile.common
+
+OBJS = \
+		ManualModel.o \
+		ManualSectionNode.o \
+		ManualPageNode.o \
+    main.o
+
+APP = Help
+
+include ../Makefile.common

+ 105 - 0
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();
+}

+ 31 - 0
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;
+};

+ 14 - 0
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; }
+};

+ 18 - 0
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());
+}

+ 27 - 0
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;
+};

+ 26 - 0
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));
+    }
+}

+ 34 - 0
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 };
+};

+ 79 - 0
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();
+}

+ 1 - 1
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 $<

+ 2 - 0
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/

+ 1 - 0
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"