diff --git a/DevTools/HackStudio/Project.cpp b/DevTools/HackStudio/Project.cpp index 0cbcc67bbd8..b02ffd870d7 100644 --- a/DevTools/HackStudio/Project.cpp +++ b/DevTools/HackStudio/Project.cpp @@ -1,5 +1,39 @@ #include "Project.h" +#include +#include #include +#include +#include +#include + +struct Project::ProjectTreeNode { + enum class Type { + Invalid, + Project, + Directory, + File, + }; + + ProjectTreeNode& find_or_create_subdirectory(const String& name) + { + for (auto& child : children) { + if (child->type == Type::Directory && child->name == name) + return *child; + } + auto new_child = make(); + new_child->type = Type::Directory; + new_child->name = name; + new_child->parent = this; + auto* ptr = new_child.ptr(); + children.append(move(new_child)); + return *ptr; + } + + Type type { Type::Invalid }; + String name; + Vector> children; + ProjectTreeNode* parent { nullptr }; +}; class ProjectModel final : public GModel { public: @@ -8,22 +42,80 @@ public: { } - virtual int row_count(const GModelIndex& = GModelIndex()) const override { return m_project.m_files.size(); } - virtual int column_count(const GModelIndex& = GModelIndex()) const override { return 1; } + virtual int row_count(const GModelIndex& index) const override + { + if (!index.is_valid()) + return 1; + auto* node = static_cast(index.internal_data()); + return node->children.size(); + } + + virtual int column_count(const GModelIndex&) const override + { + return 1; + } + virtual GVariant data(const GModelIndex& index, Role role = Role::Display) const override { - int row = index.row(); + auto* node = static_cast(index.internal_data()); if (role == Role::Display) { - return m_project.m_files.at(row).name(); + return FileSystemPath(node->name).basename(); + } + if (role == Role::Custom) { + return node->name; + } + if (role == Role::Icon) { + if (node->type == Project::ProjectTreeNode::Type::Project) + return m_project.m_project_icon; + if (node->type == Project::ProjectTreeNode::Type::Directory) + return m_project.m_directory_icon; + if (node->name.ends_with(".cpp")) + return m_project.m_cplusplus_icon; + if (node->name.ends_with(".h")) + return m_project.m_header_icon; + return m_project.m_file_icon; } if (role == Role::Font) { extern String g_currently_open_file; - if (m_project.m_files.at(row).name() == g_currently_open_file) + if (node->name == g_currently_open_file) return Font::default_bold_font(); return {}; } return {}; } + + virtual GModelIndex index(int row, int column = 0, const GModelIndex& parent = GModelIndex()) const override + { + if (!parent.is_valid()) { + return create_index(row, column, &m_project.root_node()); + } + auto& node = *static_cast(parent.internal_data()); + return create_index(row, column, node.children.at(row).ptr()); + } + + GModelIndex parent_index(const GModelIndex& index) const override + { + if (!index.is_valid()) + return {}; + auto& node = *static_cast(index.internal_data()); + if (!node.parent) + return {}; + + if (!node.parent->parent) { + return create_index(0, 0, &m_project.root_node()); + ASSERT_NOT_REACHED(); + return {}; + } + + for (int row = 0; row < node.parent->parent->children.size(); ++row) { + if (node.parent->parent->children[row].ptr() == node.parent) + return create_index(row, 0, node.parent); + } + + ASSERT_NOT_REACHED(); + return {}; + } + virtual void update() override { did_update(); @@ -36,10 +128,23 @@ private: Project::Project(const String& path, Vector&& filenames) : m_path(path) { + m_file_icon = GIcon(GraphicsBitmap::load_from_file("/res/icons/16x16/filetype-unknown.png")); + m_cplusplus_icon = GIcon(GraphicsBitmap::load_from_file("/res/icons/16x16/filetype-cplusplus.png")); + m_header_icon = GIcon(GraphicsBitmap::load_from_file("/res/icons/16x16/filetype-header.png")); + m_directory_icon = GIcon(GraphicsBitmap::load_from_file("/res/icons/16x16/filetype-folder.png")); + m_project_icon = GIcon(GraphicsBitmap::load_from_file("/res/icons/16x16/app-hack-studio.png")); + for (auto& filename : filenames) { m_files.append(ProjectFile::construct_with_name(filename)); } + m_model = adopt(*new ProjectModel(*this)); + + rebuild_tree(); +} + +Project::~Project() +{ } OwnPtr Project::load_from_file(const String& path) @@ -88,3 +193,58 @@ ProjectFile* Project::get_file(const String& filename) } return nullptr; } + +void Project::rebuild_tree() +{ + auto root = make(); + root->type = ProjectTreeNode::Type::Project; + + for (auto& file : m_files) { + FileSystemPath path(file.name()); + ProjectTreeNode* current = root.ptr(); + StringBuilder partial_path; + + for (int i = 0; i < path.parts().size(); ++i) { + auto& part = path.parts().at(i); + if (part == ".") + continue; + if (i != path.parts().size() - 1) { + current = ¤t->find_or_create_subdirectory(part); + continue; + } + struct stat st; + if (lstat(path.string().characters(), &st) < 0) + continue; + + if (S_ISDIR(st.st_mode)) { + current = ¤t->find_or_create_subdirectory(part); + continue; + } + auto file_node = make(); + file_node->name = file.name(); + file_node->type = Project::ProjectTreeNode::Type::File; + file_node->parent = current; + current->children.append(move(file_node)); + break; + } + } + + Function dump_tree = [&](ProjectTreeNode& node, int indent) { + for (int i = 0; i < indent; ++i) + printf(" "); + if (node.name.is_null()) + printf("(null)\n"); + else + printf("%s\n", node.name.characters()); + for (auto& child : node.children) { + dump_tree(*child, indent + 2); + } + }; + + printf("begin tree dump\n"); + dump_tree(*root, 0); + printf("end tree dump\n"); + + m_root_node = move(root); + m_model->update(); +} diff --git a/DevTools/HackStudio/Project.h b/DevTools/HackStudio/Project.h index 458f8ce53f0..1bab7e2793d 100644 --- a/DevTools/HackStudio/Project.h +++ b/DevTools/HackStudio/Project.h @@ -4,12 +4,15 @@ #include #include #include +#include #include class Project { AK_MAKE_NONCOPYABLE(Project) AK_MAKE_NONMOVABLE(Project) public: + ~Project(); + static OwnPtr load_from_file(const String& path); [[nodiscard]] bool add_file(const String& filename); @@ -26,12 +29,22 @@ public: } } - private: friend class ProjectModel; + struct ProjectTreeNode; explicit Project(const String& path, Vector&& files); + const ProjectTreeNode& root_node() const { return *m_root_node; } + void rebuild_tree(); + String m_path; RefPtr m_model; NonnullRefPtrVector m_files; + OwnPtr m_root_node; + + GIcon m_directory_icon; + GIcon m_file_icon; + GIcon m_cplusplus_icon; + GIcon m_header_icon; + GIcon m_project_icon; }; diff --git a/DevTools/HackStudio/main.cpp b/DevTools/HackStudio/main.cpp index 69fe4fa327d..53b4be40487 100644 --- a/DevTools/HackStudio/main.cpp +++ b/DevTools/HackStudio/main.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -43,7 +42,7 @@ RefPtr g_current_editor_wrapper; String g_currently_open_file; OwnPtr g_project; RefPtr g_window; -RefPtr g_project_list_view; +RefPtr g_project_tree_view; RefPtr g_right_hand_stack; RefPtr g_text_inner_splitter; RefPtr g_form_inner_container; @@ -120,10 +119,10 @@ int main(int argc, char** argv) auto toolbar = GToolBar::construct(widget); auto outer_splitter = GSplitter::construct(Orientation::Horizontal, widget); - g_project_list_view = GListView::construct(outer_splitter); - g_project_list_view->set_model(g_project->model()); - g_project_list_view->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); - g_project_list_view->set_preferred_size(140, 0); + g_project_tree_view = GTreeView::construct(outer_splitter); + g_project_tree_view->set_model(g_project->model()); + g_project_tree_view->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); + g_project_tree_view->set_preferred_size(140, 0); g_right_hand_stack = GStackWidget::construct(outer_splitter); @@ -306,8 +305,8 @@ int main(int argc, char** argv) toolbar->add_action(GCommonActions::make_redo_action([&](auto&) { current_editor().redo_action().activate(); })); toolbar->add_separator(); - g_project_list_view->on_activation = [&](auto& index) { - auto filename = g_project_list_view->model()->data(index).to_string(); + g_project_tree_view->on_activation = [&](auto& index) { + auto filename = g_project_tree_view->model()->data(index, GModel::Role::Custom).to_string(); open_file(filename); }; @@ -512,7 +511,7 @@ void open_file(const String& filename) g_currently_open_file = filename; g_window->set_title(String::format("%s - HackStudio", g_currently_open_file.characters())); - g_project_list_view->update(); + g_project_tree_view->update(); current_editor_wrapper().filename_label().set_text(filename);