├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── QtPropertyEditor.cpp ├── QtPropertyEditor.h ├── README.md ├── images ├── QtPropertyTableEditor.png └── QtPropertyTreeEditor.png └── test ├── CMakeLists.txt ├── test_QtPropertyEditor.cpp ├── test_QtPropertyEditor.h └── test_QtPropertyEditor.pro /.gitignore: -------------------------------------------------------------------------------- 1 | # CMake 2 | CMakeFiles/ 3 | CMakeScripts/ 4 | _deps/ 5 | *_autogen/ 6 | *.build/ 7 | CMakeCache.txt 8 | *.cmake 9 | 10 | # MacOSX 11 | .DS_Store 12 | *.xcodeproj/ 13 | *.build/ 14 | Debug/ 15 | Release/ 16 | build/ 17 | Info.plist 18 | project.pbxproj 19 | project.xcworkspace 20 | xcshareddata/ 21 | 22 | # C++ 23 | *.slo 24 | *.lo 25 | *.o 26 | *.a 27 | *.la 28 | *.lai 29 | *.so 30 | *.dll 31 | *.dylib 32 | 33 | # Qt-es 34 | .qmake.cache 35 | .qmake.stash 36 | *.pro.user 37 | *.pro.user.* 38 | *.qbs.user 39 | *.qbs.user.* 40 | *.moc 41 | moc_*.h 42 | moc_*.cpp 43 | qrc_*.cpp 44 | ui_*.h 45 | Makefile* 46 | *build-* 47 | *.mak 48 | 49 | # QtCreator 50 | *.autosave 51 | 52 | # QtCtreator Qml 53 | *.qmlproject.user 54 | *.qmlproject.user.* 55 | 56 | # QtCtreator CMake 57 | CMakeLists.txt.user 58 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Created by Marcel Paz Goldschen-Ohm 2 | 3 | # !!! Set env variable QT5_DIR to /lib/cmake/Qt5 4 | # For macx xcode project: cmake -G Xcode 5 | 6 | # cmake_minimum_required(VERSION 3.11) 7 | cmake_minimum_required(VERSION 3.21) 8 | 9 | set(PROJECT_NAME QtPropertyEditor) 10 | project(${PROJECT_NAME} LANGUAGES CXX) 11 | 12 | ## Qt5 # set(CMAKE_CXX_STANDARD 11) # This is equal to QMAKE_CXX_FLAGS += -std=c++0x 13 | ## Qt6 14 | set(CMAKE_CXX_STANDARD 17) 15 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 16 | 17 | set(CMAKE_INCLUDE_CURRENT_DIR ON) # Search in current dir. 18 | set(CMAKE_AUTOMOC ON) # Run moc automatically for Qt. 19 | set(CMAKE_AUTOUIC ON) # Run uic automatically for *.ui files. 20 | set(CMAKE_AUTORCC ON) # Run automatically for *.qrc files. 21 | 22 | if(APPLE AND EXISTS /usr/local/opt/qt5) 23 | # Homebrew installs Qt5 (up to at least 5.9.1) in 24 | # /usr/local/qt5, ensure it can be found by CMake since 25 | # it is not in the default /usr/local prefix. 26 | list(APPEND CMAKE_PREFIX_PATH "/usr/local/opt/qt5") 27 | endif() 28 | 29 | # Find required packages. 30 | ## Qt5 # find_package(Qt5 COMPONENTS Widgets REQUIRED) 31 | find_package(Qt6 COMPONENTS Widgets REQUIRED) # Qt6 32 | 33 | # Build project as a static library. 34 | add_library(${PROJECT_NAME} STATIC QtPropertyEditor.cpp QtPropertyEditor.h) 35 | 36 | ## Qt5 # qt5_use_modules(${PROJECT_NAME} Widgets) 37 | target_link_libraries(${PROJECT_NAME} Qt6::Widgets) 38 | 39 | target_link_libraries(${PROJECT_NAME} ${QT_LIBRARIES}) 40 | 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Marcel Goldschen-Ohm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QtPropertyEditor.cpp: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------- 2 | * Author: Marcel Paz Goldschen-Ohm 3 | * Email: marcel.goldschen@gmail.com 4 | * -------------------------------------------------------------------------------- */ 5 | 6 | #include "QtPropertyEditor.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | 27 | namespace QtPropertyEditor 28 | { 29 | static MetaTypeRegistration thisInstantiationRegistersQtPushButtonActionWrapperWithQt; 30 | 31 | QList getPropertyNames(QObject *object) 32 | { 33 | QList propertyNames = getMetaPropertyNames(*object->metaObject()); 34 | foreach(const QByteArray &dynamicPropertyName, object->dynamicPropertyNames()) { 35 | propertyNames << dynamicPropertyName; 36 | } 37 | return propertyNames; 38 | } 39 | 40 | QList getMetaPropertyNames(const QMetaObject &metaObject) 41 | { 42 | QList propertyNames; 43 | int numProperties = metaObject.propertyCount(); 44 | for(int i = 0; i < numProperties; ++i) { 45 | const QMetaProperty metaProperty = metaObject.property(i); 46 | propertyNames << QByteArray(metaProperty.name()); 47 | } 48 | return propertyNames; 49 | } 50 | 51 | QList getNoninheritedPropertyNames(QObject *object) 52 | { 53 | QList propertyNames = getPropertyNames(object); 54 | QList superPropertyNames = getMetaPropertyNames(*object->metaObject()->superClass()); 55 | foreach(const QByteArray &superPropertyName, superPropertyNames) { 56 | propertyNames.removeOne(superPropertyName); 57 | } 58 | return propertyNames; 59 | } 60 | 61 | QObject* descendant(QObject *object, const QByteArray &pathToDescendantObject) 62 | { 63 | // Get descendent object specified by "path.to.descendant", where "path", "to" and "descendant" 64 | // are the object names of objects with the parent->child relationship object->path->to->descendant. 65 | if(!object || pathToDescendantObject.isEmpty()) 66 | return 0; 67 | if(pathToDescendantObject.contains('.')) { 68 | QList descendantObjectNames = pathToDescendantObject.split('.'); 69 | foreach(QByteArray name, descendantObjectNames) { 70 | object = object->findChild(QString(name)); 71 | if(!object) 72 | return 0; // Invalid path to descendant object. 73 | } 74 | return object; 75 | } 76 | return object->findChild(QString(pathToDescendantObject)); 77 | } 78 | 79 | QSize getTableSize(const QTableView *table) 80 | { 81 | int w = table->verticalHeader()->width() + 4; // +4 seems to be needed 82 | int h = table->horizontalHeader()->height() + 4; 83 | for(int i = 0; i < table->model()->columnCount(); i++) 84 | w += table->columnWidth(i); 85 | for(int i = 0; i < table->model()->rowCount(); i++) 86 | h += table->rowHeight(i); 87 | return QSize(w, h); 88 | } 89 | 90 | void QtAbstractPropertyModel::setProperties(const QString &str) 91 | { 92 | // str = "name0: header0, name1, name2, name3: header3 ..." 93 | propertyNames.clear(); 94 | propertyHeaders.clear(); 95 | // QStringList fields = str.split(",", QString::SkipEmptyParts); 96 | QStringList fields = str.split(",", Qt::SkipEmptyParts); 97 | foreach(const QString &field, fields) { 98 | if(!field.trimmed().isEmpty()) 99 | addProperty(field); 100 | } 101 | } 102 | 103 | void QtAbstractPropertyModel::addProperty(const QString &str) 104 | { 105 | // "name" OR "name: header" 106 | if(str.contains(":")) { 107 | int pos = str.indexOf(":"); 108 | QByteArray propertyName = str.left(pos).trimmed().toUtf8(); 109 | QString propertyHeader = str.mid(pos+1).trimmed(); 110 | propertyNames.push_back(propertyName); 111 | propertyHeaders[propertyName] = propertyHeader; 112 | } else { 113 | QByteArray propertyName = str.trimmed().toUtf8(); 114 | propertyNames.push_back(propertyName); 115 | } 116 | } 117 | 118 | const QMetaProperty QtAbstractPropertyModel::metaPropertyAtIndex(const QModelIndex &index) const 119 | { 120 | QObject *object = objectAtIndex(index); 121 | if(!object) 122 | return QMetaProperty(); 123 | QByteArray propertyName = propertyNameAtIndex(index); 124 | if(propertyName.isEmpty()) 125 | return QMetaProperty(); 126 | // Return metaObject with same name. 127 | const QMetaObject *metaObject = object->metaObject(); 128 | int numProperties = metaObject->propertyCount(); 129 | for(int i = 0; i < numProperties; ++i) { 130 | const QMetaProperty metaProperty = metaObject->property(i); 131 | if(QByteArray(metaProperty.name()) == propertyName) 132 | return metaProperty; 133 | } 134 | return QMetaProperty(); 135 | } 136 | 137 | QVariant QtAbstractPropertyModel::data(const QModelIndex &index, int role) const 138 | { 139 | if(!index.isValid()) 140 | return QVariant(); 141 | if(role == Qt::DisplayRole || role == Qt::EditRole) { 142 | QObject *object = objectAtIndex(index); 143 | if(!object) 144 | return QVariant(); 145 | QByteArray propertyName = propertyNameAtIndex(index); 146 | if(propertyName.isEmpty()) 147 | return QVariant(); 148 | return object->property(propertyName.constData()); 149 | } 150 | return QVariant(); 151 | } 152 | 153 | bool QtAbstractPropertyModel::setData(const QModelIndex &index, const QVariant &value, int role) 154 | { 155 | if(!index.isValid()) 156 | return false; 157 | if(role == Qt::EditRole) { 158 | QObject *object = objectAtIndex(index); 159 | if(!object) 160 | return false; 161 | QByteArray propertyName = propertyNameAtIndex(index); 162 | if(propertyName.isEmpty()) 163 | return false; 164 | bool result = object->setProperty(propertyName.constData(), value); 165 | // Result will be FALSE for dynamic properties, which causes the tree view to lag. 166 | // So make sure we still return TRUE in this case. 167 | if(!result && object->dynamicPropertyNames().contains(propertyName)) 168 | return true; 169 | return result; 170 | } 171 | return false; 172 | } 173 | 174 | Qt::ItemFlags QtAbstractPropertyModel::flags(const QModelIndex &index) const 175 | { 176 | Qt::ItemFlags flags = QAbstractItemModel::flags(index); 177 | if(!index.isValid()) 178 | return flags; 179 | QObject *object = objectAtIndex(index); 180 | if(!object) 181 | return flags; 182 | flags |= Qt::ItemIsEnabled; 183 | flags |= Qt::ItemIsSelectable; 184 | QByteArray propertyName = propertyNameAtIndex(index); 185 | const QMetaProperty metaProperty = metaPropertyAtIndex(index); 186 | if(metaProperty.isWritable() || object->dynamicPropertyNames().contains(propertyName)) 187 | flags |= Qt::ItemIsEditable; 188 | return flags; 189 | } 190 | 191 | void QtPropertyTreeModel::Node::setObject(QObject *object, int maxChildDepth, const QList &propertyNames) 192 | { 193 | this->object = object; 194 | propertyName.clear(); 195 | qDeleteAll(children); 196 | children.clear(); 197 | if(!object) return; 198 | 199 | // Compiled properties (but exclude objectName as this is displayed for the object node itself). 200 | const QMetaObject *metaObject = object->metaObject(); 201 | int numProperties = metaObject->propertyCount(); 202 | for(int i = 0; i < numProperties; ++i) { 203 | const QMetaProperty metaProperty = metaObject->property(i); 204 | QByteArray propertyName = QByteArray(metaProperty.name()); 205 | if(propertyNames.isEmpty() || propertyNames.contains(propertyName)) { 206 | Node *node = new Node(this); 207 | node->propertyName = propertyName; 208 | children.append(node); 209 | } 210 | } 211 | // Dynamic properties. 212 | QList dynamicPropertyNames = object->dynamicPropertyNames(); 213 | foreach(const QByteArray &propertyName, dynamicPropertyNames) { 214 | if(propertyNames.isEmpty() || propertyNames.contains(propertyName)) { 215 | Node *node = new Node(this); 216 | node->propertyName = propertyName; 217 | children.append(node); 218 | } 219 | } 220 | // Child objects. 221 | if(maxChildDepth > 0 || maxChildDepth == -1) { 222 | if(maxChildDepth > 0) 223 | --maxChildDepth; 224 | QMap childMap; 225 | foreach(QObject *child, object->children()) { 226 | childMap[QByteArray(child->metaObject()->className())].append(child); 227 | } 228 | for(auto it = childMap.begin(); it != childMap.end(); ++it) { 229 | foreach(QObject *child, it.value()) { 230 | Node *node = new Node(this); 231 | node->setObject(child, maxChildDepth, propertyNames); 232 | children.append(node); 233 | } 234 | } 235 | } 236 | } 237 | 238 | QtPropertyTreeModel::Node* QtPropertyTreeModel::nodeAtIndex(const QModelIndex &index) const 239 | { 240 | try { 241 | return static_cast(index.internalPointer()); 242 | } catch(...) { 243 | return NULL; 244 | } 245 | } 246 | 247 | QObject* QtPropertyTreeModel::objectAtIndex(const QModelIndex &index) const 248 | { 249 | // If node is an object, return the node's object. 250 | // Else if node is a property, return the parent node's object. 251 | Node *node = nodeAtIndex(index); 252 | if(!node) return NULL; 253 | if(node->object) return node->object; 254 | if(node->parent) return node->parent->object; 255 | return NULL; 256 | } 257 | 258 | QByteArray QtPropertyTreeModel::propertyNameAtIndex(const QModelIndex &index) const 259 | { 260 | // If node is a property, return the node's property name. 261 | // Else if node is an object, return "objectName". 262 | Node *node = nodeAtIndex(index); 263 | if(!node) return QByteArray(); 264 | if(!node->propertyName.isEmpty()) return node->propertyName; 265 | return QByteArray(); 266 | } 267 | 268 | QModelIndex QtPropertyTreeModel::index(int row, int column, const QModelIndex &parent) const 269 | { 270 | // Return a model index whose internal pointer references the appropriate tree node. 271 | if(column < 0 || column >= 2 || !hasIndex(row, column, parent)) 272 | return QModelIndex(); 273 | const Node *parentNode = parent.isValid() ? nodeAtIndex(parent) : &_root; 274 | if(!parentNode || row < 0 || row >= parentNode->children.size()) 275 | return QModelIndex(); 276 | Node *node = parentNode->children.at(row); 277 | return node ? createIndex(row, column, node) : QModelIndex(); 278 | } 279 | 280 | QModelIndex QtPropertyTreeModel::parent(const QModelIndex &index) const 281 | { 282 | // Return a model index for parent node (column must be 0). 283 | if(!index.isValid()) 284 | return QModelIndex(); 285 | Node *node = nodeAtIndex(index); 286 | if(!node) 287 | return QModelIndex(); 288 | Node *parentNode = node->parent; 289 | if(!parentNode || parentNode == &_root) 290 | return QModelIndex(); 291 | int row = 0; 292 | Node *grandparentNode = parentNode->parent; 293 | if(grandparentNode) 294 | row = grandparentNode->children.indexOf(parentNode); 295 | return createIndex(row, 0, parentNode); 296 | } 297 | 298 | int QtPropertyTreeModel::rowCount(const QModelIndex &parent) const 299 | { 300 | // Return number of child nodes. 301 | const Node *parentNode = parent.isValid() ? nodeAtIndex(parent) : &_root; 302 | return parentNode ? parentNode->children.size() : 0; 303 | } 304 | 305 | int QtPropertyTreeModel::columnCount(const QModelIndex &parent) const 306 | { 307 | // Return 2 for name/value columns. 308 | const Node *parentNode = parent.isValid() ? nodeAtIndex(parent) : &_root; 309 | return (parentNode ? 2 : 0); 310 | } 311 | 312 | QVariant QtPropertyTreeModel::data(const QModelIndex &index, int role) const 313 | { 314 | if(!index.isValid()) 315 | return QVariant(); 316 | if(role == Qt::DisplayRole || role == Qt::EditRole) { 317 | QObject *object = objectAtIndex(index); 318 | if(!object) 319 | return QVariant(); 320 | QByteArray propertyName = propertyNameAtIndex(index); 321 | if(index.column() == 0) { 322 | // Object's class name or else the property name. 323 | if(propertyName.isEmpty()) 324 | return QVariant(object->metaObject()->className()); 325 | else if(propertyHeaders.contains(propertyName)) 326 | return QVariant(propertyHeaders[propertyName]); 327 | else 328 | return QVariant(propertyName); 329 | } else if(index.column() == 1) { 330 | // Object's objectName or else the property value. 331 | if(propertyName.isEmpty()) 332 | return QVariant(object->objectName()); 333 | else 334 | return object->property(propertyName.constData()); 335 | } 336 | } 337 | return QVariant(); 338 | } 339 | 340 | bool QtPropertyTreeModel::setData(const QModelIndex &index, const QVariant &value, int role) 341 | { 342 | if(!index.isValid()) 343 | return false; 344 | if(role == Qt::EditRole) { 345 | QObject *object = objectAtIndex(index); 346 | if(!object) 347 | return false; 348 | QByteArray propertyName = propertyNameAtIndex(index); 349 | if(index.column() == 0) { 350 | // Object class name or property name. 351 | return false; 352 | } else if(index.column() == 1) { 353 | // Object's objectName or else the property value. 354 | if(propertyName.isEmpty()) { 355 | object->setObjectName(value.toString()); 356 | return true; 357 | } else { 358 | bool result = object->setProperty(propertyName.constData(), value); 359 | // Result will be FALSE for dynamic properties, which causes the tree view to lag. 360 | // So make sure we still return TRUE in this case. 361 | if(!result && object->dynamicPropertyNames().contains(propertyName)) 362 | return true; 363 | return result; 364 | } 365 | } 366 | } 367 | return false; 368 | } 369 | 370 | Qt::ItemFlags QtPropertyTreeModel::flags(const QModelIndex &index) const 371 | { 372 | Qt::ItemFlags flags = QAbstractItemModel::flags(index); 373 | if(!index.isValid()) 374 | return flags; 375 | QObject *object = objectAtIndex(index); 376 | if(!object) 377 | return flags; 378 | flags |= Qt::ItemIsEnabled; 379 | flags |= Qt::ItemIsSelectable; 380 | if(index.column() == 1) { 381 | QByteArray propertyName = propertyNameAtIndex(index); 382 | const QMetaProperty metaProperty = metaPropertyAtIndex(index); 383 | if(metaProperty.isWritable() || object->dynamicPropertyNames().contains(propertyName) || objectAtIndex(index)) 384 | flags |= Qt::ItemIsEditable; 385 | } 386 | return flags; 387 | } 388 | 389 | QVariant QtPropertyTreeModel::headerData(int section, Qt::Orientation orientation, int role) const 390 | { 391 | if(role == Qt::DisplayRole) { 392 | if(orientation == Qt::Horizontal) { 393 | if(section == 0) 394 | return QVariant("Name"); 395 | else if(section == 1) 396 | return QVariant("Value"); 397 | } 398 | } 399 | return QVariant(); 400 | } 401 | 402 | QObject* QtPropertyTableModel::objectAtIndex(const QModelIndex &index) const 403 | { 404 | if(_objects.size() <= index.row()) 405 | return 0; 406 | QObject *object = _objects.at(index.row()); 407 | // If property names are specified, check if name at column is a path to a child object property. 408 | if(!propertyNames.isEmpty()) { 409 | if(propertyNames.size() > index.column()) { 410 | QByteArray propertyName = propertyNames.at(index.column()); 411 | if(propertyName.contains('.')) { 412 | int pos = propertyName.lastIndexOf('.'); 413 | return descendant(object, propertyName.left(pos)); 414 | } 415 | } 416 | } 417 | return object; 418 | } 419 | 420 | QByteArray QtPropertyTableModel::propertyNameAtIndex(const QModelIndex &index) const 421 | { 422 | // If property names are specified, return the name at column. 423 | if(!propertyNames.isEmpty()) { 424 | if(propertyNames.size() > index.column()) { 425 | QByteArray propertyName = propertyNames.at(index.column()); 426 | if(propertyName.contains('.')) { 427 | int pos = propertyName.lastIndexOf('.'); 428 | return propertyName.mid(pos + 1); 429 | } 430 | return propertyName; 431 | } 432 | return QByteArray(); 433 | } 434 | // If property names are NOT specified, return the metaObject's property name at column. 435 | QObject *object = objectAtIndex(index); 436 | if(!object) 437 | return QByteArray(); 438 | const QMetaObject *metaObject = object->metaObject(); 439 | int numProperties = metaObject->propertyCount(); 440 | if(numProperties > index.column()) 441 | return QByteArray(metaObject->property(index.column()).name()); 442 | // If column is greater than the number of metaObject properties, check for dynamic properties. 443 | const QList &dynamicPropertyNames = object->dynamicPropertyNames(); 444 | if(numProperties + dynamicPropertyNames.size() > index.column()) 445 | return dynamicPropertyNames.at(index.column() - numProperties); 446 | return QByteArray(); 447 | } 448 | 449 | QModelIndex QtPropertyTableModel::index(int row, int column, const QModelIndex &/* parent */) const 450 | { 451 | return createIndex(row, column); 452 | } 453 | 454 | QModelIndex QtPropertyTableModel::parent(const QModelIndex &/* index */) const 455 | { 456 | return QModelIndex(); 457 | } 458 | 459 | int QtPropertyTableModel::rowCount(const QModelIndex &/* parent */) const 460 | { 461 | return _objects.size(); 462 | } 463 | 464 | int QtPropertyTableModel::columnCount(const QModelIndex &/* parent */) const 465 | { 466 | // Number of properties. 467 | if(!propertyNames.isEmpty()) 468 | return propertyNames.size(); 469 | if(_objects.isEmpty()) 470 | return 0; 471 | QObject *object = _objects.at(0); 472 | const QMetaObject *metaObject = object->metaObject(); 473 | return metaObject->propertyCount() + object->dynamicPropertyNames().size(); 474 | } 475 | 476 | QVariant QtPropertyTableModel::headerData(int section, Qt::Orientation orientation, int role) const 477 | { 478 | if(role == Qt::DisplayRole) { 479 | if(orientation == Qt::Vertical) { 480 | return QVariant(section); 481 | } else if(orientation == Qt::Horizontal) { 482 | QByteArray propertyName = propertyNameAtIndex(createIndex(0, section)); 483 | QByteArray childPath; 484 | if(propertyNames.size() > section) { 485 | QByteArray pathToPropertyName = propertyNames.at(section); 486 | if(pathToPropertyName.contains('.')) { 487 | int pos = pathToPropertyName.lastIndexOf('.'); 488 | childPath = pathToPropertyName.left(pos + 1); 489 | } 490 | } 491 | if(propertyHeaders.contains(propertyName)) 492 | return QVariant(childPath + propertyHeaders.value(propertyName)); 493 | return QVariant(childPath + propertyName); 494 | } 495 | } 496 | return QVariant(); 497 | } 498 | 499 | bool QtPropertyTableModel::insertRows(int row, int count, const QModelIndex &parent) 500 | { 501 | // Only valid if we have an object creator method. 502 | if(!_objectCreator) 503 | return false; 504 | bool columnCountWillAlsoChange = _objects.isEmpty() && propertyNames.isEmpty(); 505 | beginInsertRows(parent, row, row + count - 1); 506 | for(int i = row; i < row + count; ++i) { 507 | QObject *object = _objectCreator(); 508 | _objects.insert(i, object); 509 | } 510 | endInsertRows(); 511 | if(row + count < _objects.size()) 512 | reorderChildObjectsToMatchRowOrder(row + count); 513 | if(columnCountWillAlsoChange) { 514 | beginResetModel(); 515 | endResetModel(); 516 | } 517 | emit rowCountChanged(); 518 | return true; 519 | } 520 | 521 | bool QtPropertyTableModel::removeRows(int row, int count, const QModelIndex &parent) 522 | { 523 | beginRemoveRows(parent, row, row + count - 1); 524 | for(int i = row; i < row + count; ++i) 525 | delete _objects.at(i); 526 | QObjectList::iterator begin = _objects.begin() + row; 527 | _objects.erase(begin, begin + count); 528 | endRemoveRows(); 529 | emit rowCountChanged(); 530 | return true; 531 | } 532 | 533 | bool QtPropertyTableModel::moveRows(const QModelIndex &/*sourceParent*/, int sourceRow, int count, const QModelIndex &/*destinationParent*/, int destinationRow) 534 | { 535 | beginResetModel(); 536 | QObjectList objectsToMove; 537 | for(int i = sourceRow; i < sourceRow + count; ++i) 538 | objectsToMove.append(_objects.takeAt(sourceRow)); 539 | for(int i = 0; i < objectsToMove.size(); ++i) { 540 | if(destinationRow + i >= _objects.size()) 541 | _objects.append(objectsToMove.at(i)); 542 | else 543 | _objects.insert(destinationRow + i, objectsToMove.at(i)); 544 | } 545 | endResetModel(); 546 | reorderChildObjectsToMatchRowOrder(sourceRow <= destinationRow ? sourceRow : destinationRow); 547 | emit rowOrderChanged(); 548 | return true; 549 | } 550 | 551 | void QtPropertyTableModel::reorderChildObjectsToMatchRowOrder(int firstRow) 552 | { 553 | for(int i = firstRow; i < rowCount(); ++i) { 554 | QObject *object = objectAtIndex(createIndex(i, 0)); 555 | if(object) { 556 | QObject *parent = object->parent(); 557 | if(parent) { 558 | object->setParent(NULL); 559 | object->setParent(parent); 560 | } 561 | } 562 | } 563 | } 564 | 565 | QWidget* QtPropertyDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const 566 | { 567 | QVariant value = index.data(Qt::DisplayRole); 568 | if(value.isValid()) { 569 | if(value.typeId() == QVariant::Bool) { 570 | // We want a check box, but instead of creating an editor widget we'll just directly 571 | // draw the check box in paint() and handle mouse clicks in editorEvent(). 572 | // Here, we'll just return NULL to make sure that no editor is created when this cell is double clicked. 573 | return NULL; 574 | } else if(value.typeId() == QVariant::Double) { 575 | // Return a QLineEdit to enter double values with arbitrary precision and scientific notation. 576 | QLineEdit *editor = new QLineEdit(parent); 577 | editor->setText(value.toString()); 578 | return editor; 579 | } else if(value.typeId() == QVariant::Int) { 580 | // We don't need to do anything special for an integer, we'll just use the default QSpinBox. 581 | // However, we do need to check if it is an enum. If so, we'll use a QComboBox editor. 582 | const QtAbstractPropertyModel *propertyModel = qobject_cast(index.model()); 583 | if(propertyModel) { 584 | const QMetaProperty metaProperty = propertyModel->metaPropertyAtIndex(index); 585 | if(metaProperty.isValid() && metaProperty.isEnumType()) { 586 | const QMetaEnum metaEnum = metaProperty.enumerator(); 587 | int numKeys = metaEnum.keyCount(); 588 | if(numKeys > 0) { 589 | QComboBox *editor = new QComboBox(parent); 590 | for(int j = 0; j < numKeys; ++j) { 591 | QByteArray key = QByteArray(metaEnum.key(j)); 592 | editor->addItem(QString(key)); 593 | } 594 | QByteArray currentKey = QByteArray(metaEnum.valueToKey(value.toInt())); 595 | editor->setCurrentText(QString(currentKey)); 596 | return editor; 597 | } 598 | } 599 | } 600 | } else if(value.typeId() == QVariant::Size || 601 | value.typeId() == QVariant::SizeF || 602 | value.typeId() == QVariant::Point || 603 | value.typeId() == QVariant::PointF || 604 | value.typeId() == QVariant::Rect || 605 | value.typeId() == QVariant::RectF) { 606 | // Return a QLineEdit. Parsing will be done in displayText() and setEditorData(). 607 | QLineEdit *editor = new QLineEdit(parent); 608 | editor->setText(displayText(value, QLocale())); 609 | return editor; 610 | } else if(value.typeId() == QVariant::UserType) { 611 | if(value.canConvert()) { 612 | // We want a push button, but instead of creating an editor widget we'll just directly 613 | // draw the button in paint() and handle mouse clicks in editorEvent(). 614 | // Here, we'll just return NULL to make sure that no editor is created when this cell is double clicked. 615 | return NULL; 616 | } 617 | } 618 | } 619 | return QStyledItemDelegate::createEditor(parent, option, index); 620 | } 621 | 622 | void QtPropertyDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const 623 | { 624 | QStyledItemDelegate::setEditorData(editor, index); 625 | } 626 | 627 | void QtPropertyDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const 628 | { 629 | QVariant value = index.data(Qt::DisplayRole); 630 | if(value.isValid()) { 631 | if(value.typeId() == QVariant::Double) { 632 | // Set model's double value data to numeric representation in QLineEdit editor. 633 | // Conversion from text to number handled by QVariant. 634 | QLineEdit *lineEditor = qobject_cast(editor); 635 | if(lineEditor) { 636 | QVariant value = QVariant(lineEditor->text()); 637 | bool ok; 638 | double dval = value.toDouble(&ok); 639 | if(ok) 640 | model->setData(index, QVariant(dval), Qt::EditRole); 641 | return; 642 | } 643 | } else if(value.typeId() == QVariant::Int) { 644 | // We don't need to do anything special for an integer. 645 | // However, if it's an enum we'll set the data based on the QComboBox editor. 646 | QComboBox *comboBoxEditor = qobject_cast(editor); 647 | if(comboBoxEditor) { 648 | QString selectedKey = comboBoxEditor->currentText(); 649 | const QtAbstractPropertyModel *propertyModel = qobject_cast(model); 650 | if(propertyModel) { 651 | const QMetaProperty metaProperty = propertyModel->metaPropertyAtIndex(index); 652 | if(metaProperty.isValid() && metaProperty.isEnumType()) { 653 | const QMetaEnum metaEnum = metaProperty.enumerator(); 654 | bool ok; 655 | int selectedValue = metaEnum.keyToValue(selectedKey.toLatin1().constData(), &ok); 656 | if(ok) 657 | model->setData(index, QVariant(selectedValue), Qt::EditRole); 658 | return; 659 | } 660 | } 661 | // If we got here, we have a QComboBox editor but the property at index is not an enum. 662 | } 663 | } else if(value.typeId() == QVariant::Size) { 664 | QLineEdit *lineEditor = qobject_cast(editor); 665 | if(lineEditor) { 666 | // Parse formats: (w x h) or (w,h) or (w h) <== () are optional 667 | QRegularExpression regex("\\s*\\(?\\s*(\\d+)\\s*[x,\\s]\\s*(\\d+)\\s*\\)?\\s*"); 668 | QRegularExpressionMatch match = regex.match(lineEditor->text().trimmed()); 669 | if(match.hasMatch() && match.capturedTexts().size() == 3) { 670 | bool wok, hok; 671 | int w = match.captured(1).toInt(&wok); 672 | int h = match.captured(2).toInt(&hok); 673 | if(wok && hok) 674 | model->setData(index, QVariant(QSize(w, h)), Qt::EditRole); 675 | } 676 | } 677 | } else if(value.typeId() == QVariant::SizeF) { 678 | QLineEdit *lineEditor = qobject_cast(editor); 679 | if(lineEditor) { 680 | // Parse formats: (w x h) or (w,h) or (w h) <== () are optional 681 | QRegularExpression regex("\\s*\\(?\\s*([0-9\\+\\-\\.eE]+)\\s*[x,\\s]\\s*([0-9\\+\\-\\.eE]+)\\s*\\)?\\s*"); 682 | QRegularExpressionMatch match = regex.match(lineEditor->text().trimmed()); 683 | if(match.hasMatch() && match.capturedTexts().size() == 3) { 684 | bool wok, hok; 685 | double w = match.captured(1).toDouble(&wok); 686 | double h = match.captured(2).toDouble(&hok); 687 | if(wok && hok) 688 | model->setData(index, QVariant(QSizeF(w, h)), Qt::EditRole); 689 | } 690 | } 691 | } else if(value.typeId() == QVariant::Point) { 692 | QLineEdit *lineEditor = qobject_cast(editor); 693 | if(lineEditor) { 694 | // Parse formats: (x,y) or (x y) <== () are optional 695 | QRegularExpression regex("\\s*\\(?\\s*(\\d+)\\s*[x,\\s]\\s*(\\d+)\\s*\\)?\\s*"); 696 | QRegularExpressionMatch match = regex.match(lineEditor->text().trimmed()); 697 | if(match.hasMatch() && match.capturedTexts().size() == 3) { 698 | bool xok, yok; 699 | int x = match.captured(1).toInt(&xok); 700 | int y = match.captured(2).toInt(&yok); 701 | if(xok && yok) 702 | model->setData(index, QVariant(QPoint(x, y)), Qt::EditRole); 703 | } 704 | } 705 | } else if(value.typeId() == QVariant::PointF) { 706 | QLineEdit *lineEditor = qobject_cast(editor); 707 | if(lineEditor) { 708 | // Parse formats: (x,y) or (x y) <== () are optional 709 | QRegularExpression regex("\\s*\\(?\\s*([0-9\\+\\-\\.eE]+)\\s*[x,\\s]\\s*([0-9\\+\\-\\.eE]+)\\s*\\)?\\s*"); 710 | QRegularExpressionMatch match = regex.match(lineEditor->text().trimmed()); 711 | if(match.hasMatch() && match.capturedTexts().size() == 3) { 712 | bool xok, yok; 713 | double x = match.captured(1).toDouble(&xok); 714 | double y = match.captured(2).toDouble(&yok); 715 | if(xok && yok) 716 | model->setData(index, QVariant(QPointF(x, y)), Qt::EditRole); 717 | } 718 | } 719 | } else if(value.typeId() == QVariant::Rect) { 720 | QLineEdit *lineEditor = qobject_cast(editor); 721 | if(lineEditor) { 722 | // Parse formats: [Point,Size] or [Point Size] <== [] are optional 723 | // Point formats: (x,y) or (x y) <== () are optional 724 | // Size formats: (w x h) or (w,h) or (w h) <== () are optional 725 | QRegularExpression regex("\\s*\\[?" 726 | "\\s*\\(?\\s*(\\d+)\\s*[,\\s]\\s*(\\d+)\\s*\\)?\\s*" 727 | "[,\\s]" 728 | "\\s*\\(?\\s*(\\d+)\\s*[x,\\s]\\s*(\\d+)\\s*\\)?\\s*" 729 | "\\]?\\s*"); 730 | QRegularExpressionMatch match = regex.match(lineEditor->text().trimmed()); 731 | if(match.hasMatch() && match.capturedTexts().size() == 5) { 732 | bool xok, yok, wok, hok; 733 | int x = match.captured(1).toInt(&xok); 734 | int y = match.captured(2).toInt(&yok); 735 | int w = match.captured(3).toInt(&wok); 736 | int h = match.captured(4).toInt(&hok); 737 | if(xok && yok && wok && hok) 738 | model->setData(index, QVariant(QRect(x, y, w, h)), Qt::EditRole); 739 | } 740 | } 741 | } else if(value.typeId() == QVariant::RectF) { 742 | QLineEdit *lineEditor = qobject_cast(editor); 743 | if(lineEditor) { 744 | // Parse formats: [Point,Size] or [Point Size] <== [] are optional 745 | // Point formats: (x,y) or (x y) <== () are optional 746 | // Size formats: (w x h) or (w,h) or (w h) <== () are optional 747 | QRegularExpression regex("\\s*\\[?" 748 | "\\s*\\(?\\s*([0-9\\+\\-\\.eE]+)\\s*[,\\s]\\s*([0-9\\+\\-\\.eE]+)\\s*\\)?\\s*" 749 | "[,\\s]" 750 | "\\s*\\(?\\s*([0-9\\+\\-\\.eE]+)\\s*[x,\\s]\\s*([0-9\\+\\-\\.eE]+)\\s*\\)?\\s*" 751 | "\\]?\\s*"); 752 | QRegularExpressionMatch match = regex.match(lineEditor->text().trimmed()); 753 | if(match.hasMatch() && match.capturedTexts().size() == 5) { 754 | bool xok, yok, wok, hok; 755 | double x = match.captured(1).toDouble(&xok); 756 | double y = match.captured(2).toDouble(&yok); 757 | double w = match.captured(3).toDouble(&wok); 758 | double h = match.captured(4).toDouble(&hok); 759 | if(xok && yok && wok && hok) 760 | model->setData(index, QVariant(QRectF(x, y, w, h)), Qt::EditRole); 761 | } 762 | } 763 | // } else if(value.type() == QVariant::Color) { 764 | // QLineEdit *lineEditor = qobject_cast(editor); 765 | // if(lineEditor) { 766 | // // Parse formats: (r,g,b) or (r g b) or (r,g,b,a) or (r g b a) <== () are optional 767 | // QRegularExpression regex("\\s*\\(?" 768 | // "\\s*(\\d+)\\s*" 769 | // "[,\\s]\\s*(\\d+)\\s*" 770 | // "[,\\s]\\s*(\\d+)\\s*" 771 | // "([,\\s]\\s*(\\d+)\\s*)?" 772 | // "\\)?\\s*"); 773 | // QRegularExpressionMatch match = regex.match(lineEditor->text().trimmed()); 774 | // if(match.hasMatch() && (match.capturedTexts().size() == 4 || match.capturedTexts().size() == 5)) { 775 | // bool rok, gok, bok, aok; 776 | // int r = match.captured(1).toInt(&rok); 777 | // int g = match.captured(2).toInt(&gok); 778 | // int b = match.captured(3).toInt(&bok); 779 | // if(match.capturedTexts().size() == 4) { 780 | // if(rok && gok && bok) 781 | // model->setData(index, QColor(r, g, b), Qt::EditRole); 782 | // } else if(match.capturedTexts().size() == 5) { 783 | // int a = match.captured(4).toInt(&aok); 784 | // if(rok && gok && bok && aok) 785 | // model->setData(index, QColor(r, g, b, a), Qt::EditRole); 786 | // } 787 | // } 788 | // } 789 | } 790 | } 791 | QStyledItemDelegate::setModelData(editor, model, index); 792 | } 793 | 794 | QString QtPropertyDelegate::displayText(const QVariant &value, const QLocale &locale) const 795 | { 796 | if(value.isValid()) { 797 | if(value.typeId() == QVariant::Size) { 798 | // w x h 799 | QSize size = value.toSize(); 800 | return QString::number(size.width()) + QString(" x ") + QString::number(size.height()); 801 | } else if(value.typeId() == QVariant::SizeF) { 802 | // w x h 803 | QSizeF size = value.toSizeF(); 804 | return QString::number(size.width()) + QString(" x ") + QString::number(size.height()); 805 | } else if(value.typeId() == QVariant::Point) { 806 | // (x, y) 807 | QPoint point = value.toPoint(); 808 | return QString("(") 809 | + QString::number(point.x()) + QString(", ") + QString::number(point.y()) 810 | + QString(")"); 811 | } else if(value.typeId() == QVariant::PointF) { 812 | // (x, y) 813 | QPointF point = value.toPointF(); 814 | return QString("(") 815 | + QString::number(point.x()) + QString(", ") + QString::number(point.y()) 816 | + QString(")"); 817 | } else if(value.typeId() == QVariant::Rect) { 818 | // [(x, y), w x h] 819 | QRect rect = value.toRect(); 820 | return QString("[(") 821 | + QString::number(rect.x()) + QString(", ") + QString::number(rect.y()) 822 | + QString("), ") 823 | + QString::number(rect.width()) + QString(" x ") + QString::number(rect.height()) 824 | + QString("]"); 825 | } else if(value.typeId() == QVariant::RectF) { 826 | // [(x, y), w x h] 827 | QRectF rect = value.toRectF(); 828 | return QString("[(") 829 | + QString::number(rect.x()) + QString(", ") + QString::number(rect.y()) 830 | + QString("), ") 831 | + QString::number(rect.width()) + QString(" x ") + QString::number(rect.height()) 832 | + QString("]"); 833 | // } else if(value.type() == QVariant::Color) { 834 | // // (r, g, b, a) 835 | // QColor color = value.value(); 836 | // return QString("(") 837 | // + QString::number(color.red()) + QString(", ") + QString::number(color.green()) + QString(", ") 838 | // + QString::number(color.blue()) + QString(", ") + QString::number(color.alpha()) 839 | // + QString(")"); 840 | } 841 | } 842 | return QStyledItemDelegate::displayText(value, locale); 843 | } 844 | 845 | void QtPropertyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const 846 | { 847 | QVariant value = index.data(Qt::DisplayRole); 848 | if(value.isValid()) { 849 | if(value.typeId() == QVariant::Bool) { 850 | bool checked = value.toBool(); 851 | QStyleOptionButton buttonOption; 852 | buttonOption.state |= QStyle::State_Active; // Required! 853 | buttonOption.state |= ((index.flags() & Qt::ItemIsEditable) ? QStyle::State_Enabled : QStyle::State_ReadOnly); 854 | buttonOption.state |= (checked ? QStyle::State_On : QStyle::State_Off); 855 | QRect checkBoxRect = QApplication::style()->subElementRect(QStyle::SE_CheckBoxIndicator, &buttonOption); // Only used to get size of native checkbox widget. 856 | buttonOption.rect = QStyle::alignedRect(option.direction, Qt::AlignLeft, checkBoxRect.size(), option.rect); // Our checkbox rect. 857 | QApplication::style()->drawControl(QStyle::CE_CheckBox, &buttonOption, painter); 858 | return; 859 | } else if(value.typeId() == QVariant::Int) { 860 | // We don't need to do anything special for an integer. 861 | // However, if it's an enum want to render the key name instead of the value. 862 | // This cannot be done in displayText() because we need the model index to get the key name. 863 | const QtAbstractPropertyModel *propertyModel = qobject_cast(index.model()); 864 | if(propertyModel) { 865 | const QMetaProperty metaProperty = propertyModel->metaPropertyAtIndex(index); 866 | if(metaProperty.isValid() && metaProperty.isEnumType()) { 867 | const QMetaEnum metaEnum = metaProperty.enumerator(); 868 | QByteArray currentKey = QByteArray(metaEnum.valueToKey(value.toInt())); 869 | QStyleOptionViewItem itemOption(option); 870 | initStyleOption(&itemOption, index); 871 | itemOption.text = QString(currentKey); 872 | QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &itemOption, painter); 873 | return; 874 | } 875 | } 876 | } else if(value.typeId() == QVariant::UserType) { 877 | if(value.canConvert()) { 878 | QAction *action = value.value().action; 879 | QStyleOptionButton buttonOption; 880 | buttonOption.state = QStyle::State_Active | QStyle::State_Raised; 881 | //buttonOption.features = QStyleOptionButton::DefaultButton; 882 | if(action) buttonOption.text = action->text(); 883 | buttonOption.rect = option.rect; 884 | //buttonOption.rect = QRect(option.rect.x() + 5, option.rect.y() + 5, option.rect.width() - 10, option.rect.height() - 10); 885 | QApplication::style()->drawControl(QStyle::CE_PushButton, &buttonOption, painter); 886 | return; 887 | } 888 | } 889 | } 890 | QStyledItemDelegate::paint(painter, option, index); 891 | } 892 | 893 | bool QtPropertyDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) 894 | { 895 | QVariant value = index.data(Qt::DisplayRole); 896 | if(value.isValid()) { 897 | if(value.typeId() == QVariant::Bool) { 898 | if(event->type() == QEvent::MouseButtonDblClick) 899 | return false; 900 | if(event->type() != QEvent::MouseButtonRelease) 901 | return false; 902 | QMouseEvent *mouseEvent = static_cast(event); 903 | if(mouseEvent->button() != Qt::LeftButton) 904 | return false; 905 | //QStyleOptionButton buttonOption; 906 | //QRect checkBoxRect = QApplication::style()->subElementRect(QStyle::SE_CheckBoxIndicator, &buttonOption); // Only used to get size of native checkbox widget. 907 | //buttonOption.rect = QStyle::alignedRect(option.direction, Qt::AlignLeft, checkBoxRect.size(), option.rect); // Our checkbox rect. 908 | // option.rect ==> cell 909 | // buttonOption.rect ==> check box 910 | // Here, we choose to allow clicks anywhere in the cell to toggle the checkbox. 911 | if(!option.rect.contains(mouseEvent->pos())) 912 | return false; 913 | bool checked = value.toBool(); 914 | QVariant newValue(!checked); // Toggle model's bool value. 915 | bool success = model->setData(index, newValue, Qt::EditRole); 916 | // Update entire table row just in case some other cell also refers to the same bool value. 917 | // Otherwise, that other cell will not reflect the current state of the bool set via this cell. 918 | if(success) 919 | model->dataChanged(index.sibling(index.row(), 0), index.sibling(index.row(), model->columnCount())); 920 | return success; 921 | } else if(value.typeId() == QVariant::UserType) { 922 | if(value.canConvert()) { 923 | QMouseEvent *mouseEvent = static_cast(event); 924 | if(mouseEvent->button() != Qt::LeftButton) 925 | return false; 926 | if(!option.rect.contains(mouseEvent->pos())) 927 | return false; 928 | QAction *action = value.value().action; 929 | if(action) action->trigger(); 930 | return true; 931 | } 932 | } 933 | } 934 | return QStyledItemDelegate::editorEvent(event, model, option, index); 935 | } 936 | 937 | QtPropertyTreeEditor::QtPropertyTreeEditor(QWidget *parent) : QTreeView(parent) 938 | { 939 | setItemDelegate(&_delegate); 940 | setAlternatingRowColors(true); 941 | setModel(&treeModel); 942 | } 943 | 944 | void QtPropertyTreeEditor::resizeColumnsToContents() 945 | { 946 | resizeColumnToContents(0); 947 | resizeColumnToContents(1); 948 | } 949 | 950 | QtPropertyTableEditor::QtPropertyTableEditor(QWidget *parent) : QTableView(parent) 951 | { 952 | setItemDelegate(&_delegate); 953 | setAlternatingRowColors(true); 954 | setModel(&tableModel); 955 | verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); 956 | setIsDynamic(_isDynamic); 957 | 958 | // Draggable rows. 959 | verticalHeader()->setSectionsMovable(_isDynamic); 960 | connect(verticalHeader(), SIGNAL(sectionMoved(int, int, int)), this, SLOT(handleSectionMove(int, int, int))); 961 | 962 | // Header context menus. 963 | horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); 964 | verticalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); 965 | connect(horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(horizontalHeaderContextMenu(QPoint))); 966 | connect(verticalHeader(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(verticalHeaderContextMenu(QPoint))); 967 | 968 | // Custom corner button. 969 | if(QAbstractButton *cornerButton = findChild()) { 970 | cornerButton->installEventFilter(this); 971 | } 972 | } 973 | 974 | void QtPropertyTableEditor::setIsDynamic(bool b) 975 | { 976 | _isDynamic = b; 977 | 978 | // Dragging rows. 979 | verticalHeader()->setSectionsMovable(_isDynamic); 980 | 981 | // Corner button. 982 | if(QAbstractButton *cornerButton = findChild()) { 983 | if(_isDynamic) { 984 | cornerButton->disconnect(SIGNAL(clicked())); 985 | connect(cornerButton, SIGNAL(clicked()), this, SLOT(appendRow())); 986 | cornerButton->setText("+"); 987 | cornerButton->setToolTip("Append row"); 988 | } else { 989 | cornerButton->disconnect(SIGNAL(clicked())); 990 | connect(cornerButton, SIGNAL(clicked()), this, SLOT(selectAll())); 991 | cornerButton->setText(""); 992 | cornerButton->setToolTip("Select all"); 993 | } 994 | // adjust the width of the vertical header to match the preferred corner button width 995 | // (unfortunately QAbstractButton doesn't implement any size hinting functionality) 996 | QStyleOptionHeader opt; 997 | opt.text = cornerButton->text(); 998 | //opt.icon = cornerButton->icon(); 999 | 1000 | /* 1001 | QSize s = ( cornerButton->style()->sizeFromContents( 1002 | QStyle::CT_HeaderSection, 1003 | &opt, 1004 | QSize(), 1005 | cornerButton 1006 | ).expandedTo( QApplication::globalStrut() ) 1007 | ); 1008 | */ 1009 | 1010 | // Qt6 1011 | // {{ 1012 | QSize s = cornerButton->style()->sizeFromContents(QStyle::CT_HeaderSection, &opt, QSize(), cornerButton).expandedTo(QSize(10, 10)); // QSize(10, 10)로 globalStrut 대체 1013 | // }} 1014 | 1015 | if(s.isValid()) { 1016 | verticalHeader()->setMinimumWidth(s.width()); 1017 | } 1018 | } 1019 | } 1020 | 1021 | void QtPropertyTableEditor::horizontalHeaderContextMenu(QPoint pos) 1022 | { 1023 | QModelIndexList indexes = selectionModel()->selectedColumns(); 1024 | QMenu *menu = new QMenu; 1025 | menu->addAction("Resize Columns To Contents", this, SLOT(resizeColumnsToContents())); 1026 | menu->popup(horizontalHeader()->viewport()->mapToGlobal(pos)); 1027 | } 1028 | 1029 | void QtPropertyTableEditor::verticalHeaderContextMenu(QPoint pos) 1030 | { 1031 | QModelIndexList indexes = selectionModel()->selectedRows(); 1032 | QMenu *menu = new QMenu; 1033 | if(_isDynamic) { 1034 | QtPropertyTableModel *propertyTableModel = qobject_cast(model()); 1035 | if(propertyTableModel->objectCreator()) { 1036 | menu->addAction("Append Row", this, SLOT(appendRow())); 1037 | } 1038 | if(indexes.size()) { 1039 | if(propertyTableModel->objectCreator()) { 1040 | menu->addSeparator(); 1041 | menu->addAction("Insert Rows", this, SLOT(insertSelectedRows())); 1042 | menu->addSeparator(); 1043 | } 1044 | menu->addAction("Delete Rows", this, SLOT(removeSelectedRows())); 1045 | } 1046 | } 1047 | menu->popup(verticalHeader()->viewport()->mapToGlobal(pos)); 1048 | } 1049 | 1050 | void QtPropertyTableEditor::appendRow() 1051 | { 1052 | if(!_isDynamic) 1053 | return; 1054 | QtPropertyTableModel *propertyTableModel = qobject_cast(model()); 1055 | if(!propertyTableModel || !propertyTableModel->objectCreator()) 1056 | return; 1057 | model()->insertRows(model()->rowCount(), 1); 1058 | } 1059 | 1060 | void QtPropertyTableEditor::insertSelectedRows() 1061 | { 1062 | if(!_isDynamic) 1063 | return; 1064 | QtPropertyTableModel *propertyTableModel = qobject_cast(model()); 1065 | if(!propertyTableModel || !propertyTableModel->objectCreator()) 1066 | return; 1067 | QModelIndexList indexes = selectionModel()->selectedRows(); 1068 | if(indexes.size() == 0) 1069 | return; 1070 | QList rows; 1071 | foreach(const QModelIndex &index, indexes) { 1072 | rows.append(index.row()); 1073 | } 1074 | 1075 | // Qt5 1076 | // qSort(rows); 1077 | 1078 | // Qt6 1079 | QVector vec = rows.toVector(); 1080 | std::sort(vec.begin(), vec.end()); 1081 | rows = QList::fromVector(vec); 1082 | 1083 | model()->insertRows(rows.at(0), rows.size()); 1084 | } 1085 | 1086 | void QtPropertyTableEditor::removeSelectedRows() 1087 | { 1088 | if(!_isDynamic) 1089 | return; 1090 | QModelIndexList indexes = selectionModel()->selectedRows(); 1091 | if(indexes.size() == 0) 1092 | return; 1093 | QList rows; 1094 | foreach(const QModelIndex &index, indexes) { 1095 | rows.append(index.row()); 1096 | } 1097 | 1098 | // Qt5 1099 | // qSort(rows); 1100 | 1101 | // Qt6 1102 | QVector vec = rows.toVector(); 1103 | std::sort(vec.begin(), vec.end()); 1104 | rows = QList::fromVector(vec); 1105 | 1106 | 1107 | for(int i = rows.size() - 1; i >= 0; --i) { 1108 | model()->removeRows(rows.at(i), 1); 1109 | } 1110 | } 1111 | 1112 | void QtPropertyTableEditor::handleSectionMove(int /* logicalIndex */, int oldVisualIndex, int newVisualIndex) 1113 | { 1114 | if(!_isDynamic) 1115 | return; 1116 | QtPropertyTableModel *propertyTableModel = qobject_cast(model()); 1117 | if(!propertyTableModel) 1118 | return; 1119 | // Move objects in the model, and then move the sections back to maintain logicalIndex order. 1120 | propertyTableModel->moveRows(QModelIndex(), oldVisualIndex, 1, QModelIndex(), newVisualIndex); 1121 | disconnect(verticalHeader(), SIGNAL(sectionMoved(int, int, int)), this, SLOT(handleSectionMove(int, int, int))); 1122 | verticalHeader()->moveSection(newVisualIndex, oldVisualIndex); 1123 | connect(verticalHeader(), SIGNAL(sectionMoved(int, int, int)), this, SLOT(handleSectionMove(int, int, int))); 1124 | } 1125 | 1126 | void QtPropertyTableEditor::keyPressEvent(QKeyEvent *event) 1127 | { 1128 | switch(event->key()) { 1129 | case Qt::Key_Backspace: 1130 | case Qt::Key_Delete: 1131 | if(_isDynamic && QMessageBox::question(this, "Delete Rows?", "Delete selected rows?", QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { 1132 | removeSelectedRows(); 1133 | } 1134 | break; 1135 | 1136 | case Qt::Key_Plus: 1137 | appendRow(); 1138 | break; 1139 | 1140 | default: 1141 | break; 1142 | } 1143 | } 1144 | 1145 | bool QtPropertyTableEditor::eventFilter(QObject* o, QEvent* e) 1146 | { 1147 | if (e->type() == QEvent::Paint) { 1148 | if(QAbstractButton *btn = qobject_cast(o)) { 1149 | // paint by hand (borrowed from QTableCornerButton) 1150 | QStyleOptionHeader opt; 1151 | 1152 | // opt.init(btn); 1153 | opt.initFrom(btn); // Qt6 1154 | 1155 | QStyle::State styleState = QStyle::State_None; 1156 | if (btn->isEnabled()) 1157 | styleState |= QStyle::State_Enabled; 1158 | if (btn->isActiveWindow()) 1159 | styleState |= QStyle::State_Active; 1160 | if (btn->isDown()) 1161 | styleState |= QStyle::State_Sunken; 1162 | opt.state = styleState; 1163 | opt.rect = btn->rect(); 1164 | opt.text = btn->text(); // this line is the only difference to QTableCornerButton 1165 | //opt.icon = btn->icon(); // this line is the only difference to QTableCornerButton 1166 | opt.position = QStyleOptionHeader::OnlyOneSection; 1167 | QStylePainter painter(btn); 1168 | painter.drawControl(QStyle::CE_Header, opt); 1169 | return true; // eat event 1170 | } 1171 | } 1172 | return false; 1173 | } 1174 | 1175 | } // QtPropertyEditor 1176 | -------------------------------------------------------------------------------- /QtPropertyEditor.h: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------- 2 | * QObject property editor UI. 3 | * 4 | * Author: Marcel Paz Goldschen-Ohm 5 | * Email: marcel.goldschen@gmail.com 6 | * -------------------------------------------------------------------------------- */ 7 | 8 | #ifndef __QtPropertyEditor_H__ 9 | #define __QtPropertyEditor_H__ 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #ifdef DEBUG 31 | #include 32 | #include 33 | #endif 34 | 35 | namespace QtPropertyEditor 36 | { 37 | // List all object property names. 38 | QList getPropertyNames(QObject *object); 39 | QList getMetaPropertyNames(const QMetaObject &metaObject); 40 | QList getNoninheritedPropertyNames(QObject *object); 41 | 42 | // Handle descendant properties such as "child.grandchild.property". 43 | QObject* descendant(QObject *object, const QByteArray &pathToDescendantObject); 44 | 45 | // Get the size of a QTableView widget. 46 | QSize getTableSize(const QTableView *table); 47 | 48 | /* -------------------------------------------------------------------------------- 49 | * Things that all QObject property models should be able to do. 50 | * -------------------------------------------------------------------------------- */ 51 | class QtAbstractPropertyModel : public QAbstractItemModel 52 | { 53 | Q_OBJECT 54 | 55 | public: 56 | QtAbstractPropertyModel(QObject *parent = 0) : QAbstractItemModel(parent) {} 57 | 58 | QList propertyNames; 59 | QHash propertyHeaders; 60 | void setProperties(const QString &str); 61 | void addProperty(const QString &str); 62 | 63 | virtual QObject* objectAtIndex(const QModelIndex &index) const = 0; 64 | virtual QByteArray propertyNameAtIndex(const QModelIndex &index) const = 0; 65 | const QMetaProperty metaPropertyAtIndex(const QModelIndex &index) const; 66 | virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; 67 | virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); 68 | virtual Qt::ItemFlags flags(const QModelIndex &index) const; 69 | }; 70 | 71 | /* -------------------------------------------------------------------------------- 72 | * Property tree model for a QObject tree. 73 | * Max tree depth can be specified (i.e. depth = 0 --> single object only). 74 | * -------------------------------------------------------------------------------- */ 75 | class QtPropertyTreeModel : public QtAbstractPropertyModel 76 | { 77 | Q_OBJECT 78 | 79 | public: 80 | // Internal tree node. 81 | struct Node 82 | { 83 | // Node traversal. 84 | Node *parent = NULL; 85 | QList children; 86 | 87 | // Node data. 88 | QObject *object = NULL; 89 | QByteArray propertyName; 90 | 91 | Node(Node *parent = NULL) : parent(parent) {} 92 | ~Node() { qDeleteAll(children); } 93 | 94 | void setObject(QObject *object, int maxChildDepth = -1, const QList &propertyNames = QList()); 95 | }; 96 | 97 | QtPropertyTreeModel(QObject *parent = NULL) : QtAbstractPropertyModel(parent) {} 98 | 99 | // Getters. 100 | QObject* object() const { return _root.object; } 101 | int maxDepth() const { return _maxTreeDepth; } 102 | 103 | // Setters. 104 | void setObject(QObject *object) { beginResetModel(); _root.setObject(object, _maxTreeDepth, propertyNames); endResetModel(); } 105 | void setMaxDepth(int i) { beginResetModel(); _maxTreeDepth = i; reset(); endResetModel(); } 106 | void setProperties(const QString &str) { beginResetModel(); QtAbstractPropertyModel::setProperties(str); reset(); endResetModel(); } 107 | void addProperty(const QString &str) { beginResetModel(); QtAbstractPropertyModel::addProperty(str); reset(); endResetModel(); } 108 | 109 | // Model interface. 110 | Node* nodeAtIndex(const QModelIndex &index) const; 111 | QObject* objectAtIndex(const QModelIndex &index) const; 112 | QByteArray propertyNameAtIndex(const QModelIndex &index) const; 113 | QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; 114 | QModelIndex parent(const QModelIndex &index) const; 115 | int rowCount(const QModelIndex &parent = QModelIndex()) const; 116 | int columnCount(const QModelIndex &parent = QModelIndex()) const; 117 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; 118 | bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); 119 | Qt::ItemFlags flags(const QModelIndex &index) const; 120 | QVariant headerData(int section, Qt::Orientation orientation, int role) const; 121 | 122 | public slots: 123 | void reset() { setObject(object()); } 124 | 125 | protected: 126 | Node _root; 127 | int _maxTreeDepth = -1; 128 | }; 129 | 130 | /* -------------------------------------------------------------------------------- 131 | * Property table model for a list of QObjects (rows are objects, columns are properties). 132 | * -------------------------------------------------------------------------------- */ 133 | class QtPropertyTableModel : public QtAbstractPropertyModel 134 | { 135 | Q_OBJECT 136 | 137 | public: 138 | typedef std::function ObjectCreatorFunction; 139 | 140 | QtPropertyTableModel(QObject *parent = NULL) : QtAbstractPropertyModel(parent) {} 141 | 142 | // Getters. 143 | QObjectList objects() const { return _objects; } 144 | ObjectCreatorFunction objectCreator() const { return _objectCreator; } 145 | 146 | // Setters. 147 | void setObjects(const QObjectList &objects) { beginResetModel(); _objects = objects; endResetModel(); } 148 | template 149 | void setObjects(const QList &objects); 150 | template 151 | void setChildObjects(QObject *parent); 152 | void setObjectCreator(ObjectCreatorFunction creator) { _objectCreator = creator; } 153 | void setProperties(const QString &str) { beginResetModel(); QtAbstractPropertyModel::setProperties(str); endResetModel(); } 154 | void addProperty(const QString &str) { beginResetModel(); QtAbstractPropertyModel::addProperty(str); endResetModel(); } 155 | 156 | // Model interface. 157 | QObject* objectAtIndex(const QModelIndex &index) const; 158 | QByteArray propertyNameAtIndex(const QModelIndex &index) const; 159 | QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; 160 | QModelIndex parent(const QModelIndex &index) const; 161 | int rowCount(const QModelIndex &parent = QModelIndex()) const; 162 | int columnCount(const QModelIndex &parent = QModelIndex()) const; 163 | QVariant headerData(int section, Qt::Orientation orientation, int role) const; 164 | bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()); 165 | bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); 166 | bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationRow); 167 | void reorderChildObjectsToMatchRowOrder(int firstRow = 0); 168 | 169 | // Default creator functions for convenience. 170 | // Requires template class T to implement a default constructor T(). 171 | template 172 | static QObject* defaultCreator() { return new T(); } 173 | template 174 | static QObject* defaultChildCreator(QObject *parent) { T *object = new T(); object->setParent(parent); return object; } 175 | 176 | signals: 177 | void rowCountChanged(); 178 | void rowOrderChanged(); 179 | 180 | protected: 181 | QObjectList _objects; 182 | ObjectCreatorFunction _objectCreator = NULL; 183 | }; 184 | 185 | template 186 | void QtPropertyTableModel::setObjects(const QList &objects) 187 | { 188 | beginResetModel(); 189 | _objects.clear(); 190 | foreach(T *object, objects) { 191 | if(QObject *obj = qobject_cast(object)) 192 | _objects.append(obj); 193 | } 194 | endResetModel(); 195 | } 196 | 197 | template 198 | void QtPropertyTableModel::setChildObjects(QObject *parent) 199 | { 200 | beginResetModel(); 201 | _objects.clear(); 202 | foreach(T *derivedObject, parent->findChildren(QString(), Qt::FindDirectChildrenOnly)) { 203 | if(QObject *object = qobject_cast(derivedObject)) 204 | _objects.append(object); 205 | } 206 | _objectCreator = std::bind(&QtPropertyTableModel::defaultChildCreator, parent); 207 | endResetModel(); 208 | } 209 | 210 | /* -------------------------------------------------------------------------------- 211 | * Property editor delegate. 212 | * -------------------------------------------------------------------------------- */ 213 | class QtPropertyDelegate: public QStyledItemDelegate 214 | { 215 | public: 216 | QtPropertyDelegate(QWidget *parent = 0) : QStyledItemDelegate(parent) {} 217 | 218 | QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; 219 | void setEditorData(QWidget *editor, const QModelIndex &index) const Q_DECL_OVERRIDE; 220 | void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const Q_DECL_OVERRIDE; 221 | QString displayText(const QVariant &value, const QLocale &locale) const Q_DECL_OVERRIDE; 222 | void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; 223 | 224 | protected: 225 | bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) Q_DECL_OVERRIDE; 226 | }; 227 | 228 | /* -------------------------------------------------------------------------------- 229 | * User types for QVariant that will be handled by QtPropertyDelegate. 230 | * User types need to be declared via Q_DECLARE_METATYPE (see below outside of namespace) 231 | * and also registered via qRegisterMetaType (see static instantiation in .cpp file) 232 | * -------------------------------------------------------------------------------- */ 233 | 234 | // For static registration of user types (see static instantiation in QtPropertyEditor.cpp). 235 | template class MetaTypeRegistration 236 | { 237 | public: 238 | inline MetaTypeRegistration() 239 | { 240 | qRegisterMetaType(); 241 | } 242 | }; 243 | 244 | // For push buttons. 245 | // See Q_DECLARE_METATYPE below and qRegisterMetaType in .cpp file. 246 | class QtPushButtonActionWrapper 247 | { 248 | public: 249 | QtPushButtonActionWrapper(QAction *action = NULL) : action(action) {} 250 | QtPushButtonActionWrapper(const QtPushButtonActionWrapper &other) { action = other.action; } 251 | ~QtPushButtonActionWrapper() {} 252 | QAction *action = NULL; 253 | }; 254 | 255 | /* -------------------------------------------------------------------------------- 256 | * Tree editor for properties in a QObject tree. 257 | * -------------------------------------------------------------------------------- */ 258 | class QtPropertyTreeEditor : public QTreeView 259 | { 260 | Q_OBJECT 261 | 262 | public: 263 | QtPropertyTreeEditor(QWidget *parent = NULL); 264 | 265 | // Owns its own tree model for convenience. This means model will be deleted along with editor. 266 | // However, you're not forced to use this model. 267 | QtPropertyTreeModel treeModel; 268 | 269 | public slots: 270 | void resizeColumnsToContents(); 271 | 272 | protected: 273 | QtPropertyDelegate _delegate; 274 | }; 275 | 276 | 277 | /* -------------------------------------------------------------------------------- 278 | * Table editor for properties in a list of QObjects. 279 | * -------------------------------------------------------------------------------- */ 280 | class QtPropertyTableEditor : public QTableView 281 | { 282 | Q_OBJECT 283 | 284 | public: 285 | QtPropertyTableEditor(QWidget *parent = NULL); 286 | 287 | // Owns its own table model for convenience. This means model will be deleted along with editor. 288 | // However, you're not forced to use this model. 289 | QtPropertyTableModel tableModel; 290 | 291 | bool isDynamic() const { return _isDynamic; } 292 | void setIsDynamic(bool b); 293 | 294 | QSize sizeHint() const Q_DECL_OVERRIDE { return getTableSize(this); } 295 | 296 | public slots: 297 | void horizontalHeaderContextMenu(QPoint pos); 298 | void verticalHeaderContextMenu(QPoint pos); 299 | void appendRow(); 300 | void insertSelectedRows(); 301 | void removeSelectedRows(); 302 | void handleSectionMove(int logicalIndex, int oldVisualIndex, int newVisualIndex); 303 | 304 | protected: 305 | QtPropertyDelegate _delegate; 306 | bool _isDynamic = true; 307 | 308 | void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; 309 | bool eventFilter(QObject* o, QEvent* e) Q_DECL_OVERRIDE; 310 | }; 311 | 312 | } // QtPropertyEditor 313 | 314 | Q_DECLARE_METATYPE(QtPropertyEditor::QtPushButtonActionWrapper); 315 | 316 | #endif // __QtPropertyEditor_H__ 317 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QtPropertyEditor 2 | 3 | UI property editors for QObject-derived classes. 4 | 5 | * QObject editor is tree view similar to the editor in QtDesigner. 6 | * QObjectList editor is a table view where rows are objects and columns are properties. 7 | * Allows dynamic object (row) insertion and deletion similar to Excel. 8 | * Supports object reordering by dragging rows with the mouse. 9 | * Default delegates for editing common value types (these are in addition to the default delegates already in Qt): 10 | * bool: QCheckBox 11 | * QEnum: QComboBox 12 | * double: QLineEdit that can handle scientific notation 13 | * QSize/QSizeF: QLineEdit for text format *(w x h)* 14 | * QPoint/QPointF: QLineEdit for text format *(x, y)* 15 | * QRect/QRectF: QLineEdit for text format *[(x, y) w x h]* 16 | * Handles QPushButton actions. 17 | 18 | **Author**: Marcel Goldschen-Ohm 19 | **Email**: 20 | **License**: MIT 21 | Copyright (c) 2017 Marcel Goldschen-Ohm 22 | 23 | ## QtPropertyTreeEditor 24 | 25 | Property editor for a QObject is a tree view with two columns of property name/value pairs. Child objects are expandable branches with their own property name/value pairs. Maximum tree depth can be specified (i.e. depth = 0 implies no children shown). 26 | 27 | 28 | 29 | ## QtPropertyTableEditor 30 | 31 | Editor for a list of QObjects is a table where rows are objects and columns are properties. Allows dynamic insertion/deletion of objects (rows) via a context menu obtainable by right clicking on the row headers (similar to Excel). List objects (rows) can be reordered by dragging the row header with the mouse. :warning: **All of this only makes sense if all of the objects to be exposed in the editor have the same properties (i.e. they are all the same type of object).** 32 | 33 | 34 | 35 | ## INSTALL 36 | 37 | Everything is in: 38 | 39 | * `QtPropertyEditor.h` 40 | * `QtPropertyEditor.cpp` 41 | 42 | ### CMake: 43 | 44 | See `CMakeLists.txt` for example build as a static library. 45 | 46 | :point_right: **This is most likely what you want:** See `test/CMakeLists.txt` for example build of an app that uses QtPropertyEditor. This build uses CMake to automatically download QtPropertyEditor files directly from this GitHub repository, builds QtPropertyEditor as a static library and links it to the app executable. This way you can use QtPropertyEditor in your project without downloading or managing the QtPropertyEditor repository manually. 47 | 48 | ### Requires: 49 | 50 | * [Qt](http://www.qt.io) 51 | 52 | ## QtPropertyTreeEditor Example 53 | 54 | The QApplication, same as always. 55 | 56 | ```cpp 57 | QApplication app(...); 58 | ``` 59 | 60 | An object derived from QObject whose properties will be exposed in the editor. 61 | 62 | ```cpp 63 | TestObject object; // See test_QtPropertyEditor.h 64 | ``` 65 | 66 | Children of the object (and their children recursively) are shown as branches of the object's tree. 67 | 68 | ```cpp 69 | TestObject *child = new TestObject("MyChild"); 70 | child->setParent(&object); 71 | ``` 72 | 73 | The model interface to our object's properties. 74 | 75 | ```cpp 76 | QtPropertyEditor::QtPropertyTreeModel model; 77 | model.setObject(&object); 78 | ``` 79 | 80 | **[Optional]** You can define which properties to expose in the editor (default includes all properties including dynamic properties). For example, if we only wanted to show the "objectName" and "myInt" properties: 81 | 82 | ```cpp 83 | model.setProperties("objectName, myInt"); 84 | model.addProperty("myDouble"); 85 | ``` 86 | 87 | **[Optional]** You can map property names to headers that will be displayed instead of the property name. Usually, this is when you want some nonstandard charachters to be displayed that are not allowed to be part of the property name. For example, if we wanted the "objectName" property to be displayed as if it was the "Name" property instead: 88 | 89 | ```cpp 90 | model.propertyHeaders["objectName"] = "Name"; 91 | ``` 92 | 93 | You can also specify property headers in the `setProperties` or `addProperty` functions by including "name: header" string pairs: 94 | 95 | ```cpp 96 | model.setProperties("objectName: Name, myInt"); 97 | model.addProperty("myDouble: My Cool Double"); 98 | ``` 99 | 100 | The tree view UI editor linked to our object's model interface. **Note: The editor owns its own tree model that it is linked to by default and which will be deleted along with the editor. However, you are free to link the editor to another model via `setModel()` if you want to.** 101 | 102 | ```cpp 103 | QtPropertyEditor::QtPropertyTreeEditor editor; 104 | editor.setModel(&model); // OR do NOT call this to use the default editor.treeModel model. 105 | ``` 106 | 107 | Show the editor and run the application. 108 | 109 | ```cpp 110 | editor.show(); 111 | app.exec(); 112 | ``` 113 | 114 | ## QtPropertyTableEditor Example 115 | 116 | The QApplication, same as always. 117 | 118 | ```cpp 119 | QApplication app(...); 120 | ``` 121 | 122 | A list of objects derived from QObject whose properties will be exposed in the editor. Although it is NOT required, for this example we'll make the objects in our list children of a single parent object. :warning: **All of this only makes sense if all of the objects to be exposed in the editor have the same properties (i.e. they are all the same type of object).** 123 | 124 | ```cpp 125 | QObject parent; 126 | for(int i = 0; i < 5; ++i) { 127 | // TestObject defined in test_QtPropertyEditor.h 128 | QObject *object = new TestObject("My Obj " + QString::number(i)); 129 | object->setParent(&parent); 130 | } 131 | QObjectList objects = parent->children(); 132 | ``` 133 | 134 | The model interface to the properties in our list of objects. 135 | 136 | ```cpp 137 | QtPropertyEditor::QtPropertyTableModel model; 138 | model.setObjects(objects); 139 | ``` 140 | 141 | **[Optional]** For dynamic object insertion in the list, you need to supply an object creator function of type `QtPropertyTableModel::ObjectCreatorFunction` which is a typedef for `std::function`. **If you want the newly created objects to be children of a particular parent object, you need to wrap this into the creator function. For example, as shown below.** 142 | 143 | ```cpp 144 | // The creator function. 145 | QObject* createNewTestObject(QObject *parent) 146 | { 147 | // TestObject defined in test_QtPropertyEditor.h 148 | return new TestObject("New Test Object", parent); 149 | } 150 | ``` 151 | 152 | ```cpp 153 | // This will make sure all newly inserted objects 154 | // in the model are children of parent. 155 | std::function func = 156 | std::bind(createNewTestObject, &parent); 157 | model.setObjectCreator(func); 158 | ``` 159 | 160 | **[Optional]** Exposed properties and their column headers can be specified exactly the same as shown in the example above for QtPropertyTreeEditor. 161 | 162 | **[Optional]** Default is a flat editor for each object's properties excluding properties of child objects. However, specific child object properties can be made available in the table view by adding *"path.to.child.property"* to the specified list of property names to be displayed. In this case, *path*, *to* and *child* are the object names of a child object tree, and *property* is a property name for *child*. Note that for this to make sense all objects in the list should have a valid *"path.to.child.property"*. For example, to expose the "myInt" property of the child object named "child": 163 | 164 | ```cpp 165 | model.addProperty("child.myInt"); 166 | ``` 167 | 168 | The table view UI editor linked to the model interface for our list of objects. **Note: The editor owns its own table model that it is linked to by default and which will be deleted along with the editor. However, you are free to link the editor to another model via `setModel()` if you want to.** 169 | 170 | ```cpp 171 | QtPropertyEditor::QtPropertyTableEditor editor; 172 | editor.setModel(&model); // OR do NOT call this to use the default editor.tableModel model. 173 | ``` 174 | 175 | Show the editor and run the application. 176 | 177 | ```cpp 178 | editor.show(); 179 | app.exec(); 180 | ``` 181 | -------------------------------------------------------------------------------- /images/QtPropertyTableEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcel-goldschen-ohm/QtPropertyEditor/35d813ee06de9f86174dac2d2bc5956b63f50998/images/QtPropertyTableEditor.png -------------------------------------------------------------------------------- /images/QtPropertyTreeEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcel-goldschen-ohm/QtPropertyEditor/35d813ee06de9f86174dac2d2bc5956b63f50998/images/QtPropertyTreeEditor.png -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Created by Marcel Paz Goldschen-Ohm 2 | 3 | # !!! Set env variable QT6_DIR to /lib/cmake/Qt6 4 | 5 | cmake_minimum_required(VERSION 3.11) 6 | 7 | set(PROJECT_NAME test_QtPropertyEditor) 8 | project(${PROJECT_NAME} LANGUAGES CXX) 9 | 10 | include(FetchContent) 11 | 12 | # Fetch QtPropertyEditor repository from GitHub. 13 | set(REPO QtPropertyEditor) 14 | string(TOLOWER ${REPO} REPOlc) 15 | FetchContent_Declare(${REPO} 16 | GIT_REPOSITORY "https://github.com/marcel-goldschen-ohm/QtPropertyEditor.git" 17 | ) 18 | FetchContent_GetProperties(${REPO}) 19 | if(NOT ${REPOlc}_POPULATED) 20 | FetchContent_Populate(${REPO}) 21 | message(STATUS "${REPO} source dir: ${${REPOlc}_SOURCE_DIR}") 22 | message(STATUS "${REPO} binary dir: ${${REPOlc}_BINARY_DIR}") 23 | endif() 24 | 25 | set(CMAKE_CXX_STANDARD 17) # This is equal to QMAKE_CXX_FLAGS += -std=c++0x 26 | set(CMAKE_INCLUDE_CURRENT_DIR ON) # Search in current dir. 27 | set(CMAKE_AUTOMOC ON) # Run moc automatically for Qt. 28 | set(CMAKE_AUTOUIC ON) # Run uic automatically for *.ui files. 29 | set(CMAKE_AUTORCC ON) # Run automatically for *.qrc files. 30 | 31 | if(APPLE AND EXISTS /usr/local/opt/qt6) 32 | # Homebrew installs Qt6, ensure it can be found by CMake. 33 | list(APPEND CMAKE_PREFIX_PATH "/usr/local/opt/qt6") 34 | endif() 35 | 36 | # Find required packages. 37 | find_package(Qt6 COMPONENTS Widgets REQUIRED) 38 | 39 | # Build fetched repository as a static library. 40 | add_library(QtPropertyEditor STATIC ${qtpropertyeditor_SOURCE_DIR}/QtPropertyEditor.cpp ${qtpropertyeditor_SOURCE_DIR}/QtPropertyEditor.h) 41 | target_link_libraries(QtPropertyEditor Qt6::Widgets) 42 | 43 | # Build project executable. 44 | add_executable(${PROJECT_NAME} test_QtPropertyEditor.cpp test_QtPropertyEditor.h) 45 | target_include_directories(${PROJECT_NAME} PUBLIC ${qtpropertyeditor_SOURCE_DIR}) 46 | target_link_libraries(${PROJECT_NAME} Qt6::Widgets QtPropertyEditor) 47 | 48 | -------------------------------------------------------------------------------- /test/test_QtPropertyEditor.cpp: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------- 2 | * Example tests for QtObjectPropertyEditor. 3 | * 4 | * Author: Marcel Paz Goldschen-Ohm 5 | * Email: marcel.goldschen@gmail.com 6 | * -------------------------------------------------------------------------------- */ 7 | 8 | #include "test_QtPropertyEditor.h" 9 | 10 | #include 11 | #include 12 | 13 | #include "QtPropertyEditor.h" 14 | 15 | int testQtPropertyTreeEditor(int argc, char **argv) 16 | { 17 | QApplication app(argc, argv); 18 | 19 | // Object. 20 | TestObject object("My Obj"); 21 | 22 | // Dynamic properties. 23 | object.setProperty("myDynamicBool", false); 24 | object.setProperty("myDynamicInt", 3); 25 | object.setProperty("myDynamicDouble", 3.0); 26 | object.setProperty("myDynamicString", "3 amigos"); 27 | object.setProperty("myDynamicDateTime", QDateTime::currentDateTime()); 28 | 29 | // UI. 30 | QtPropertyEditor::QtPropertyTreeEditor editor; 31 | editor.treeModel.propertyNames = QtPropertyEditor::getPropertyNames(&object); 32 | editor.treeModel.addProperty("child.myInt"); 33 | editor.treeModel.propertyHeaders["objectName"] = "Name"; 34 | editor.treeModel.setObject(&object); 35 | editor.show(); 36 | editor.resizeColumnsToContents(); 37 | 38 | int status = app.exec(); 39 | 40 | object.dumpObjectInfo(); 41 | 42 | return status; 43 | } 44 | 45 | QObject* newTestObject(QObject *parent) { return new TestObject("", parent); } 46 | 47 | int testQtPropertyTableEditor(int argc, char **argv) 48 | { 49 | QApplication app(argc, argv); 50 | 51 | // Objects. 52 | QObject parent; 53 | QObjectList objects; 54 | for(int i = 0; i < 5; ++i) { 55 | QObject *object = new TestObject("My Obj " + QString::number(i), &parent); 56 | // Dynamic properties. 57 | object->setProperty("myDynamicBool", false); 58 | object->setProperty("myDynamicInt", 3); 59 | object->setProperty("myDynamicDouble", 3.0); 60 | object->setProperty("myDynamicString", "3 amigos"); 61 | object->setProperty("myDynamicDateTime", QDateTime::currentDateTime()); 62 | objects.append(object); 63 | } 64 | 65 | // UI. 66 | QtPropertyEditor::QtPropertyTableEditor editor; 67 | editor.tableModel.propertyNames = QtPropertyEditor::getMetaPropertyNames(TestObject::staticMetaObject); 68 | editor.tableModel.addProperty("child.myInt"); 69 | editor.tableModel.propertyHeaders["objectName"] = "Name"; 70 | editor.tableModel.setChildObjects(&parent); 71 | editor.show(); 72 | editor.resizeColumnsToContents(); 73 | 74 | int status = app.exec(); 75 | 76 | // Check child object order. 77 | foreach(QObject *object, parent.findChildren(QString(), Qt::FindDirectChildrenOnly)) { 78 | qDebug() << object->objectName(); 79 | } 80 | 81 | return status; 82 | } 83 | 84 | int main(int argc, char **argv) 85 | { 86 | testQtPropertyTreeEditor(argc, argv); 87 | testQtPropertyTableEditor(argc, argv); 88 | return 0; 89 | } 90 | -------------------------------------------------------------------------------- /test/test_QtPropertyEditor.h: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------- 2 | * QObject property editor UI. 3 | * 4 | * Author: Marcel Paz Goldschen-Ohm 5 | * Email: marcel.goldschen@gmail.com 6 | * -------------------------------------------------------------------------------- */ 7 | 8 | #ifndef __test_QtPropertyEditor_H__ 9 | #define __test_QtPropertyEditor_H__ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | /* -------------------------------------------------------------------------------- 22 | * QObject derived class with some properties. 23 | * -------------------------------------------------------------------------------- */ 24 | class TestObject : public QObject 25 | { 26 | Q_OBJECT 27 | Q_PROPERTY(MyEnum myEnum READ myEnum WRITE setMyEnum) 28 | Q_PROPERTY(MyEnum myReadOnlyEnum READ myEnum) 29 | Q_PROPERTY(bool myBool READ myBool WRITE setMyBool) 30 | Q_PROPERTY(bool myReadOnlyBool READ myBool) 31 | Q_PROPERTY(int myInt READ myInt WRITE setMyInt) 32 | Q_PROPERTY(int myReadOnlyInt READ myInt) 33 | Q_PROPERTY(float myFloat READ myFloat WRITE setMyFloat) 34 | Q_PROPERTY(float myReadOnlyFloat READ myFloat) 35 | Q_PROPERTY(double myDouble READ myDouble WRITE setMyDouble) 36 | Q_PROPERTY(double myReadOnlyDouble READ myDouble) 37 | Q_PROPERTY(QString myString READ myString WRITE setMyString) 38 | Q_PROPERTY(QString myReadOnlyString READ myString) 39 | Q_PROPERTY(QDateTime myDateTime READ myDateTime WRITE setMyDateTime) 40 | Q_PROPERTY(QDateTime myReadOnlyDateTime READ myDateTime) 41 | Q_PROPERTY(QSize mySize READ mySize WRITE setMySize) 42 | Q_PROPERTY(QSizeF mySizeF READ mySizeF WRITE setMySizeF) 43 | Q_PROPERTY(QPoint myPoint READ myPoint WRITE setMyPoint) 44 | Q_PROPERTY(QPointF myPointF READ myPointF WRITE setMyPointF) 45 | Q_PROPERTY(QRect myRect READ myRect WRITE setMyRect) 46 | Q_PROPERTY(QRectF myRectF READ myRectF WRITE setMyRectF) 47 | 48 | public: 49 | // Custom enum will be editable via a QComboBox so long as we tell Qt about it with Q_ENUMS(). 50 | enum MyEnum { A, B, C }; 51 | Q_ENUMS(MyEnum) 52 | 53 | // Init. 54 | TestObject(const QString &name = "", QObject *parent = 0, bool hasChild = true) : QObject(parent), _myEnum(B), _myBool(true), _myInt(82), _myFloat(3.14), _myDouble(3.14e-12), _myString("Hi-ya!"), _myDateTime(QDateTime::currentDateTime()), _mySize(2, 4), _mySizeF(3.1, 4.9), _myPoint(0, 1), _myPointF(0.05, 1.03), _myRect(0, 0, 3, 3), _myRectF(0.5, 0.5, 1.3, 3.1) 55 | { 56 | setObjectName(name); 57 | // Child object. 58 | if(hasChild) 59 | new TestObject("child", this, false); 60 | } 61 | 62 | // Property getters. 63 | MyEnum myEnum() const { return _myEnum; } 64 | bool myBool() const { return _myBool; } 65 | int myInt() const { return _myInt; } 66 | float myFloat() const { return _myFloat; } 67 | double myDouble() const { return _myDouble; } 68 | QString myString() const { return _myString; } 69 | QDateTime myDateTime() const { return _myDateTime; } 70 | QSize mySize() const { return _mySize; } 71 | QSizeF mySizeF() const { return _mySizeF; } 72 | QPoint myPoint() const { return _myPoint; } 73 | QPointF myPointF() const { return _myPointF; } 74 | QRect myRect() const { return _myRect; } 75 | QRectF myRectF() const { return _myRectF; } 76 | 77 | // Property setters. 78 | void setMyEnum(MyEnum myEnum) { _myEnum = myEnum; } 79 | void setMyBool(bool myBool) { _myBool = myBool; } 80 | void setMyInt(int myInt) { _myInt = myInt; } 81 | void setMyFloat(float myFloat) { _myFloat = myFloat; } 82 | void setMyDouble(double myDouble) { _myDouble = myDouble; } 83 | void setMyString(QString myString) { _myString = myString; } 84 | void setMyDateTime(QDateTime myDateTime) { _myDateTime = myDateTime; } 85 | void setMySize(QSize mySize) { _mySize = mySize; } 86 | void setMySizeF(QSizeF mySizeF) { _mySizeF = mySizeF; } 87 | void setMyPoint(QPoint myPoint) { _myPoint = myPoint; } 88 | void setMyPointF(QPointF myPointF) { _myPointF = myPointF; } 89 | void setMyRect(QRect myRect) { _myRect = myRect; } 90 | void setMyRectF(QRectF myRectF) { _myRectF = myRectF; } 91 | 92 | protected: 93 | MyEnum _myEnum; 94 | bool _myBool; 95 | int _myInt; 96 | double _myFloat; 97 | double _myDouble; 98 | QString _myString; 99 | QDateTime _myDateTime; 100 | QSize _mySize; 101 | QSizeF _mySizeF; 102 | QPoint _myPoint; 103 | QPointF _myPointF; 104 | QRect _myRect; 105 | QRectF _myRectF; 106 | }; 107 | 108 | #endif // __test_QtPropertyEditor_H__ 109 | -------------------------------------------------------------------------------- /test/test_QtPropertyEditor.pro: -------------------------------------------------------------------------------- 1 | TARGET = test_QtPropertyEditor 2 | TEMPLATE = app 3 | QT += core gui widgets 4 | CONFIG += c++17 5 | 6 | OBJECTS_DIR = Debug/.obj 7 | MOC_DIR = Debug/.moc 8 | RCC_DIR = Debug/.rcc 9 | UI_DIR = Debug/.ui 10 | 11 | DEFINES += DEBUG 12 | 13 | INCLUDEPATH += .. 14 | 15 | HEADERS += ../QtPropertyEditor.h 16 | SOURCES += ../QtPropertyEditor.cpp 17 | 18 | HEADERS += test_QtPropertyEditor.h 19 | SOURCES += test_QtPropertyEditor.cpp 20 | 21 | # Upgrade for Qt 6 compatibility 22 | QT += core gui widgets 23 | 24 | # Enforce higher C++ standard for modern Qt 25 | CONFIG += c++17 26 | 27 | # Ensure that relative paths work consistently in different build systems 28 | CONFIG += qtquickcompiler 29 | 30 | # Modern build directories (optional change) 31 | OBJECTS_DIR = build/debug/.obj 32 | MOC_DIR = build/debug/.moc 33 | RCC_DIR = build/debug/.rcc 34 | UI_DIR = build/debug/.ui 35 | --------------------------------------------------------------------------------