TAMSVIZ
Visualization and annotation tool for ROS
All Classes Pages
propertygrid.cpp
1 // TAMSVIZ
2 // (c) 2020 Philipp Ruppel
3 
4 #include "propertygrid.h"
5 
6 #include "../core/history.h"
7 #include "../core/log.h"
8 #include "../core/serialization.h"
9 #include "../core/workspace.h"
10 
11 Q_DECLARE_METATYPE(Property);
12 static constexpr int PropertyRole = Qt::UserRole + 1;
13 static Property itemProperty(const QModelIndex &index) {
14  return index.data(PropertyRole).value<Property>();
15 }
16 static Property itemProperty(const QTreeWidgetItem *item) {
17  return item->data(1, PropertyRole).value<Property>();
18 }
19 
20 PropertyGridWidget::PropertyGridWidget() : QDockWidget("Properties") {
21  LockScope ws;
22  class PropertyTreeWidget : public TreeWidget {
23  PropertyGridWidget *property_grid = nullptr;
24  QPoint drag_start_pos;
25  double drag_start_value = 0.0;
26  bool aggregate = false;
27 
28  public:
29  PropertyTreeWidget(PropertyGridWidget *property_grid)
30  : property_grid(property_grid) {
31  setParent(property_grid);
32  }
33  /*virtual void paintEvent(QPaintEvent *event) override {
34  TreeWidget::paintEvent(event);
35  }*/
36  virtual void mousePressEvent(QMouseEvent *event) override {
37  QTreeWidget::mousePressEvent(event);
38  if (property_grid->drag_property) {
39  LockScope ws;
40  drag_start_pos = event->pos();
41  auto object = property_grid->object_ptr.lock();
42  if (!object) {
43  LOG_DEBUG("object is null");
44  return;
45  }
46  auto &t = property_grid->drag_property->typeId();
47  std::string value_string;
48  property_grid->drag_property->tryToString(value_string);
49  drag_start_value = std::stod(value_string);
50  }
51  aggregate = false;
52  }
53  virtual void mouseMoveEvent(QMouseEvent *event) override {
54  if (property_grid->drag_property) {
55  LockScope ws;
56  auto object = property_grid->object_ptr.lock();
57  if (!object) {
58  LOG_DEBUG("object is null");
59  return;
60  }
61  auto &t = property_grid->drag_property->typeId();
62  double value = 0.0;
63  if (std::isfinite(property_grid->drag_property->attributes()->min) &&
64  std::isfinite(property_grid->drag_property->attributes()->max)) {
65  value = drag_start_value + (event->x() - event->y() -
66  drag_start_pos.x() + drag_start_pos.y()) *
67  1.0 / columnWidth(1);
68  } else {
69  int step_div = 2;
70  double step_size = 0.005;
71  if (t != typeid(float) && t != typeid(double)) {
72  step_div = 10;
73  step_size = 1;
74  }
75  if (std::isfinite(
76  property_grid->drag_property->attributes()->step_scale)) {
77  step_size *= property_grid->drag_property->attributes()->step_scale;
78  }
79  int delta = (event->x() - event->y() - drag_start_pos.x() +
80  drag_start_pos.y()) /
81  step_div;
82  value = drag_start_value + delta * step_size;
83  }
84  if (property_grid->drag_property->attributes()->wrap) {
85  value -= property_grid->drag_property->attributes()->min;
86  value /= property_grid->drag_property->attributes()->max -
87  property_grid->drag_property->attributes()->min;
88  value -= std::floor(value);
89  value *= property_grid->drag_property->attributes()->max -
90  property_grid->drag_property->attributes()->min;
91  value += property_grid->drag_property->attributes()->min;
92  } else {
93  if (value < property_grid->drag_property->attributes()->min) {
94  value = property_grid->drag_property->attributes()->min;
95  }
96  if (value > property_grid->drag_property->attributes()->max) {
97  value = property_grid->drag_property->attributes()->max;
98  }
99  }
100  LOG_INFO("drag " << value << " "
101  << property_grid->drag_property->attributes()->min
102  << " "
103  << property_grid->drag_property->attributes()->max);
104  ActionScope action(std::string("Drag Property ") +
105  property_grid->drag_property->displayName(),
106  /*object*/ nullptr, aggregate);
107  aggregate = true;
108  property_grid->drag_property->fromStringOrThrow(std::to_string(value));
109  ws->modified();
110  } else {
111  QTreeWidget::mouseMoveEvent(event);
112  }
113  }
114  virtual void mouseReleaseEvent(QMouseEvent *event) override {
115  unsetCursor();
116 
117  aggregate = false;
118  property_grid->drag_property = nullptr;
119  property_grid->drag_item = nullptr;
120  QTreeWidget::mouseReleaseEvent(event);
121  }
122  };
123  view = new PropertyTreeWidget(this);
124  setWidget(view);
125  ws->modified.connect(this, [this]() { sync(); });
126  view->setColumnCount(2);
127  view->setEditTriggers(QAbstractItemView::DoubleClicked |
128  QAbstractItemView::EditKeyPressed |
129  QAbstractItemView::AnyKeyPressed);
130  view->setSelectionMode(QAbstractItemView::SingleSelection);
131  view->setHeaderLabels({"Name", "Value"});
132 
133  view->header()->setSectionResizeMode(1, QHeaderView::Stretch);
134  view->setIndentation(10);
135 
136  class ItemDelegateBase : public QStyledItemDelegate {
137  PropertyGridWidget *_parent = nullptr;
138 
139  public:
140  ItemDelegateBase(PropertyGridWidget *parent)
141  : QStyledItemDelegate(parent), _parent(parent) {}
142  void drawLines(QPainter *painter, const QStyleOptionViewItem &option,
143  const QModelIndex &index) const {
144  painter->setPen(QPen(QApplication::palette().brush(QPalette::Window), 0));
145  painter->setBrush(QBrush(Qt::transparent));
146 
147  auto rect = option.rect;
148  rect.setX(_parent->view->columnViewportPosition(index.column()));
149  rect.setWidth(_parent->view->columnWidth(index.column()));
150  if (index.column() == 0) {
151  rect.setX(rect.x() - 1);
152  rect.setWidth(rect.width() + 1);
153  }
154  painter->drawRect(rect);
155  }
156  void paint(QPainter *painter, const QStyleOptionViewItem &option,
157  const QModelIndex &index) const {
158  QStyledItemDelegate::paint(painter, option, index);
159  drawLines(painter, option, index);
160  }
161  };
162  {
163  class EditDelegate : public ItemDelegateBase {
164  PropertyGridWidget *property_grid = nullptr;
165 
166  public:
167  EditDelegate(PropertyGridWidget *property_grid)
168  : ItemDelegateBase(property_grid), property_grid(property_grid) {}
169  virtual QWidget *createEditor(QWidget *parent,
170  const QStyleOptionViewItem &option,
171  const QModelIndex &index) const override {
172 
173  return nullptr;
174  }
175  };
176  view->setItemDelegateForColumn(0, new EditDelegate(this));
177  }
178 
179  connect(view, &QTreeWidget::currentItemChanged, view, [this]() {
180  if (view->currentColumn() != 1) {
181  view->setCurrentItem(view->currentItem(), 1);
182  }
183  });
184  {
185  class EditDelegate : public ItemDelegateBase {
186  PropertyGridWidget *property_grid = nullptr;
187 
188  public:
189  EditDelegate(PropertyGridWidget *property_grid)
190  : ItemDelegateBase(property_grid), property_grid(property_grid) {}
191  virtual void paint(QPainter *painter, const QStyleOptionViewItem &option,
192  const QModelIndex &index) const override {
193 
194  drawLines(painter, option, index);
195 
196  auto rect = option.rect.marginsRemoved(QMargins(1, 1, 0, 0));
197 
198  {
199  auto color = ((option.state & QStyle::State_Active) &&
200  (option.state & QStyle::State_Selected))
201  ? option.palette.color(QPalette::Highlight)
202  : option.palette.color(QPalette::Base);
203  painter->fillRect(rect, QBrush(color));
204  }
205 
206  if (auto object = property_grid->object_ptr.lock()) {
207  Property property = itemProperty(index);
208  if (std::isfinite(property.attributes()->min) &&
209  std::isfinite(property.attributes()->max)) {
210  double v = index.data(Qt::DisplayRole).toDouble();
211  rect.setWidth(std::round(
212  rect.width() *
213  ((v - property.attributes()->min) /
214  (property.attributes()->max - property.attributes()->min))));
215  {
216  auto color = option.palette.color(QPalette::Text);
217  color.setAlphaF(0.1);
218  if ((option.state & QStyle::State_Active) &&
219  (option.state & QStyle::State_Selected)) {
220  color.setAlphaF(1.0 / 3.0);
221  }
222  painter->fillRect(rect, QBrush(color));
223  }
224  }
225  }
226 
227  QStyleOptionViewItem opt = option;
228  initStyleOption(&opt, index);
229  opt.backgroundBrush = QBrush(Qt::transparent);
230  opt.state &= ~QStyle::State_Selected;
231  if ((option.state & QStyle::State_Active) &&
232  (option.state & QStyle::State_Selected)) {
233  opt.palette.setBrush(QPalette::Text, opt.palette.highlightedText());
234  }
235  QStyle *style =
236  (option.widget ? option.widget->style() : QApplication::style());
237  style->drawControl(QStyle::CE_ItemViewItem, &opt, painter,
238  option.widget);
239  }
240  virtual bool editorEvent(QEvent *event, QAbstractItemModel *model,
241  const QStyleOptionViewItem &option,
242  const QModelIndex &index) override {
243  LockScope ws;
244  bool ret =
245  QStyledItemDelegate::editorEvent(event, model, option, index);
246  if (auto *item = property_grid->view->itemFromIndex(index)) {
247  if (event->type() == QEvent::MouseButtonPress) {
248  if (auto *mouse_event = dynamic_cast<QMouseEvent *>(event)) {
249  if (mouse_event->button() == Qt::LeftButton) {
250  auto current_property = itemProperty(index);
251  if (property_grid->isPropertyDraggable(item)) {
252  property_grid->drag_property =
253  std::make_shared<Property>(current_property);
254  property_grid->drag_item = item;
255  property_grid->view->setCursor(Qt::ClosedHandCursor);
256  }
257  if (current_property.typeId() == typeid(bool)) {
258  ActionScope ws("Toggle");
259  bool v = current_property.read().value<bool>();
260  v = !v;
261  current_property.assign(Variant(v));
262  ws->modified();
263  }
264  }
265  }
266  }
267  }
268  return ret;
269  }
270  virtual QWidget *createEditor(QWidget *parent,
271  const QStyleOptionViewItem &option,
272  const QModelIndex &index) const override {
273  LockScope ws;
274  auto *item = property_grid->view->itemFromIndex(index);
275  if (!item) {
276  LOG_ERROR("tree item not found");
277  return nullptr;
278  }
279  auto current_property = itemProperty(index);
280  if (current_property.typeId() == typeid(bool)) {
281  LOG_DEBUG("bool");
282  ActionScope ws("Toggle");
283  bool v = current_property.read().value<bool>();
284  v = !v;
285  current_property.assign(Variant(v));
286  ws->modified();
287  return nullptr;
288  }
289  if (current_property.fromStringSupported()) {
290  LOG_DEBUG("editable");
291  if (current_property.attributes()->list) {
292  LOG_DEBUG("combo box");
293  auto *editor = new QComboBox(parent);
294  editor->setEditable(true);
295  editor->setFrame(false);
296  editor->setStyleSheet("border: none;");
297  for (auto &v :
298  current_property.attributes()->list(current_property)) {
299  editor->addItem(v.c_str());
300  }
301  connect(
302  editor,
303  static_cast<void (QComboBox::*)(int)>(&QComboBox::activated),
304  [editor, index, this](int i) {
305  setModelData(editor, property_grid->view->model(), index);
306  });
307  return editor;
308  } else {
309  LOG_DEBUG("line edit");
310 
311  auto editor = new QLineEdit(parent);
312  editor->setFrame(false);
313  if (auto complete = current_property.attributes()->complete) {
314  auto *completion_model = new QStringListModel(editor);
315  auto *completer = new QCompleter(completion_model, editor);
316  completer->setCaseSensitivity(Qt::CaseInsensitive);
317  completer->setCompletionMode(
318  QCompleter::UnfilteredPopupCompletion);
319  completer->setMaxVisibleItems(20);
320  completer->setWidget(editor);
321  auto update = [current_property, completion_model, complete,
322  editor, completer](bool show) {
323  QString text = editor->text();
324  QStringList l;
325  AutoCompletion completion;
326  {
327  LockScope ws;
328  complete(current_property, text.toStdString(), completion);
329  for (auto &s : completion.items) {
330  QString qs = QString(s.c_str());
331  l.push_back(qs);
332  }
333  }
334  l.sort();
335  {
336  QStringList a, b;
337  for (auto &s : l) {
338  if (s.startsWith(text) && s.size() > text.size()) {
339  a.push_back(s);
340  } else {
341  b.push_back(s);
342  }
343  }
344  l = a + b;
345  }
346  if (!l.isEmpty()) {
347  completion_model->setStringList(l);
348  completer->complete();
349  if (!completion.completed || show) {
350  LOG_DEBUG("show auto completion list");
351  completer->popup()->show();
352  } else {
353  LOG_DEBUG("hide auto completion list");
354  completer->popup()->hide();
355  }
356  } else {
357  LOG_DEBUG("not auto completions, hide list");
358  completer->popup()->hide();
359  }
360  };
361  connect(editor, &QLineEdit::textEdited, editor,
362  [update, completer](const QString &) { update(false); });
363  connect(completer,
364  static_cast<void (QCompleter::*)(const QString &text)>(
365  &QCompleter::activated),
366  [editor, update, completer](const QString &text) {
367  if (text != editor->text()) {
368  editor->setText(text);
369  update(false);
370  }
371  });
372  QTimer::singleShot(0, completer,
373  [completer, update]() { update(true); });
374  }
375 
376  return editor;
377  }
378  } else {
379  LOG_DEBUG("not editable");
380  return nullptr;
381  }
382  }
383  virtual void setModelData(QWidget *editor, QAbstractItemModel *model,
384  const QModelIndex &index) const override {
385  LockScope ws;
386  if (editor) {
387  auto *item = property_grid->view->itemFromIndex(index);
388  if (!item) {
389  LOG_ERROR("tree item not found");
390  return;
391  }
392  std::shared_ptr<Object> object = property_grid->object_ptr.lock();
393  auto property = itemProperty(index);
394  std::string value =
395  editor->property(editor->metaObject()->userProperty().name())
396  .toString()
397  .toStdString();
398  {
399  std::string v;
400  property.tryToString(v);
401  if (value == v) {
402  LOG_DEBUG("property unchanged");
403  return;
404  }
405  }
406  {
407  ActionScope action(
408  std::string("Change ") +
409  property.displayName() /*,
410 object*/);
411  try {
412  property.fromStringOrThrow(value);
413  LOG_DEBUG("property changed");
414  } catch (std::exception &ex) {
415  LOG_ERROR("failed to parse value string");
416  }
417  }
418  {
419  std::string v;
420  property.tryToString(v);
421  model->setData(index, v.c_str(), 0);
422  }
423  }
424  }
425  };
426  view->setItemDelegateForColumn(1, new EditDelegate(this));
427  }
428 }
429 
430 bool PropertyGridWidget::isPropertyDraggable(QTreeWidgetItem *item) {
431  LockScope ws;
432  if (item) {
433  if (auto object = object_ptr.lock()) {
434  if (!item->data(1, PropertyRole).isNull()) {
435  auto current_property = itemProperty(item);
436  std::type_index t = current_property.typeId();
437  return t == typeid(double) || t == typeid(float) ||
438  t == typeid(short) || t == typeid(int) || t == typeid(long) ||
439  t == typeid(long long) || t == typeid(unsigned short) ||
440  t == typeid(unsigned int) || t == typeid(unsigned long) ||
441  t == typeid(unsigned long long);
442  }
443  }
444  }
445  return false;
446 }
447 
448 void PropertyGridWidget::sync(QTreeWidgetItem *parent_item,
449  std::vector<Property> properties) {
450  LockScope ws;
451  size_t rows = 0;
452  for (auto &property : properties) {
453  if (property.attributes()->hidden) {
454  continue;
455  }
456  std::string s;
457  bool ok = property.tryToString(s);
458  std::vector<Property> children;
459  if (!ok) {
460  property.expand(children);
461  if (!children.empty()) {
462  ok = true;
463  }
464  }
465  if (!ok) {
466  ok = property.expandList(children);
467  LOG_DEBUG("list elements " << children.size());
468  }
469  if (!ok) {
470  LOG_DEBUG("failed to analyze property " << property.name());
471  continue;
472  }
473  rows++;
474  QTreeWidgetItem *item = nullptr;
475  bool is_new_item = false;
476  if (parent_item) {
477  item = parent_item->child(rows - 1);
478  if (!item) {
479  item = new QTreeWidgetItem();
480  item->setFlags(item->flags() | Qt::ItemIsEditable);
481  parent_item->addChild(item);
482  is_new_item = true;
483  }
484  } else {
485  item = view->topLevelItem(rows - 1);
486  if (!item) {
487  item = new QTreeWidgetItem();
488  view->addTopLevelItem(item);
489  is_new_item = true;
490  }
491  }
492  item->setData(1, PropertyRole, QVariant::fromValue(property));
493  if (is_new_item) {
494  item->setText(0, property.displayName().c_str());
495  }
496  item->setText(1, QString::fromStdString(s));
497  item->setFlags(item->flags() | Qt::ItemIsEditable);
498  if (isPropertyDraggable(item)) {
499  item->setData(1, Qt::DecorationRole, MATERIAL_ICON("swap_vert"));
500  }
501  if (property.typeId() == typeid(bool)) {
502  if (property.get<bool>()) {
503  item->setData(1, Qt::DecorationRole, MATERIAL_ICON("check_box"));
504  } else {
505  item->setData(1, Qt::DecorationRole,
506  MATERIAL_ICON("check_box_outline_blank"));
507  }
508  }
509  if (children.size()) {
510  sync(item, children);
511  if (is_new_item) {
512  item->setExpanded(true);
513  }
514  }
515  }
516  if (parent_item) {
517  while (parent_item->childCount() > rows) {
518  delete parent_item->takeChild(rows);
519  }
520  } else {
521  while (view->topLevelItemCount() > rows) {
522  delete view->topLevelItem(rows);
523  }
524  }
525 }
526 
527 void PropertyGridWidget::sync() {
528  LOG_DEBUG("PropertyGridWidget::sync");
529  LockScope ws;
530  auto selected_objects = ws->selection().resolve(ws());
531  auto object =
532  (selected_objects.size() == 1 ? selected_objects.front() : nullptr);
533  if (object == nullptr || object_id != object->id()) {
534  while (view->topLevelItemCount()) {
535  delete view->topLevelItem(0);
536  }
537  }
538 
539  object_id = (object ? object->id() : 0);
540  object_ptr = object;
541  if (object != nullptr) {
542  auto props = object->properties();
543  sync(nullptr, std::vector<Property>(props.begin(), props.end()));
544  }
545 }
Definition: variant.h:9