ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
TreeView.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
9 
10 #include <imgui.h>
11 
12 #include <cmath>
13 #include <list>
14 #include <sstream>
15 #include <unordered_map>
16 
22 #include "visualization/gui/Util.h"
23 
24 namespace cloudViewer {
25 namespace visualization {
26 namespace gui {
27 
29  std::shared_ptr<Checkbox> checkbox_;
30  std::shared_ptr<Label> label_;
31 };
32 
34  const char *text, bool is_checked, std::function<void(bool)> on_toggled)
35  : impl_(new CheckableTextTreeCell::Impl()) {
36  // We don't want any text in the checkbox, but passing "" seems to make it
37  // not toggle, so we need to pass in something. This way it will just be
38  // extra spacing.
39  impl_->checkbox_ = std::make_shared<Checkbox>(" ");
40  impl_->checkbox_->SetChecked(is_checked);
41  impl_->checkbox_->SetOnChecked(on_toggled);
42  impl_->label_ = std::make_shared<Label>(text);
43  AddChild(impl_->checkbox_);
44  AddChild(impl_->label_);
45 }
46 
48 
49 std::shared_ptr<Checkbox> CheckableTextTreeCell::GetCheckbox() {
50  return impl_->checkbox_;
51 }
52 
53 std::shared_ptr<Label> CheckableTextTreeCell::GetLabel() {
54  return impl_->label_;
55 }
56 
58  const LayoutContext &context, const Constraints &constraints) const {
59  auto check_pref = impl_->checkbox_->CalcPreferredSize(context, constraints);
60  auto label_pref = impl_->label_->CalcPreferredSize(context, constraints);
61  return Size(check_pref.width + label_pref.width,
62  std::max(check_pref.height, label_pref.height));
63 }
64 
66  auto &frame = GetFrame();
67  auto check_width =
68  impl_->checkbox_->CalcPreferredSize(context, Constraints()).width;
69  impl_->checkbox_->SetFrame(
70  Rect(frame.x, frame.y, check_width, frame.height));
71  auto x = impl_->checkbox_->GetFrame().GetRight();
72  impl_->label_->SetFrame(
73  Rect(x, frame.y, frame.GetRight() - x, frame.height));
74 }
75 
76 // ----------------------------------------------------------------------------
78  std::shared_ptr<Checkbox> checkbox_;
79  std::shared_ptr<Label> label_;
80  std::shared_ptr<ColorEdit> color_;
81  float color_width_percent = 0.2f;
82 };
83 
84 LUTTreeCell::LUTTreeCell(const char *text,
85  bool is_checked,
86  const Color &color,
87  std::function<void(bool)> on_enabled,
88  std::function<void(const Color &)> on_color_changed)
89  : impl_(new LUTTreeCell::Impl()) {
90  // We don't want any text in the checkbox, but passing "" seems to make it
91  // not toggle, so we need to pass in something. This way it will just be
92  // extra spacing.
93  impl_->checkbox_ = std::make_shared<Checkbox>(" ");
94  impl_->checkbox_->SetChecked(is_checked);
95  impl_->checkbox_->SetOnChecked(on_enabled);
96  impl_->label_ = std::make_shared<Label>(text);
97  impl_->color_ = std::make_shared<ColorEdit>();
98  impl_->color_->SetValue(color);
99  impl_->color_->SetOnValueChanged(on_color_changed);
100  AddChild(impl_->checkbox_);
101  AddChild(impl_->label_);
102  AddChild(impl_->color_);
103 }
104 
106 
107 std::shared_ptr<Checkbox> LUTTreeCell::GetCheckbox() {
108  return impl_->checkbox_;
109 }
110 
111 std::shared_ptr<Label> LUTTreeCell::GetLabel() { return impl_->label_; }
112 
113 std::shared_ptr<ColorEdit> LUTTreeCell::GetColorEdit() { return impl_->color_; }
114 
116  const Constraints &constraints) const {
117  auto check_pref = impl_->checkbox_->CalcPreferredSize(context, constraints);
118  auto label_pref = impl_->label_->CalcPreferredSize(context, constraints);
119  auto color_pref = impl_->color_->CalcPreferredSize(context, constraints);
120  return Size(check_pref.width + label_pref.width + color_pref.width,
121  std::max(check_pref.height,
122  std::max(label_pref.height, color_pref.height)));
123 }
124 
126  auto em = context.theme.font_size;
127  auto &frame = GetFrame();
128  auto check_width =
129  impl_->checkbox_->CalcPreferredSize(context, Constraints()).width;
130  auto color_width =
131  int(std::ceil(impl_->color_width_percent * float(frame.width)));
132  auto min_color_width = 8 * context.theme.font_size;
133  color_width = std::max(min_color_width, color_width);
134  if (frame.width - (color_width + check_width) < 8 * em) {
135  color_width = frame.width - check_width - 8 * em;
136  }
137  impl_->checkbox_->SetFrame(
138  Rect(frame.x, frame.y, check_width, frame.height));
139  impl_->color_->SetFrame(Rect(frame.GetRight() - color_width, frame.y,
140  color_width, frame.height));
141  auto x = impl_->checkbox_->GetFrame().GetRight();
142  impl_->label_->SetFrame(
143  Rect(x, frame.y, impl_->color_->GetFrame().x - x, frame.height));
144 }
145 
146 // ----------------------------------------------------------------------------
148  std::shared_ptr<NumberEdit> value_;
149  std::shared_ptr<ColorEdit> color_;
150 };
151 
153  double value,
154  const Color &color,
155  std::function<void(double)> on_value_changed,
156  std::function<void(const Color &)> on_color_changed)
157  : impl_(new ColormapTreeCell::Impl()) {
158  impl_->value_ = std::make_shared<NumberEdit>(NumberEdit::DOUBLE);
159  impl_->value_->SetDecimalPrecision(3);
160  impl_->value_->SetLimits(0.0, 1.0);
161  impl_->value_->SetValue(value);
162  impl_->value_->SetOnValueChanged(on_value_changed);
163  impl_->color_ = std::make_shared<ColorEdit>();
164  impl_->color_->SetValue(color);
165  impl_->color_->SetOnValueChanged(on_color_changed);
166  AddChild(impl_->value_);
167  AddChild(impl_->color_);
168 }
169 
171 
172 std::shared_ptr<NumberEdit> ColormapTreeCell::GetNumberEdit() {
173  return impl_->value_;
174 }
175 
176 std::shared_ptr<ColorEdit> ColormapTreeCell::GetColorEdit() {
177  return impl_->color_;
178 }
179 
181  const Constraints &constraints) const {
182  auto number_pref = impl_->value_->CalcPreferredSize(context, constraints);
183  auto color_pref = impl_->color_->CalcPreferredSize(context, constraints);
184  return Size(number_pref.width + color_pref.width,
185  std::max(number_pref.height, color_pref.height));
186 }
187 
189  auto &frame = GetFrame();
190  auto number_pref = impl_->value_->CalcPreferredSize(context, Constraints());
191  impl_->value_->SetFrame(
192  Rect(frame.x, frame.y, number_pref.width, frame.height));
193  auto x = impl_->value_->GetFrame().GetRight();
194  impl_->color_->SetFrame(
195  Rect(x, frame.y, frame.GetRight() - x, frame.height));
196 }
197 
198 // ----------------------------------------------------------------------------
199 namespace {
200 static int g_treeview_id = 1;
201 }
202 
203 struct TreeView::Impl {
205 
206  // Note: use std::list because pointers remain valid, unlike std::vector
207  // which will invalidate pointers when it resizes the underlying
208  // array
209  struct Item {
211  std::string id_string;
212  std::shared_ptr<Widget> cell;
213  Item *parent = nullptr;
214  std::list<Item> children;
215  };
216  int id_;
218  std::unordered_map<TreeView::ItemId, Item *> id2item_;
220  bool can_select_parents_ = false;
222 };
223 
225 
226 TreeView::TreeView() : impl_(new TreeView::Impl()) {
227  impl_->id_ = g_treeview_id++;
228  impl_->root_.id = Impl::g_next_id++;
229  impl_->id2item_[impl_->root_.id] = &impl_->root_;
230 }
231 
233 
234 TreeView::ItemId TreeView::GetRootItem() const { return impl_->root_.id; }
235 
237  std::shared_ptr<Widget> w) {
238  Impl::Item item;
239  item.id = Impl::g_next_id++;
240  // ImGUI uses the text to identify the item, create a ID string
241  std::stringstream s;
242  s << "treeview" << impl_->id_ << "item" << item.id;
243  item.id_string = s.str();
244  item.cell = w;
245 
246  Impl::Item *parent = &impl_->root_;
247  auto parent_it = impl_->id2item_.find(parent_id);
248  if (parent_it != impl_->id2item_.end()) {
249  parent = parent_it->second;
250  }
251  item.parent = parent;
252  parent->children.push_back(item);
253  impl_->id2item_[item.id] = &parent->children.back();
254 
255  return item.id;
256 }
257 
258 TreeView::ItemId TreeView::AddTextItem(ItemId parent_id, const char *text) {
259  std::shared_ptr<Widget> w = std::make_shared<Label>(text);
260  return AddItem(parent_id, w);
261 }
262 
264  auto item_it = impl_->id2item_.find(item_id);
265  if (item_it != impl_->id2item_.end()) {
266  auto item = item_it->second;
267  // Erase the item here, because RemoveItem(child) will also erase,
268  // which will invalidate our iterator.
269  impl_->id2item_.erase(item_it);
270 
271  // Remove children. Note that we can't use a foreach loop here,
272  // because when we remove the item from its parent it will
273  // invalidate the iterator to the current item that exists under
274  // the hood, making `it++` not workable. So we use a while loop
275  // instead. Because this is a list, we can erase from the front
276  // in O(1).
277  while (!item->children.empty()) {
278  RemoveItem(item->children.front().id);
279  }
280 
281  // Remove ourself from our parent's list of children
282  if (item->parent) {
283  for (auto sibling = item->parent->children.begin();
284  sibling != item->parent->children.end(); ++sibling) {
285  if (sibling->id == item_id) {
286  item->parent->children.erase(sibling);
287  break;
288  }
289  }
290  }
291  }
292 }
293 
295  impl_->selected_id_ = -1;
296  impl_->id2item_.clear();
297  impl_->root_.children.clear();
298 }
299 
300 std::shared_ptr<Widget> TreeView::GetItem(ItemId item_id) const {
301  auto item_it = impl_->id2item_.find(item_id);
302  if (item_it != impl_->id2item_.end()) {
303  return item_it->second->cell;
304  }
305  return nullptr;
306 }
307 
308 std::vector<TreeView::ItemId> TreeView::GetItemChildren(
309  ItemId parent_id) const {
310  std::vector<TreeView::ItemId> children;
311  auto item_it = impl_->id2item_.find(parent_id);
312  if (item_it != impl_->id2item_.end()) {
313  auto *parent = item_it->second->parent;
314  if (parent) {
315  children.reserve(parent->children.size());
316  for (auto &child : parent->children) {
317  children.push_back(child.id);
318  }
319  }
320  }
321  return children;
322 }
323 
325  return impl_->can_select_parents_;
326 }
327 
329  impl_->can_select_parents_ = can_select;
330 }
331 
333  if (impl_->selected_id_ < 0) {
334  return impl_->root_.id;
335  } else {
336  return impl_->selected_id_;
337  }
338 }
339 
341  impl_->selected_id_ = item_id;
342 }
343 
345  std::function<void(ItemId)> on_selection_changed) {
346  impl_->on_selection_changed_ = on_selection_changed;
347 }
348 
350  const Constraints &constraints) const {
351  return Size(constraints.width, Widget::DIM_GROW);
352 }
353 
355  return Size(0, 3 * context.theme.font_size);
356 }
357 
359  // Nothing to do here. We don't know the x position because of the
360  // indentations, which also means we don't know the size. So we need
361  // to defer layout to Draw().
362 }
363 
366  auto &frame = GetFrame();
367 
369  float outer_scroll_y = ImGui::GetScrollY();
370  ImGui::SetCursorScreenPos(
371  ImVec2(float(frame.x), float(frame.y) - outer_scroll_y));
372 
373  // ImGUI's tree wants to highlight the row as the user moves over it.
374  // There are several problems here. First, there seems to be a bug in
375  // ImGUI where the highlight ignores the pushed item width and extends
376  // to the end of the ImGUI-window (i.e. the topmost parent Widget). This
377  // means the highlight extends into any margins we have. Not good. Second,
378  // the highlight extends past the clickable area, which is misleading.
379  // Third, no operating system has hover highlights like this, and it looks
380  // really strange. I mean, you can see the cursor right over your text,
381  // what do you need a highligh for? So make this highlight transparent.
382  ImGui::PushStyleColor(ImGuiCol_HeaderActive, // click-hold on item
383  colorToImgui(Color(0, 0, 0, 0)));
384  ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
385  colorToImgui(Color(0, 0, 0, 0)));
386 
387  ImGui::PushStyleColor(ImGuiCol_ChildBg,
388  colorToImgui(context.theme.tree_background_color));
389 
390  // ImGUI's tree is basically a layout in the parent ImGUI window.
391  // Make this a child so it's all in a nice frame.
392  ImGui::BeginChild(impl_->id_,
393  ImVec2(float(frame.width), float(frame.height)), true);
394 
395  Impl::Item *new_selection = nullptr;
396 
397  std::function<void(Impl::Item &)> DrawItem;
398  DrawItem = [&DrawItem, this, &frame, &context, &new_selection, &result,
399  outer_scroll_y](Impl::Item &item) {
400  int height = item.cell
401  ->CalcPreferredSize({context.theme, context.fonts},
402  Constraints())
403  .height;
404 
405  // ImGUI's tree doesn't seem to support selected items,
406  // so we have to draw our own selection.
407  if (item.id == impl_->selected_id_) {
408  // Since we are in a child, the cursor is relative to the upper left
409  // of the tree's frame. To draw directly to the window list we
410  // need to the absolute coordinates (relative the OS window's
411  // upper left)
412  auto y = frame.y - outer_scroll_y + ImGui::GetCursorPosY() -
413  ImGui::GetScrollY();
414  ImGui::GetWindowDrawList()->AddRectFilled(
415  ImVec2(float(frame.x), y),
416  ImVec2(float(frame.GetRight()), y + height),
417  colorToImguiRGBA(context.theme.tree_selected_color));
418  }
419 
420  int flags = ImGuiTreeNodeFlags_DefaultOpen |
421  ImGuiTreeNodeFlags_AllowItemOverlap;
422  if (impl_->can_select_parents_) {
423  flags |= ImGuiTreeNodeFlags_OpenOnDoubleClick;
424  flags |= ImGuiTreeNodeFlags_OpenOnArrow;
425  }
426  if (item.children.empty()) {
427  flags |= ImGuiTreeNodeFlags_Leaf;
428  }
429  bool is_selectable =
430  (item.children.empty() || impl_->can_select_parents_);
431  auto DrawThis = [this, &tree_frame = frame, &context, &new_selection,
432  &result](TreeView::Impl::Item &item, int height,
433  bool is_selectable) {
434  ImGui::SameLine(0, 0);
435  auto x = int(std::round(ImGui::GetCursorScreenPos().x));
436  auto y = int(std::round(
437  ImGui::GetCursorScreenPos().y /*- ImGui::GetScrollY()*/));
438  auto scroll_y = ImGui::GetScrollY();
439  auto scroll_width = int(ImGui::GetStyle().ScrollbarSize);
440  auto indent = x - tree_frame.x;
441  // Note that we add scroll_y to undo the Widget's Draw() subtracting
442  // it off. It needs to subtract it off if the whole ImGUI window
443  // is being scrolled, but not if it is in a TreeView, and it has no
444  // way of knowing the difference. Also it is necessary for clicks to
445  // work correctly.
446  item.cell->SetFrame(Rect(x, y + scroll_y,
447  tree_frame.width - indent - scroll_width,
448  height));
449  // Now that we know the frame we can finally layout. It would be
450  // nice to not relayout until something changed, which would
451  // usually work, unless the cell changes shape in response to
452  // something, which would be a problem. So do it every time.
453  item.cell->Layout({context.theme, context.fonts});
454 
455  ImGui::BeginGroup();
456  auto this_result = item.cell->Draw(context);
457  if (this_result == Widget::DrawResult::REDRAW) {
459  }
460  ImGui::EndGroup();
461 
462  if (ImGui::IsItemClicked() && is_selectable) {
463  impl_->selected_id_ = item.id;
464  new_selection = &item;
465  }
466  };
467 
468  if (ImGui::TreeNodeEx(item.id_string.c_str(), flags, "%s", "")) {
469  DrawThis(item, height, is_selectable);
470 
471  for (auto &child : item.children) {
472  DrawItem(child);
473  }
474  ImGui::TreePop();
475  } else {
476  DrawThis(item, height, is_selectable);
477  }
478  };
479  for (auto &top : impl_->root_.children) {
480  DrawItem(top);
481  }
482 
483  ImGui::EndChild();
484 
485  ImGui::PopStyleColor(3);
487 
488  // If the selection changed, handle the callback here, after we have
489  // finished drawing, so that the callback is able to change the contents
490  // of the tree if it wishes (which could cause a crash if done while
491  // drawing, e.g. deleting the current item).
492  if (new_selection) {
493  if (impl_->on_selection_changed_) {
494  impl_->on_selection_changed_(new_selection->id);
495  }
497  }
498 
499  return result;
500 }
501 
502 } // namespace gui
503 } // namespace visualization
504 } // namespace cloudViewer
Rect frame
int height
math::float4 color
core::Tensor result
Definition: VtkUtils.cpp:76
CheckableTextTreeCell(const char *text, bool is_checked, std::function< void(bool)> on_toggled)
Definition: TreeView.cpp:33
Size CalcPreferredSize(const LayoutContext &context, const Constraints &constraints) const override
Definition: TreeView.cpp:57
void Layout(const LayoutContext &context) override
Definition: TreeView.cpp:65
void Layout(const LayoutContext &context) override
Definition: TreeView.cpp:188
ColormapTreeCell(double value, const Color &color, std::function< void(double)> on_value_changed, std::function< void(const Color &)> on_color_changed)
Definition: TreeView.cpp:152
std::shared_ptr< NumberEdit > GetNumberEdit()
Definition: TreeView.cpp:172
Size CalcPreferredSize(const LayoutContext &context, const Constraints &constraints) const override
Definition: TreeView.cpp:180
std::shared_ptr< ColorEdit > GetColorEdit()
Definition: TreeView.cpp:176
std::shared_ptr< Label > GetLabel()
Definition: TreeView.cpp:111
void Layout(const LayoutContext &context) override
Definition: TreeView.cpp:125
Size CalcPreferredSize(const LayoutContext &context, const Constraints &constraints) const override
Definition: TreeView.cpp:115
std::shared_ptr< ColorEdit > GetColorEdit()
Definition: TreeView.cpp:113
std::shared_ptr< Checkbox > GetCheckbox()
Definition: TreeView.cpp:107
LUTTreeCell(const char *text, bool is_checked, const Color &color, std::function< void(bool)> on_enabled, std::function< void(const Color &)> on_color_changed)
Definition: TreeView.cpp:84
std::shared_ptr< Widget > GetItem(ItemId item_id) const
Returns item, or nullptr if item_id cannot be found.
Definition: TreeView.cpp:300
void Clear()
Clears all the items.
Definition: TreeView.cpp:294
void SetOnSelectionChanged(std::function< void(ItemId)> on_selection_changed)
Definition: TreeView.cpp:344
void SetSelectedItemId(ItemId item_id)
Selects the indicated item of the list. Does not call onValueChanged.
Definition: TreeView.cpp:340
ItemId AddTextItem(ItemId parent_id, const char *text)
Adds a text item to the tree.
Definition: TreeView.cpp:258
void Layout(const LayoutContext &context) override
Definition: TreeView.cpp:358
void SetCanSelectItemsWithChildren(bool can_select)
Definition: TreeView.cpp:328
void RemoveItem(ItemId item_id)
Removes an item an all its children (if any) from the tree.
Definition: TreeView.cpp:263
std::vector< ItemId > GetItemChildren(ItemId parent_id) const
Definition: TreeView.cpp:308
Size CalcPreferredSize(const LayoutContext &context, const Constraints &constraints) const override
Definition: TreeView.cpp:349
DrawResult Draw(const DrawContext &context) override
Definition: TreeView.cpp:364
ItemId GetSelectedItemId() const
Returns the currently selected item id in the tree.
Definition: TreeView.cpp:332
ItemId AddItem(ItemId parent_id, std::shared_ptr< Widget > item)
Adds an item to the tree.
Definition: TreeView.cpp:236
Size CalcMinimumSize(const LayoutContext &context) const override
Definition: TreeView.cpp:354
virtual const Rect & GetFrame() const
Returns the frame size in pixels.
Definition: Widget.cpp:51
virtual void AddChild(std::shared_ptr< Widget > child)
Definition: Widget.cpp:43
static constexpr int DIM_GROW
Definition: Widget.h:83
int max(int a, int b)
Definition: cutil_math.h:48
ImGuiContext * context
Definition: Window.cpp:76
MiniVec< float, N > ceil(const MiniVec< float, N > &a)
Definition: MiniVec.h:89
uint32_t colorToImguiRGBA(const Color &color)
Definition: Util.cpp:25
ImVec4 colorToImgui(const Color &color)
Definition: Util.cpp:20
Generic file read and write utility for python interface.
std::function< void(TreeView::ItemId)> on_selection_changed_
Definition: TreeView.cpp:221
std::unordered_map< TreeView::ItemId, Item * > id2item_
Definition: TreeView.cpp:218