NJointControllerClassesWidget.cpp
Go to the documentation of this file.
1 /*
2  * This file is part of ArmarX.
3  *
4  * ArmarX is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License version 2 as
6  * published by the Free Software Foundation.
7  *
8  * ArmarX is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  *
16  * \package RobotAPI::gui-plugins::RobotUnitPlugin
17  * \author Raphael Grimm ( raphael dot grimm at kit dot edu )
18  * \date 2017
19  * \copyright http://www.gnu.org/licenses/gpl-2.0.txt
20  * GNU General Public License
21  */
22 
23 #include <filesystem>
24 
29 
30 #include <QGridLayout>
31 #include <QDir>
32 #include <QSortFilterProxyModel>
33 
34 namespace armarx
35 {
37  RobotUnitWidgetTemplateBase("NJointControllerClassesWidget", parent)
38  {
39  connect(ui->pushButtonLoadLib, SIGNAL(released()), this, SLOT(loadLibClicked()));
40  connect(ui->comboBoxPackage, SIGNAL(currentIndexChanged(QString)), this, SLOT(packageEditChanged()));
41  connect(ui->comboBoxPackage, SIGNAL(editTextChanged(QString)), this, SLOT(packageEditChanged()));
42  ui->comboBoxPackage->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
43  ui->comboBoxPackage->setFixedWidth(300);
44 
45  //get package hints
46  {
47  using namespace std::filesystem;
48  std::string homeDir = QDir::homePath().toStdString();
49  path p = path {homeDir} / ".cmake" / "packages";
50  if (is_directory(p))
51  {
52  for (const path& entry : directory_iterator(p))
53  {
54  const std::string pkg = entry.filename().string();
55  if (CMakePackageFinder {pkg, "", true} .packageFound())
56  {
57  ui->comboBoxPackage->addItem(QString::fromStdString(pkg));
58  }
59  }
60  // for sorting you need the following 4 lines
61  QSortFilterProxyModel* proxy = new QSortFilterProxyModel(ui->comboBoxPackage);
62  proxy->setSourceModel(ui->comboBoxPackage->model());
63  // combo's current model must be reparented,
64  // otherwise QComboBox::setModel() will delete it
65  ui->comboBoxPackage->model()->setParent(proxy);
66  ui->comboBoxPackage->setModel(proxy);
67  ui->comboBoxPackage->model()->sort(0);
68  }
69  }
70 
71  ui->comboBoxPackage->setEditText("");
72  packageEditChanged();
73 
74  ui->treeWidget->setColumnCount(2);
75 
76  QTreeWidgetItem* head = ui->treeWidget->headerItem();
77  head->setText(0, "Class");
78  head->setText(1, "");
79  head->setToolTip(0, "Controller class name");
80  ui->treeWidget->header()->setResizeMode(0, QHeaderView::Fixed);
81  ui->treeWidget->header()->setResizeMode(1, QHeaderView::Fixed);
82 
83  }
84 
86  {
87  delete ui;
88  }
89 
91  {
93  std::unique_lock<std::recursive_timed_mutex> guard {mutex};
94  if (!robotUnit)
95  {
96  return;
97  }
98  ru = robotUnit;
99  guard.unlock();
100  auto data = ru->getNJointControllerClassDescription(name);
101  guard.lock();
102  nJointControllerClassDescriptions[data.className] = std::move(data);
103  if (doMetaCall)
104  {
105  QMetaObject::invokeMethod(this, "updateContent", Qt::QueuedConnection);
106  }
107  }
108 
110  {
111  const auto oldName = getDefaultName();
112  if (oldName == createdName || force)
113  {
114  ++defaultControllerName;
115  const auto newName = getDefaultName();
116  for (auto& pair : entries)
117  {
118  pair.second->updateDefaultName(oldName, newName);
119  }
120  }
121  }
122 
124  {
125  return QString::number(defaultControllerName);
126  }
127 
129  {
130  ui->lineEditLibrary->setText(settings->value("classLoadLineEditLib", "").toString());
131  ui->comboBoxPackage->lineEdit()->setText(settings->value("classLoadComboBoxPkg", "").toString());
132  ui->comboBoxLibrary->lineEdit()->setText(settings->value("classLoadComboBoxLib", "").toString());
133  }
134 
136  {
137  settings->setValue("classLoadLineEditLib", ui->lineEditLibrary->text());
138  settings->setValue("classLoadComboBoxPkg", ui->comboBoxPackage->currentText());
139  settings->setValue("classLoadComboBoxLib", ui->comboBoxLibrary->currentText());
140  }
141 
143  {
144  entries.clear();
145  nJointControllerClassDescriptions.clear();
146  }
147 
149  {
150  for (const auto& pair : nJointControllerClassDescriptions)
151  {
152  add(pair.second);
153  }
154  nJointControllerClassDescriptions.clear();
155  }
156 
158  {
159  auto temp = robotUnit->getNJointControllerClassDescriptions();
160  {
161  std::unique_lock<std::recursive_timed_mutex> guard {mutex};
162  for (NJointControllerClassDescription& ds : temp)
163  {
164  nJointControllerClassDescriptions[ds.className] = std::move(ds);
165  }
166  }
167  }
168 
170  {
171  if (nJointControllerClassDescriptions.empty())
172  {
173  return true;
174  }
175  add(nJointControllerClassDescriptions.begin()->second);
176  nJointControllerClassDescriptions.erase(nJointControllerClassDescriptions.begin());
177  return false;
178  }
179 
180  void NJointControllerClassesWidget::filterUpdated()
181  {
182  for (auto& entry : entries)
183  {
184  NJointControllerClassesWidgetEntry* item = entry.second;
185  if (! filterNameActive->isChecked() && !filterRemoteCreationActive->isChecked())
186  {
187  item->setVisible(true);
188  return;
189  }
190  const bool combineOr = (filterCombination->currentText() == "Or");
191  bool showName = !combineOr; //init to neutral element
192  bool showRemote = !combineOr;//init to neutral element
193 
194  if (filterNameActive->isChecked())
195  {
196  showName = (item->matchName(filterName->text()) != filterNameInverted->isChecked());
197  }
198  if (filterRemoteCreationActive->isChecked())
199  {
200  if (item->hasRemoteCreation())
201  {
202  showRemote = (filterRemoteCreation->currentText() != "Without");
203  }
204  else
205  {
206  showRemote = (filterRemoteCreation->currentText() != "With");
207  }
208  }
209  if (combineOr)
210  {
211  item->setVisible(showName || showRemote);
212  }
213  else
214  {
215  item->setVisible(showName && showRemote);
216  }
217  }
218  }
219 
220  void NJointControllerClassesWidget::add(const NJointControllerClassDescription& desc)
221  {
222  std::unique_lock<std::recursive_timed_mutex> guard {mutex};
223  if (entries.count(desc.className))
224  {
225  return;
226  }
227  entries[desc.className] = new NJointControllerClassesWidgetEntry(*this, *(ui->treeWidget), desc, robotUnit);
228  }
229 
230  void NJointControllerClassesWidget::addFilter()
231  {
232  QTreeWidgetItem* filterHeader = new QTreeWidgetItem;
233  ui->treeWidget->addTopLevelItem(filterHeader);
234  filterHeader->setText(0, "Filter");
235 
236  QTreeWidgetItem* filter = new QTreeWidgetItem;
237  filterHeader->addChild(filter);
238  filter->setFirstColumnSpanned(true);
239  QWidget* w = new QWidget;
240  QGridLayout* l = new QGridLayout;
241  w->setLayout(l);
242  l->setContentsMargins(0, 0, 0, 0);
243  l->addItem(new QSpacerItem {0, 0, QSizePolicy::MinimumExpanding}, 0, 3);
244  l->addWidget(new QLabel {"Combine filters with"}, 0, 0, 1, 1);
245  filterCombination = new QComboBox;
246  filterCombination->addItem("Or");
247  filterCombination->addItem("And");
248  l->addWidget(filterCombination, 0, 1, 1, 1);
249 
250  filterNameActive = new QCheckBox;
251  filterNameActive->setText("Filter by name");
252  l->addWidget(filterNameActive, 1, 0, 1, 1);
253 
254  filterName = new QLineEdit;
255  l->addWidget(filterName, 1, 1, 1, 1);
256 
257  filterNameInverted = new QCheckBox;
258  filterNameInverted->setText("Invert filter");
259  l->addWidget(filterNameInverted, 1, 2, 1, 1);
260  connect(filterNameActive, SIGNAL(toggled(bool)), filterName, SLOT(setEnabled(bool)));
261  connect(filterNameActive, SIGNAL(toggled(bool)), filterNameInverted, SLOT(setEnabled(bool)));
262  filterName->setEnabled(false);
263  filterNameInverted->setEnabled(false);
264 
265  filterRemoteCreationActive = new QCheckBox;
266  filterRemoteCreationActive->setText("Filter by remote creation capabilities");
267  l->addWidget(filterRemoteCreationActive, 2, 0, 1, 1);
268  filterRemoteCreation = new QComboBox;
269  filterRemoteCreation->addItem("Both");
270  filterRemoteCreation->addItem("With");
271  filterRemoteCreation->addItem("Without");
272  l->addWidget(filterRemoteCreation, 2, 1, 1, 1);
273  connect(filterRemoteCreationActive, SIGNAL(toggled(bool)), filterRemoteCreation, SLOT(setEnabled(bool)));
274  filterRemoteCreationActive->setEnabled(false);
275 
276  connect(filterCombination, SIGNAL(currentIndexChanged(QString)), this, SLOT(filterUpdated()));
277  connect(filterNameActive, SIGNAL(clicked(bool)), this, SLOT(filterUpdated()));
278  connect(filterRemoteCreationActive, SIGNAL(clicked(bool)), this, SLOT(filterUpdated()));
279  connect(filterNameInverted, SIGNAL(clicked(bool)), this, SLOT(filterUpdated()));
280  connect(filterName, SIGNAL(textChanged(QString)), this, SLOT(filterUpdated()));
281  connect(filterRemoteCreation, SIGNAL(currentIndexChanged(int)), this, SLOT(filterUpdated()));
282 
283  filterNameActive->setChecked(false);
284  filterRemoteCreationActive->setChecked(false);
285 
286  ui->treeWidget->setItemWidget(filter, 0, w);
287  }
288 
291  QTreeWidget& treeWidget,
292  const NJointControllerClassDescription& desc,
294  ) :
295  QObject {&parent},
296  className {desc.className},
297  classNameQStr {QString::fromStdString(className)},
298  robotUnit {robotUnit},
299  parent {&parent}
300  {
301  header = new QTreeWidgetItem {{QString::fromStdString(desc.className)}};
302  treeWidget.addTopLevelItem(header);
303  treeWidget.resizeColumnToContents(0);
304  QWidget* headerW = new QWidget;
305  headerW->setLayout(new QHBoxLayout);
306  headerW->layout()->setContentsMargins(0, 0, 0, 0);
307  headerW->layout()->addItem(new QSpacerItem {0, 0, QSizePolicy::MinimumExpanding});
308  if (desc.configDescription)
309  {
310  //add textfiel + button
311  {
312  nameEdit = new QLineEdit;
313  nameEdit->setText(parent.getDefaultName());
314  nameEdit->setToolTip("The instance name for the created controller instance");
315  headerW->layout()->addWidget(new QLabel {"Controller name"});
316  headerW->layout()->addWidget(nameEdit);
317  QPushButton* button = new QPushButton {"Create"};
318  headerW->layout()->addWidget(button);
319  button->setToolTip("Create a new controller instance of this class");
320  connect(button, &QPushButton::clicked, this, &NJointControllerClassesWidgetEntry::createCtrl);
321  connect(nameEdit, &QLineEdit::returnPressed, this, &NJointControllerClassesWidgetEntry::createCtrl);
322  }
323  //descr
324  {
325  QTreeWidgetItem* child = new QTreeWidgetItem;
326  header->addChild(child);
327  child->setFirstColumnSpanned(true);
328  QWidget* compressWid = new QWidget;
329  QHBoxLayout* compressLay = new QHBoxLayout;
330  compressWid->setLayout(compressLay);
331  compressLay->setContentsMargins(0, 0, 0, 0);
332  creator = WidgetDescription::makeDescribedWidget(desc.configDescription);
333  creator->layout()->setContentsMargins(0, 0, 0, 0);
334  compressLay->addWidget(creator);
335  compressLay->addItem(new QSpacerItem {0, 0, QSizePolicy::MinimumExpanding});
336  treeWidget.setItemWidget(child, 0, compressWid);
337  }
338  }
339  else
340  {
341  //dummy to force the same height for all lines
342  QLineEdit* dummy = new QLineEdit;
343  dummy->setFixedWidth(0);
344  dummy->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum);
345  headerW->layout()->addWidget(dummy);
346 
347  headerW->layout()->addWidget(new QLabel {"No remote creation allowed."});
348  }
349  treeWidget.setItemWidget(header, 1, headerW);
350  }
351 
353  {
354  return classNameQStr.contains(name, Qt::CaseInsensitive);
355  }
356 
358  {
359  return creator;
360  }
361 
362  void NJointControllerClassesWidgetEntry::updateDefaultName(const QString& oldName, const QString& newName)
363  {
364  if (nameEdit && nameEdit->text() == oldName)
365  {
366  nameEdit->setText(newName);
367  }
368  }
369 
371  {
372  header->setHidden(!vis);
373  }
374 
375  void NJointControllerClassesWidgetEntry::createCtrl()
376  {
377  const auto instanceName = nameEdit->text().toStdString();
378  const auto variants = creator->getVariants();
379  if (variants.empty())
380  {
381  ARMARX_INFO << "creating " << instanceName << " of class " << className << " with no parameters\n";
382  }
383  else
384  {
385  std::stringstream ss;
386  ss << "creating " << instanceName << " of class " << className << " with parameters:\n";
387  for (const auto& pair : variants)
388  {
389  if (pair.second)
390  {
391 
392  if (pair.second->data)
393  {
394  ss << " '" << pair.first << "' of type " << pair.second->data->ice_id() << "\n";
395  }
396  else
397  {
398  ss << " '" << pair.first << "' nullptr data \n";
399  }
400  }
401  else
402  {
403  ss << " '" << pair.first << "' nullptr\n";
404  }
405  }
406  ARMARX_INFO << ss.str();
407  }
408  robotUnit->createNJointControllerFromVariantConfig(className, instanceName, variants);
410  }
411 
412  void NJointControllerClassesWidget::packageEditChanged()
413  {
414  auto package = ui->comboBoxPackage->currentText();
415  if (package.isEmpty())
416  {
417  selectLibMode = SelectLibsMode::LineEdit;
418  ui->lineEditLibrary->setVisible(true);
419  ui->comboBoxLibrary->setVisible(false);
420  ui->pushButtonLoadLib->setEnabled(true);
421  ui->labelPackageFound->setPixmap(QPixmap(":/icons/Blank.svg").scaled(16, 16));
422  ui->labelPackageFound->setToolTip("");
423  }
424  else
425  {
426  libShortNameToFileName.clear();
427  ui->comboBoxLibrary->clear();
428 
429  selectLibMode = SelectLibsMode::ComboBox;
430  ui->lineEditLibrary->setVisible(false);
431  ui->comboBoxLibrary->setVisible(true);
432  CMakePackageFinder pFinder(package.toStdString());
433  if (pFinder.packageFound())
434  {
435  ui->pushButtonLoadLib->setEnabled(true);
436  ui->labelPackageFound->setPixmap(QPixmap(":/icons/user-online.svg").scaled(16, 16));
437  ui->labelPackageFound->setToolTip("Found Package");
438  int libidx = -1;
439  for (const std::string& lib : Split(pFinder.getLibs(), ",; ", true))
440  {
441  if (lib.empty())
442  {
443  return;
444  }
445  const auto libPrefix = lib.find("lib");
446  const auto libSubstrStart = libPrefix == lib.npos ? 0 : libPrefix + 3;
447  const auto libSuffix = lib.find(".");
448  const auto libSubstrEnd = libSuffix - libSubstrStart;
449  std::string shortName = lib.substr(libSubstrStart, libSubstrEnd);
450  libShortNameToFileName[shortName] = lib;
451  ui->comboBoxLibrary->addItem(QString::fromStdString(shortName));
452  if (libidx == -1 && (lib.find("Controller") != lib.npos || lib.find("controller") != lib.npos))
453  {
454  libidx = libShortNameToFileName.size() - 1 ;
455  }
456  ui->comboBoxLibrary->setCurrentIndex(libidx);
457  }
458  }
459  else
460  {
461  ui->pushButtonLoadLib->setEnabled(false);
462  ui->labelPackageFound->setPixmap(QPixmap(":/icons/dialog-cancel-5.svg").scaled(16, 16));
463  ui->labelPackageFound->setToolTip("Cannot find Package");
464  }
465  }
466  }
467 
468  void NJointControllerClassesWidget::loadLibClicked()
469  {
470  ARMARX_WARNING << "The functionality of dynamically loading libraries during runtime is no longer supported. Use"
471  "the component properties to load additional libraries.";
472  return;
473 // if (!robotUnit)
474 // {
475 // return;
476 // }
477 
478 // switch (selectLibMode)
479 // {
480 // case SelectLibsMode::LineEdit:
481 // {
482 // auto lib = ui->lineEditLibrary->text().toStdString();
483 // if (lib.empty())
484 // {
485 // return;
486 // }
487 //// ARMARX_INFO << "requesting to load lib " << lib
488 //// << " -> " << robotUnit->loadLibFromPath(lib);
489 // }
490 // break;
491 //
492 // case SelectLibsMode::ComboBox:
493 // {
494 // auto package = ui->comboBoxPackage->currentText().toStdString();
495 // auto lib = ui->comboBoxLibrary->currentText().toStdString();
496 // if (lib.empty())
497 // {
498 // return;
499 // }
500 // if (libShortNameToFileName.count(lib))
501 // {
502 // lib = libShortNameToFileName.at(lib);
503 // }
504 //// ARMARX_INFO << "requesting to load lib " << lib << " from package " << package
505 //// << " -> " << robotUnit->loadLibFromPackage(package, lib);
506 // }
507 // break;
508 // default:
509 // ARMARX_WARNING << "Load lib for given SelectLibsMode nyi";
510 // break;
511 // }
512  }
513 
514 }
armarx::NJointControllerClassesWidget::clearAll
void clearAll() override
Definition: NJointControllerClassesWidget.cpp:142
armarx::NJointControllerClassesWidgetEntry::creator
WidgetDescription::DescribedWidgetBase * creator
Definition: NJointControllerClassesWidget.h:133
armarx::NJointControllerClassesWidgetEntry::nameEdit
QLineEdit * nameEdit
Definition: NJointControllerClassesWidget.h:132
armarx::NJointControllerClassesWidget::nJointControllerClassAdded
virtual void nJointControllerClassAdded(std::string name)
Definition: NJointControllerClassesWidget.cpp:90
armarx::CMakePackageFinder::packageFound
bool packageFound() const
Returns whether or not this package was found with cmake.
Definition: CMakePackageFinder.cpp:485
armarx::NJointControllerClassesWidget::loadSettings
void loadSettings(QSettings *settings) override
Definition: NJointControllerClassesWidget.cpp:128
armarx::NJointControllerClassesWidgetEntry
Definition: NJointControllerClassesWidget.h:105
armarx::NJointControllerClassesWidget::doContentUpdate
void doContentUpdate() override
Definition: NJointControllerClassesWidget.cpp:148
armarx::Split
std::vector< std::string > Split(const std::string &source, const std::string &splitBy, bool trimElements=false, bool removeEmptyElements=false)
Definition: StringHelperTemplates.h:35
armarx::CMakePackageFinder
The CMakePackageFinder class provides an interface to the CMake Package finder capabilities.
Definition: CMakePackageFinder.h:53
armarx::NJointControllerClassesWidget::saveSettings
void saveSettings(QSettings *settings) override
Definition: NJointControllerClassesWidget.cpp:135
armarx::NJointControllerClassesWidget::addOneFromResetData
bool addOneFromResetData() override
Definition: NJointControllerClassesWidget.cpp:169
armarx::RobotUnitWidgetTemplateBase< Ui::NJointControllerClassesWidget >::ui
Ui::NJointControllerClassesWidget * ui
Definition: RobotUnitWidgetBase.h:117
StringHelpers.h
armarx::RobotUnitWidgetBase::doMetaCall
std::atomic_bool doMetaCall
Definition: RobotUnitWidgetBase.h:87
armarx::NJointControllerClassesWidgetEntry::parent
NJointControllerClassesWidget * parent
Definition: NJointControllerClassesWidget.h:134
armarx::RobotUnitWidgetBase::robotUnit
RobotUnitInterfacePrx robotUnit
Definition: RobotUnitWidgetBase.h:81
armarx::NJointControllerClassesWidget::getResetData
void getResetData() override
Definition: NJointControllerClassesWidget.cpp:157
data
uint8_t data[1]
Definition: EtherCATFrame.h:68
armarx::NJointControllerClassesWidget::getDefaultName
QString getDefaultName() const
Definition: NJointControllerClassesWidget.cpp:123
armarx::NJointControllerClassesWidgetEntry::robotUnit
RobotUnitInterfacePrx robotUnit
Definition: NJointControllerClassesWidget.h:130
armarx::NJointControllerClassesWidgetEntry::hasRemoteCreation
bool hasRemoteCreation()
Definition: NJointControllerClassesWidget.cpp:357
armarx::RobotUnitWidgetBase::mutex
std::recursive_timed_mutex mutex
Definition: RobotUnitWidgetBase.h:82
armarx::RobotUnitWidgetTemplateBase
Definition: RobotUnitWidgetBase.h:93
armarx::NJointControllerClassesWidget::~NJointControllerClassesWidget
~NJointControllerClassesWidget() override
Definition: NJointControllerClassesWidget.cpp:85
armarx::NJointControllerClassesWidgetEntry::classNameQStr
QString classNameQStr
Definition: NJointControllerClassesWidget.h:129
armarx::WidgetDescription::makeDescribedWidget
DescribedWidgetBase * makeDescribedWidget(const WidgetPtr &p, ValueChangedListenerInterface *listener)
Definition: WidgetDescription.cpp:742
armarx::NJointControllerClassesWidgetEntry::NJointControllerClassesWidgetEntry
NJointControllerClassesWidgetEntry(NJointControllerClassesWidget &parent, QTreeWidget &treeWidget, const NJointControllerClassDescription &desc, RobotUnitInterfacePrx robotUnit)
Definition: NJointControllerClassesWidget.cpp:289
CMakePackageFinder.h
armarx::NJointControllerClassesWidgetEntry::updateDefaultName
void updateDefaultName(const QString &oldName, const QString &newName)
Definition: NJointControllerClassesWidget.cpp:362
ARMARX_INFO
#define ARMARX_INFO
Definition: Logging.h:174
armarx::NJointControllerClassesWidget::NJointControllerClassesWidget
NJointControllerClassesWidget(QWidget *parent=0)
Definition: NJointControllerClassesWidget.cpp:36
armarx::NJointControllerClassesWidget
Definition: NJointControllerClassesWidget.h:51
IceInternal::ProxyHandle<::IceProxy::armarx::RobotUnitInterface >
armarx::WidgetDescription::DescribedWidgetBase::getVariants
virtual std::map< std::string, VariantBasePtr > getVariants()
Definition: WidgetDescription.h:51
armarx::NJointControllerClassesWidgetEntry::matchName
bool matchName(const QString &name)
Definition: NJointControllerClassesWidget.cpp:352
NJointControllerClassesWidget.h
Logging.h
armarx::NJointControllerClassesWidget::updateDefaultNameOnControllerCreated
virtual void updateDefaultNameOnControllerCreated(QString createdName, bool force=false)
Definition: NJointControllerClassesWidget.cpp:109
ARMARX_WARNING
#define ARMARX_WARNING
Definition: Logging.h:186
armarx::NJointControllerClassesWidgetEntry::className
std::string className
Definition: NJointControllerClassesWidget.h:128
armarx
This file offers overloads of toIce() and fromIce() functions for STL container types.
Definition: ArmarXTimeserver.cpp:28
armarx::NJointControllerClassesWidgetEntry::header
QTreeWidgetItem * header
Definition: NJointControllerClassesWidget.h:131
armarx::NJointControllerClassesWidgetEntry::setVisible
void setVisible(bool vis)
Definition: NJointControllerClassesWidget.cpp:370