diff options
-rw-r--r-- | Applications/FileManager/main.cpp | 2 | ||||
-rw-r--r-- | LibGUI/GModel.h | 8 | ||||
-rw-r--r-- | LibGUI/GModelIndex.h | 2 | ||||
-rw-r--r-- | LibGUI/GTreeView.cpp | 198 | ||||
-rw-r--r-- | LibGUI/GTreeView.h | 14 |
5 files changed, 215 insertions, 9 deletions
diff --git a/Applications/FileManager/main.cpp b/Applications/FileManager/main.cpp index d40582a093..ef87839545 100644 --- a/Applications/FileManager/main.cpp +++ b/Applications/FileManager/main.cpp @@ -53,6 +53,8 @@ int main(int argc, char** argv) auto* splitter = new GWidget(widget); splitter->set_layout(make<GBoxLayout>(Orientation::Horizontal)); auto* tree_view = new GTreeView(splitter); + tree_view->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); + tree_view->set_preferred_size({ 200, 0 }); auto* directory_view = new DirectoryView(splitter); auto* statusbar = new GStatusBar(widget); diff --git a/LibGUI/GModel.h b/LibGUI/GModel.h index 03f6b2d7f5..4dd2df72ae 100644 --- a/LibGUI/GModel.h +++ b/LibGUI/GModel.h @@ -76,7 +76,8 @@ public: Function<void(GModel&)> on_model_update; Function<void(const GModelIndex&)> on_selection_changed; - virtual GModelIndex index(int row, int column) const { return create_index(row, column); } + virtual GModelIndex parent_index(const GModelIndex&) const { return { }; } + virtual GModelIndex index(int row, int column = 0, const GModelIndex& = GModelIndex()) const { return create_index(row, column); } protected: GModel(); @@ -91,3 +92,8 @@ private: GModelIndex m_selected_index; bool m_activates_on_selection { false }; }; + +inline GModelIndex GModelIndex::parent() const +{ + return m_model ? m_model->parent_index(*this) : GModelIndex(); +} diff --git a/LibGUI/GModelIndex.h b/LibGUI/GModelIndex.h index 4e66d573dc..a303cf90d5 100644 --- a/LibGUI/GModelIndex.h +++ b/LibGUI/GModelIndex.h @@ -13,6 +13,8 @@ public: void* internal_data() const { return m_internal_data; } + GModelIndex parent() const; + bool operator==(const GModelIndex& other) const { return m_row == other.m_row && m_column == other.m_column; } private: diff --git a/LibGUI/GTreeView.cpp b/LibGUI/GTreeView.cpp index 031efc55e5..4c46462298 100644 --- a/LibGUI/GTreeView.cpp +++ b/LibGUI/GTreeView.cpp @@ -1,20 +1,64 @@ #include <LibGUI/GTreeView.h> #include <LibGUI/GPainter.h> +#include <LibGUI/GScrollBar.h> + +struct Node { + String text; + Node* parent { nullptr }; + Vector<Node*> children; +}; class TestModel : public GModel { public: static Retained<TestModel> create() { return adopt(*new TestModel); } - virtual int row_count(const GModelIndex& = GModelIndex()) const; - virtual int column_count(const GModelIndex& = GModelIndex()) const; - virtual GVariant data(const GModelIndex&, Role = Role::Display) const; - virtual void update(); - virtual ColumnMetadata column_metadata(int) const { return { 100 }; } + TestModel(); + + 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 index(int row, int column = 0, const GModelIndex& parent = GModelIndex()) const override; + virtual ColumnMetadata column_metadata(int) const override{ return { 100 }; } + + Node* m_root { nullptr }; }; +Node* make_little_tree(int depth, Node* parent) +{ + static int next_id = 0; + Node* node = new Node; + node->text = String::format("Node #%d", next_id++); + node->parent = parent; + if (depth) + node->children.append(make_little_tree(depth - 1, node)); + return node; +} + +GModelIndex TestModel::index(int row, int column, const GModelIndex& parent) const +{ + if (!parent.is_valid()) + return create_index(row, column, m_root); + auto& node = *(Node*)parent.internal_data(); + return create_index(row, column, node.children[row]); +} + +TestModel::TestModel() +{ + m_root = new Node; + m_root->text = "Root"; + + m_root->children.append(make_little_tree(3, m_root)); + m_root->children.append(make_little_tree(2, m_root)); + m_root->children.append(make_little_tree(1, m_root)); +} + int TestModel::row_count(const GModelIndex& index) const { - return 0; + if (!index.is_valid()) + return 1; + auto& node = *(const Node*)index.internal_data(); + return node.children.size(); } int TestModel::column_count(const GModelIndex&) const @@ -26,11 +70,39 @@ void TestModel::update() { } -GVariant TestModel::data(const GModelIndex&, Role) const +GVariant TestModel::data(const GModelIndex& index, Role role) const { + if (!index.is_valid()) + return { }; + auto& node = *(const Node*)index.internal_data(); + if (role == GModel::Role::Display) { + return node.text; + } + if (role == GModel::Role::Icon) { + if (node.children.is_empty()) + return GIcon::default_icon("filetype-unknown"); + return GIcon::default_icon("filetype-folder"); + } return { }; } +struct GTreeView::MetadataForIndex { + bool open { true }; +}; + + +GTreeView::MetadataForIndex& GTreeView::ensure_metadata_for_index(const GModelIndex& index) const +{ + ASSERT(index.is_valid()); + auto it = m_view_metadata.find(index.internal_data()); + if (it != m_view_metadata.end()) + return *it->value; + auto new_metadata = make<MetadataForIndex>(); + auto& new_metadata_ref = *new_metadata; + m_view_metadata.set(index.internal_data(), move(new_metadata)); + return new_metadata_ref; +} + GTreeView::GTreeView(GWidget* parent) : GAbstractView(parent) { @@ -45,12 +117,122 @@ GTreeView::~GTreeView() { } +GModelIndex GTreeView::index_at_content_position(const Point& position) const +{ + if (!model()) + return { }; + auto& model = *this->model(); + int indent_level = 0; + int y_offset = 0; + GModelIndex result; + Function<bool(const GModelIndex&, GModelIndex&)> hit_test_index = [&] (const GModelIndex& index, GModelIndex& result) { + if (index.is_valid()) { + auto& metadata = ensure_metadata_for_index(index); + auto& node = *(const Node*)index.internal_data(); + int x_offset = indent_level * indent_width_in_pixels(); + auto data = model.data(index, GModel::Role::Display); + Rect rect = { x_offset, y_offset, icon_size() + icon_spacing() + font().width(data.to_string()), item_height() }; + dbgprintf("%s %s (%s)\n", data.to_string().characters(), rect.to_string().characters(), metadata.open ? "open" : "closed"); + y_offset += item_height(); + + if (rect.contains(position)) { + result = index; + return true; + } + + // NOTE: Skip traversing children if this index is closed! + if (!metadata.open) + return false; + } + + ++indent_level; + for (int i = 0; i < model.row_count(index); ++i) { + auto child_index = model.index(i, 0, index); + if (hit_test_index(child_index, result)) + return true; + } + --indent_level; + return false; + }; + + hit_test_index(model.index(0, 0, GModelIndex()), result); + return result; +} + +void GTreeView::mousedown_event(GMouseEvent& event) +{ + if (!model()) + return; + auto& model = *this->model(); + auto adjusted_position = event.position().translated(horizontal_scrollbar().value() - frame_thickness(), vertical_scrollbar().value() - frame_thickness()); + auto index = index_at_content_position(adjusted_position); + if (!index.is_valid()) { + dbgprintf("GTV::mousedown: No valid index at %s (adjusted to: %s)\n", event.position().to_string().characters(), adjusted_position.to_string().characters()); + return; + } + dbgprintf("GTV::mousedown: Index %d,%d {%p}] at %s (adjusted to: %s)\n", index.row(), index.column(), index.internal_data(), event.position().to_string().characters(), adjusted_position.to_string().characters()); + auto& metadata = ensure_metadata_for_index(index); + + if (model.row_count(index)) { + metadata.open = !metadata.open; + dbgprintf("GTV::mousedown: toggle index %d,%d {%p} open: %d -> %d\n", index.row(), index.column(), index.internal_data(), !metadata.open, metadata.open); + update(); + } +} + void GTreeView::paint_event(GPaintEvent& event) { GFrame::paint_event(event); GPainter painter(*this); painter.set_clip_rect(frame_inner_rect()); painter.set_clip_rect(event.rect()); - painter.fill_rect(event.rect(), Color::White); + painter.translate(frame_inner_rect().location()); + + if (!model()) + return; + auto& model = *this->model(); + + int indent_level = 0; + int y_offset = 0; + + Function<void(const GModelIndex&)> render_index = [&] (const GModelIndex& index) { + if (index.is_valid()) { + auto& metadata = ensure_metadata_for_index(index); + int x_offset = indent_level * indent_width_in_pixels(); + auto node_text = model.data(index, GModel::Role::Display).to_string(); + Rect rect = { + x_offset, y_offset, + icon_size() + icon_spacing() + font().width(node_text), item_height() + }; + painter.fill_rect(rect, Color::LightGray); + + Rect icon_rect = { rect.x(), rect.y(), icon_size(), icon_size() }; + auto icon = model.data(index, GModel::Role::Icon); + if (icon.is_icon()) { + if (auto* bitmap = icon.as_icon().bitmap_for_size(icon_size())) + painter.blit(rect.location(), *bitmap, bitmap->rect()); + } + + Rect text_rect = { + icon_rect.right() + 1 + icon_spacing(), rect.y(), + rect.width() - icon_size() - icon_spacing(), rect.height() + }; + painter.draw_text(text_rect, node_text, TextAlignment::CenterLeft, Color::Black); + y_offset += item_height(); + + // NOTE: Skip traversing children if this index is closed! + if (!metadata.open) + return; + } + + ++indent_level; + for (int i = 0; i < model.row_count(index); ++i) { + auto child_index = model.index(i, 0, index); + render_index(child_index); + } + --indent_level; + }; + + render_index(model.index(0, 0, GModelIndex())); } diff --git a/LibGUI/GTreeView.h b/LibGUI/GTreeView.h index 7ad6dfad8e..a453ceb08e 100644 --- a/LibGUI/GTreeView.h +++ b/LibGUI/GTreeView.h @@ -9,8 +9,22 @@ public: virtual const char* class_name() const override { return "GTreeView"; } + GModelIndex index_at_content_position(const Point&) const; + protected: virtual void paint_event(GPaintEvent&) override; + virtual void mousedown_event(GMouseEvent&) override; private: + int item_height() const { return 16; } + int max_item_width() const { return frame_inner_rect().width(); } + int indent_width_in_pixels() const { return 12; } + int icon_size() const { return 16; } + int icon_spacing() const { return 4; } + + struct MetadataForIndex; + + MetadataForIndex& ensure_metadata_for_index(const GModelIndex&) const; + + mutable HashMap<void*, OwnPtr<MetadataForIndex>> m_view_metadata; }; |