├── .gitignore ├── CMake ├── BuildProperties.cmake └── CPM.cmake ├── CMakeLists.txt ├── LICENSE ├── QJsonModel.cpp ├── README.md ├── include ├── QJsonModel.hpp └── details │ └── QUtf8.hpp └── screen.png /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Windows 5 | Thumbs.db 6 | ehthumbs.db 7 | 8 | # Folder config file commonly created by Windows Explorer 9 | Desktop.ini 10 | 11 | # Recycle Bin used on file shares and remote volumes 12 | $RECYCLE.BIN/ 13 | 14 | # Windows Installer files 15 | *.cab 16 | *.msi 17 | *.msm 18 | *.msp 19 | 20 | # Windows shortcuts 21 | *.lnk 22 | 23 | # Thumbnail cache files created by Windows 24 | Thumbs.db 25 | Thumbs.db:encryptable 26 | 27 | # Windows Desktop Search 28 | *.pst 29 | *.ost 30 | *.log 31 | 32 | # Compiled Object files, Static and Dynamic libs 33 | *.o 34 | *.lo 35 | *.la 36 | *.a 37 | *.class 38 | *.so 39 | *.lib 40 | *.dll 41 | *.exe 42 | 43 | # Python 44 | __pycache__/ 45 | *.pyc 46 | *.pyo 47 | *.pyd 48 | 49 | # Java 50 | *.class 51 | 52 | # Eclipse 53 | .project 54 | .classpath 55 | .settings/ 56 | 57 | # IntelliJ 58 | .idea/ 59 | 60 | # Visual Studio Code 61 | .vscode/ 62 | .vscodium/ 63 | 64 | # Node.js 65 | node_modules/ 66 | 67 | # Jupyter Notebook 68 | .ipynb_checkpoints/ 69 | 70 | # Thumbnails 71 | Thumbs/ 72 | Thumbs.db 73 | 74 | # macOS metadata 75 | ._* 76 | 77 | # TextMate 78 | *.tmproj 79 | *.tmproject 80 | .tmtags 81 | 82 | # Sublime Text 83 | *.sublime-workspace 84 | *.sublime-project 85 | 86 | # VS Code directories 87 | .vscode/ 88 | 89 | # CodeKit 90 | .codekit-config.json 91 | 92 | # Windows Installer files 93 | *.cab 94 | *.msi 95 | *.msm 96 | *.msp 97 | 98 | # Compiled files 99 | *.com 100 | *.class 101 | *.dll 102 | *.exe 103 | *.o 104 | *.so 105 | 106 | # Logs and databases 107 | *.log 108 | *.sql 109 | *.sqlite 110 | *.sqlite3 111 | *.xml 112 | 113 | # Binary and source packages 114 | *.dmg 115 | *.gz 116 | *.iso 117 | *.jar 118 | *.tar 119 | *.zip 120 | *.rar 121 | *.bin 122 | *.war 123 | *.ear 124 | *.sar 125 | *.bbl 126 | *.pdf 127 | *.xls 128 | *.xlsx 129 | *.ppt 130 | *.pptx 131 | 132 | # Virtual environment 133 | venv/ 134 | env/ 135 | 136 | ### Manually Entered 137 | vim-debug/ 138 | **/out/bin 139 | **/out/lib 140 | **/out/share 141 | _deps 142 | .cache/ 143 | compile_commands.json 144 | *.bak 145 | docs/ 146 | *.old 147 | 148 | # clangd cache 149 | .cache/ 150 | .vim/ 151 | build/ 152 | debug/ 153 | realease/ 154 | Release/ 155 | Debug 156 | -------------------------------------------------------------------------------- /CMake/BuildProperties.cmake: -------------------------------------------------------------------------------- 1 | # BuildProperties.cmake 2 | # Copyright (c) 2024 Saul D Beniquez 3 | # License: MIT 4 | # 5 | # This module defines a function prevent_in_source_build() that prevents in-source builds 6 | # and sets a policy for CMake version 3.24.0 and above. 7 | 8 | function(prevent_in_source_build) 9 | # Prevent in-source builds 10 | if (CMAKE_BINARY_DIR STREQUAL CMAKE_SOURCE_DIR) 11 | message(FATAL_ERROR "Source and build directories cannot be the same.") 12 | endif() 13 | endfunction() 14 | 15 | function(disable_deprecated_features) 16 | # Use new timestamp behavior when extracting files archives 17 | if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") 18 | cmake_policy(SET CMP0135 NEW) 19 | endif() 20 | endfunction() 21 | 22 | function(git_setup_submodules) 23 | find_package(Git QUIET) 24 | if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git") 25 | option(GIT_SUBMODULE "Check submodules during build" ON) 26 | if(GIT_SUBMODULE) 27 | message(STATUS "Git submodule update") 28 | execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive 29 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 30 | RESULT_VARIABLE GIT_SUBMOD_RESULT) 31 | if(NOT GIT_SUBMOD_RESULT EQUAL "0") 32 | message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") 33 | endif() 34 | endif() 35 | endif() 36 | endfunction() 37 | 38 | function(set_artifact_dir path) 39 | # Set local variable, not necessary to be parent scope since it's not used outside this function 40 | set(ARTIFACT_DIR "${path}") 41 | 42 | # Set project-specific artifact directory in parent scope 43 | set(${PROJECT_NAME}_ARTIFACT_DIR "${path}" PARENT_SCOPE) 44 | 45 | # Set output directories in parent scope using the provided path directly 46 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${path}/lib" PARENT_SCOPE) 47 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${path}/lib" PARENT_SCOPE) 48 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${path}/bin" PARENT_SCOPE) 49 | endfunction() 50 | 51 | function(disable_tests_if_subproject) 52 | if (NOT DEFINED PROJECT_NAME) 53 | option(BUILD_TESTING "Build and run unit tests" ON) 54 | else() 55 | option(BUILD_TESTING "Build and run unit tests" OFF) 56 | endif() 57 | endfunction() 58 | 59 | function(use_ccache) 60 | option(USE_CCACHE 61 | [=[Use ccache compiler cache to speed up builds. 62 | Enabled by default if ccache is found]=] 63 | ON 64 | ) 65 | # Search for the code caching compiler wrapper, ccache and enable it 66 | # if found. This will speed up repeated builds. 67 | if (USE_CCACHE) 68 | message(CHECK_START "Detecting cacche") 69 | 70 | find_program(CCACHE_PATH ccache) 71 | if(CCACHE_PATH) 72 | message(CHECK_PASS("found")) 73 | set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE_PATH}) 74 | set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_PATH}) 75 | endif() 76 | 77 | list(APPEND CMAKE_MESSAGE_INDENT " ") 78 | message(STATUS "(set -DUSE_CCACHE=Off to disable)") 79 | list(POP_BACK CMAKE_MESSAGE_INDENT) 80 | endif() 81 | 82 | endfunction() 83 | 84 | function(detect_linkers) 85 | option(USE_MOLD "Use the mold/sold parallel linker for faster builds" OFF) 86 | if(USE_MOLD) 87 | # Determine if the compiler is GCC or Clang 88 | if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") 89 | message(STATUS "Detected GCC/Clang, checking for mold/sold linker...") 90 | 91 | # Check for mold linker on general systems and ld64.mold on macOS 92 | if(APPLE) 93 | find_program(MOLD_LINKER ld64.mold) 94 | set(CMAKE_LINKER_TYPE SOLD) 95 | else() 96 | find_program(MOLD_LINKER mold) 97 | set(CMAKE_LINKER_TYPE MOLD) 98 | endif() 99 | 100 | if(MOLD_LINKER) 101 | message(STATUS "LINKER_TYPE set to ${CMAKE_LINKER_TYPE} for faster builds") 102 | list(APPEND CMAKE_MESSAGE_INDENT " ") 103 | message(STATUS "(set -DUSE_MOLD=OFF to disable)") 104 | list(POP_BACK CMAKE_MESSAGE_INDENT) 105 | else() 106 | message(STATUS " -- No suitable mold linker found. Using default linker.") 107 | endif() 108 | else() 109 | message(STATUS "Compiler is neither GCC nor Clang. Skipping mold linker check.") 110 | endif() 111 | endif() 112 | endfunction() 113 | 114 | # vim: ts=4 sts=4 sw=4 noet foldmethod=indent : 115 | -------------------------------------------------------------------------------- /CMake/CPM.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | # 3 | # SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors 4 | 5 | set(CPM_DOWNLOAD_VERSION 0.38.7) 6 | set(CPM_HASH_SUM "83e5eb71b2bbb8b1f2ad38f1950287a057624e385c238f6087f94cdfc44af9c5") 7 | 8 | if(CPM_SOURCE_CACHE) 9 | set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 10 | elseif(DEFINED ENV{CPM_SOURCE_CACHE}) 11 | set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 12 | else() 13 | set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 14 | endif() 15 | 16 | # Expand relative path. This is important if the provided path contains a tilde (~) 17 | get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) 18 | 19 | file(DOWNLOAD 20 | https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake 21 | ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} 22 | ) 23 | 24 | include(${CPM_DOWNLOAD_LOCATION}) 25 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.26) 2 | project( 3 | Qt6JsonModel 4 | VERSION 0.0.2 5 | LANGUAGES CXX 6 | DESCRIPTION 7 | "QJsonModel is a json tree model class for Qt6/C++17 based on QAbstractItemModel. MIT License." 8 | ) 9 | 10 | set(CMAKE_CXX_STANDARD 17) 11 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 12 | set(CMAKE_CXX_EXTENSIONS OFF) 13 | 14 | find_package(Qt6 REQUIRED COMPONENTS Core Widgets Gui) 15 | qt_standard_project_setup() 16 | qt_add_library(QJsonModel OBJECT QJsonModel.cpp include/QJsonModel.hpp) 17 | add_library(Qt6::QJsonModel ALIAS QJsonModel) 18 | target_link_libraries(QJsonModel PUBLIC Qt6::Core Qt6::Gui Qt6::Widgets) 19 | target_include_directories(QJsonModel PUBLIC include) 20 | 21 | add_library(QJsonModelStatic STATIC) 22 | add_library(Qt6::QJsonModelStatic ALIAS QJsonModelStatic) 23 | if(WIN32) 24 | set_target_properties(QJsonModelStatic PROPERTIES OUTPUT_NAME "QJsonModelLIB") 25 | else() 26 | set_target_properties(QJsonModelStatic PROPERTIES OUTPUT_NAME "QJsonModel") 27 | endif() 28 | target_link_libraries(QJsonModelStatic PUBLIC QJsonModel) 29 | 30 | add_library(QJsonModelShared SHARED) 31 | add_library(Qt6::QJsonModelShared ALIAS QJsonModelShared) 32 | set_target_properties(QJsonModelShared PROPERTIES OUTPUT_NAME "QJsonModel") 33 | target_link_libraries(QJsonModelShared PUBLIC QJsonModel) 34 | 35 | # vim: ts=2 sw=2 noet foldmethod=indent : 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 sacha schutz 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 | -------------------------------------------------------------------------------- /QJsonModel.cpp: -------------------------------------------------------------------------------- 1 | /* QJsonModel.cpp 2 | * Copyright (c) 2011 SCHUTZ Sacha 3 | * Copyright © 2024 Saul D. Beniquez 4 | * 5 | * License: 6 | * The MIT License (MIT) 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | // NOLINTBEGIN 27 | 28 | #include "QJsonModel.hpp" 29 | #include 30 | #include 31 | #include 32 | 33 | inline bool contains(const QStringList &list, const QString &value) { 34 | for (auto val : list) 35 | if (value.contains(val, Qt::CaseInsensitive)) 36 | return true; 37 | 38 | return false; 39 | } 40 | 41 | QJsonTreeItem::QJsonTreeItem(QJsonTreeItem *parent) { mParent = parent; } 42 | 43 | QJsonTreeItem::~QJsonTreeItem() { qDeleteAll(mChilds); } 44 | 45 | void QJsonTreeItem::appendChild(QJsonTreeItem *item) { mChilds.append(item); } 46 | 47 | QJsonTreeItem *QJsonTreeItem::child(int row) { return mChilds.value(row); } 48 | 49 | QJsonTreeItem *QJsonTreeItem::parent() { return mParent; } 50 | 51 | int QJsonTreeItem::childCount() const { return mChilds.count(); } 52 | 53 | int QJsonTreeItem::row() const { 54 | if (mParent) 55 | return mParent->mChilds.indexOf(const_cast(this)); 56 | 57 | return 0; 58 | } 59 | 60 | void QJsonTreeItem::setKey(const QString &key) { mKey = key; } 61 | 62 | void QJsonTreeItem::setValue(const QVariant &value) { mValue = value; } 63 | 64 | void QJsonTreeItem::setType(const QJsonValue::Type &type) { mType = type; } 65 | 66 | QString QJsonTreeItem::key() const { return mKey; } 67 | 68 | QVariant QJsonTreeItem::value() const { return mValue; } 69 | 70 | QJsonValue::Type QJsonTreeItem::type() const { return mType; } 71 | 72 | QJsonTreeItem *QJsonTreeItem::load(const QJsonValue &value, 73 | const QStringList &exceptions, 74 | QJsonTreeItem *parent) { 75 | QJsonTreeItem *rootItem = new QJsonTreeItem(parent); 76 | rootItem->setKey("root"); 77 | 78 | if (value.isObject()) { 79 | // Get all QJsonValue childs 80 | const QStringList keys = 81 | value.toObject().keys(); // To prevent clazy-range warning 82 | for (const QString &key : keys) { 83 | if (contains(exceptions, key)) { 84 | continue; 85 | } 86 | QJsonValue v = value.toObject().value(key); 87 | QJsonTreeItem *child = load(v, exceptions, rootItem); 88 | child->setKey(key); 89 | child->setType(v.type()); 90 | rootItem->appendChild(child); 91 | } 92 | } else if (value.isArray()) { 93 | // Get all QJsonValue childs 94 | int index = 0; 95 | const QJsonArray array = value.toArray(); // To prevent clazy-range warning 96 | for (const QJsonValue &v : array) { 97 | QJsonTreeItem *child = load(v, exceptions, rootItem); 98 | child->setKey(QString::number(index)); 99 | child->setType(v.type()); 100 | rootItem->appendChild(child); 101 | ++index; 102 | } 103 | } else { 104 | rootItem->setValue(value.toVariant()); 105 | rootItem->setType(value.type()); 106 | } 107 | 108 | return rootItem; 109 | } 110 | 111 | //========================================================================= 112 | 113 | inline uchar hexdig(uint u) { return (u < 0xa ? '0' + u : 'a' + u - 0xa); } 114 | 115 | QByteArray escapedString(const QString &s) { 116 | QByteArray ba(s.length(), Qt::Uninitialized); 117 | uchar *cursor = reinterpret_cast(const_cast(ba.constData())); 118 | const uchar *ba_end = cursor + ba.length(); 119 | const ushort *src = reinterpret_cast(s.constBegin()); 120 | const ushort *const end = reinterpret_cast(s.constEnd()); 121 | while (src != end) { 122 | if (cursor >= ba_end - 6) { 123 | // ensure we have enough space 124 | int pos = cursor - reinterpret_cast(ba.constData()); 125 | ba.resize(ba.size() * 2); 126 | cursor = reinterpret_cast(ba.data()) + pos; 127 | ba_end = reinterpret_cast(ba.constData()) + ba.length(); 128 | } 129 | uint u = *src++; 130 | if (u < 0x80) { 131 | if (u < 0x20 || u == 0x22 || u == 0x5c) { 132 | *cursor++ = '\\'; 133 | switch (u) { 134 | case 0x22: 135 | *cursor++ = '"'; 136 | break; 137 | case 0x5c: 138 | *cursor++ = '\\'; 139 | break; 140 | case 0x8: 141 | *cursor++ = 'b'; 142 | break; 143 | case 0xc: 144 | *cursor++ = 'f'; 145 | break; 146 | case 0xa: 147 | *cursor++ = 'n'; 148 | break; 149 | case 0xd: 150 | *cursor++ = 'r'; 151 | break; 152 | case 0x9: 153 | *cursor++ = 't'; 154 | break; 155 | default: 156 | *cursor++ = 'u'; 157 | *cursor++ = '0'; 158 | *cursor++ = '0'; 159 | *cursor++ = hexdig(u >> 4); 160 | *cursor++ = hexdig(u & 0xf); 161 | } 162 | } else { 163 | *cursor++ = (uchar)u; 164 | } 165 | } else if (QUtf8Functions::toUtf8(u, cursor, src, end) < 166 | 0) { 167 | // failed to get valid utf8 use JSON escape sequence 168 | *cursor++ = '\\'; 169 | *cursor++ = 'u'; 170 | *cursor++ = hexdig(u >> 12 & 0x0f); 171 | *cursor++ = hexdig(u >> 8 & 0x0f); 172 | *cursor++ = hexdig(u >> 4 & 0x0f); 173 | *cursor++ = hexdig(u & 0x0f); 174 | } 175 | } 176 | ba.resize(cursor - reinterpret_cast(ba.constData())); 177 | return ba; 178 | } 179 | 180 | QJsonModel::QJsonModel(QObject *parent) 181 | : QAbstractItemModel(parent), mRootItem{new QJsonTreeItem} { 182 | mHeaders.append("key"); 183 | mHeaders.append("value"); 184 | } 185 | 186 | QJsonModel::QJsonModel(const QString &fileName, QObject *parent) 187 | : QAbstractItemModel(parent), mRootItem{new QJsonTreeItem} { 188 | mHeaders.append("key"); 189 | mHeaders.append("value"); 190 | load(fileName); 191 | } 192 | 193 | QJsonModel::QJsonModel(QIODevice *device, QObject *parent) 194 | : QAbstractItemModel(parent), mRootItem{new QJsonTreeItem} { 195 | mHeaders.append("key"); 196 | mHeaders.append("value"); 197 | load(device); 198 | } 199 | 200 | QJsonModel::QJsonModel(const QByteArray &json, QObject *parent) 201 | : QAbstractItemModel(parent), mRootItem{new QJsonTreeItem} { 202 | mHeaders.append("key"); 203 | mHeaders.append("value"); 204 | loadJson(json); 205 | } 206 | 207 | QJsonModel::~QJsonModel() { delete mRootItem; } 208 | 209 | bool QJsonModel::load(const QString &fileName) { 210 | QFile file(fileName); 211 | bool success = false; 212 | if (file.open(QIODevice::ReadOnly)) { 213 | success = load(&file); 214 | file.close(); 215 | } else { 216 | success = false; 217 | } 218 | 219 | return success; 220 | } 221 | 222 | bool QJsonModel::load(QIODevice *device) { return loadJson(device->readAll()); } 223 | 224 | bool QJsonModel::loadJson(const QByteArray &json) { 225 | auto const &jdoc = QJsonDocument::fromJson(json); 226 | 227 | if (!jdoc.isNull()) { 228 | beginResetModel(); 229 | delete mRootItem; 230 | if (jdoc.isArray()) { 231 | mRootItem = QJsonTreeItem::load(QJsonValue(jdoc.array()), mExceptions); 232 | mRootItem->setType(QJsonValue::Array); 233 | 234 | } else { 235 | mRootItem = QJsonTreeItem::load(QJsonValue(jdoc.object()), mExceptions); 236 | mRootItem->setType(QJsonValue::Object); 237 | } 238 | endResetModel(); 239 | return true; 240 | } 241 | 242 | qDebug() << Q_FUNC_INFO << "cannot load json"; 243 | return false; 244 | } 245 | 246 | QVariant QJsonModel::data(const QModelIndex &index, int role) const { 247 | if (!index.isValid()) 248 | return {}; 249 | 250 | QJsonTreeItem *item = static_cast(index.internalPointer()); 251 | 252 | if (role == Qt::DisplayRole) { 253 | if (index.column() == 0) 254 | return QString("%1").arg(item->key()); 255 | 256 | if (index.column() == 1) 257 | return item->value(); 258 | } else if (Qt::EditRole == role) { 259 | if (index.column() == 1) 260 | return item->value(); 261 | } 262 | 263 | return {}; 264 | } 265 | 266 | bool QJsonModel::setData(const QModelIndex &index, const QVariant &value, 267 | int role) { 268 | int col = index.column(); 269 | if (Qt::EditRole == role) { 270 | if (col == 1) { 271 | QJsonTreeItem *item = 272 | static_cast(index.internalPointer()); 273 | item->setValue(value); 274 | emit dataChanged(index, index, {Qt::EditRole}); 275 | return true; 276 | } 277 | } 278 | 279 | return false; 280 | } 281 | 282 | QVariant QJsonModel::headerData(int section, Qt::Orientation orientation, 283 | int role) const { 284 | if (role != Qt::DisplayRole) 285 | return {}; 286 | 287 | if (orientation == Qt::Horizontal) 288 | return mHeaders.value(section); 289 | else 290 | return {}; 291 | } 292 | 293 | QModelIndex QJsonModel::index(int row, int column, 294 | const QModelIndex &parent) const { 295 | if (!hasIndex(row, column, parent)) 296 | return {}; 297 | 298 | QJsonTreeItem *parentItem; 299 | 300 | if (!parent.isValid()) 301 | parentItem = mRootItem; 302 | else 303 | parentItem = static_cast(parent.internalPointer()); 304 | 305 | QJsonTreeItem *childItem = parentItem->child(row); 306 | if (childItem) 307 | return createIndex(row, column, childItem); 308 | else 309 | return {}; 310 | } 311 | 312 | QModelIndex QJsonModel::parent(const QModelIndex &index) const { 313 | if (!index.isValid()) 314 | return {}; 315 | 316 | QJsonTreeItem *childItem = 317 | static_cast(index.internalPointer()); 318 | QJsonTreeItem *parentItem = childItem->parent(); 319 | 320 | if (parentItem == mRootItem) 321 | return QModelIndex(); 322 | 323 | return createIndex(parentItem->row(), 0, parentItem); 324 | } 325 | 326 | int QJsonModel::rowCount(const QModelIndex &parent) const { 327 | QJsonTreeItem *parentItem; 328 | if (parent.column() > 0) 329 | return 0; 330 | 331 | if (!parent.isValid()) 332 | parentItem = mRootItem; 333 | else 334 | parentItem = static_cast(parent.internalPointer()); 335 | 336 | return parentItem->childCount(); 337 | } 338 | 339 | int QJsonModel::columnCount(const QModelIndex &parent) const { 340 | Q_UNUSED(parent) 341 | return 2; 342 | } 343 | 344 | Qt::ItemFlags QJsonModel::flags(const QModelIndex &index) const { 345 | int col = index.column(); 346 | auto item = static_cast(index.internalPointer()); 347 | 348 | auto isArray = QJsonValue::Array == item->type(); 349 | auto isObject = QJsonValue::Object == item->type(); 350 | 351 | if ((col == 1) && !(isArray || isObject)) 352 | return Qt::ItemIsEditable | QAbstractItemModel::flags(index); 353 | else 354 | return QAbstractItemModel::flags(index); 355 | } 356 | 357 | QByteArray QJsonModel::json(bool compact) { 358 | auto jsonValue = genJson(mRootItem); 359 | QByteArray json; 360 | if (jsonValue.isNull()) 361 | return json; 362 | 363 | if (jsonValue.isArray()) 364 | arrayToJson(jsonValue.toArray(), json, 0, compact); 365 | else 366 | objectToJson(jsonValue.toObject(), json, 0, compact); 367 | 368 | return json; 369 | } 370 | 371 | void QJsonModel::objectToJson(QJsonObject jsonObject, QByteArray &json, 372 | int indent, bool compact) { 373 | json += compact ? "{" : "{\n"; 374 | objectContentToJson(jsonObject, json, indent + (compact ? 0 : 1), compact); 375 | json += QByteArray(4 * indent, ' '); 376 | json += compact ? "}" : "}\n"; 377 | } 378 | void QJsonModel::arrayToJson(QJsonArray jsonArray, QByteArray &json, int indent, 379 | bool compact) { 380 | json += compact ? "[" : "[\n"; 381 | arrayContentToJson(jsonArray, json, indent + (compact ? 0 : 1), compact); 382 | json += QByteArray(4 * indent, ' '); 383 | json += compact ? "]" : "]\n"; 384 | } 385 | 386 | void QJsonModel::arrayContentToJson(QJsonArray jsonArray, QByteArray &json, 387 | int indent, bool compact) { 388 | if (jsonArray.size() <= 0) 389 | return; 390 | 391 | QByteArray indentString(4 * indent, ' '); 392 | int i = 0; 393 | while (1) { 394 | json += indentString; 395 | valueToJson(jsonArray.at(i), json, indent, compact); 396 | if (++i == jsonArray.size()) { 397 | if (!compact) 398 | json += '\n'; 399 | break; 400 | } 401 | json += compact ? "," : ",\n"; 402 | } 403 | } 404 | void QJsonModel::objectContentToJson(QJsonObject jsonObject, QByteArray &json, 405 | int indent, bool compact) { 406 | if (jsonObject.size() <= 0) 407 | return; 408 | 409 | QByteArray indentString(4 * indent, ' '); 410 | int i = 0; 411 | while (1) { 412 | QString key = jsonObject.keys().at(i); 413 | json += indentString; 414 | json += '"'; 415 | json += escapedString(key); 416 | json += compact ? "\":" : "\": "; 417 | valueToJson(jsonObject.value(key), json, indent, compact); 418 | if (++i == jsonObject.size()) { 419 | if (!compact) 420 | json += '\n'; 421 | break; 422 | } 423 | json += compact ? "," : ",\n"; 424 | } 425 | } 426 | 427 | void QJsonModel::valueToJson(QJsonValue jsonValue, QByteArray &json, int indent, 428 | bool compact) { 429 | QJsonValue::Type type = jsonValue.type(); 430 | switch (type) { 431 | case QJsonValue::Bool: 432 | json += jsonValue.toBool() ? "true" : "false"; 433 | break; 434 | case QJsonValue::Double: { 435 | const double d = jsonValue.toDouble(); 436 | if (qIsFinite(d)) { 437 | json += QByteArray::number(d, 'f', QLocale::FloatingPointShortest); 438 | } else { 439 | json += "null"; // +INF || -INF || NaN (see RFC4627#section2.4) 440 | } 441 | break; 442 | } 443 | case QJsonValue::String: 444 | json += '"'; 445 | json += escapedString(jsonValue.toString()); 446 | json += '"'; 447 | break; 448 | case QJsonValue::Array: 449 | json += compact ? "[" : "[\n"; 450 | arrayContentToJson(jsonValue.toArray(), json, indent + (compact ? 0 : 1), 451 | compact); 452 | json += QByteArray(4 * indent, ' '); 453 | json += ']'; 454 | break; 455 | case QJsonValue::Object: 456 | json += compact ? "{" : "{\n"; 457 | objectContentToJson(jsonValue.toObject(), json, indent + (compact ? 0 : 1), 458 | compact); 459 | json += QByteArray(4 * indent, ' '); 460 | json += '}'; 461 | break; 462 | case QJsonValue::Null: 463 | default: 464 | json += "null"; 465 | } 466 | } 467 | 468 | void QJsonModel::addException(const QStringList &exceptions) { 469 | mExceptions = exceptions; 470 | } 471 | 472 | QJsonValue QJsonModel::genJson(QJsonTreeItem *item) const { 473 | auto type = item->type(); 474 | int nchild = item->childCount(); 475 | 476 | if (QJsonValue::Object == type) { 477 | QJsonObject jo; 478 | for (int i = 0; i < nchild; ++i) { 479 | auto ch = item->child(i); 480 | auto key = ch->key(); 481 | jo.insert(key, genJson(ch)); 482 | } 483 | return jo; 484 | } else if (QJsonValue::Array == type) { 485 | QJsonArray arr; 486 | for (int i = 0; i < nchild; ++i) { 487 | auto ch = item->child(i); 488 | arr.append(genJson(ch)); 489 | } 490 | return arr; 491 | } else { 492 | QJsonValue va; 493 | switch (item->value().typeId()) { 494 | case QMetaType::Bool: { 495 | va = item->value().toBool(); 496 | break; 497 | } 498 | default: 499 | va = item->value().toString(); 500 | break; 501 | } 502 | (item->value()); 503 | return va; 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QJsonModel 2 | QJsonModel is a JSON tree model class for Qt6/C++17 based on QAbstractItemModel. 3 | 4 | QJsonModel was originally written by Sacha Shutz (https://github.com/dridk). 5 | 6 | This fork is also released under the MIT License. 7 | 8 | 9 | ![QJsonModel](https://gitea.beniquez.me/sdaveb/QJsonModel/raw/branch/master/screen.png) 10 | 11 | ## Build Instructions 12 | 13 | ### Build Tools 14 | - CMake (version 3.21 or higher) 15 | - C++17-compatible compiler 16 | 17 | ### Building the Project 18 | 1. Clone the repository: 19 | ``` 20 | git clone 21 | ``` 22 | 23 | 2. Navigate to the project directory: 24 | ``` 25 | cd elemental-game 26 | ``` 27 | 3. Configure your build system: 28 | ```bash 29 | cmake -B debug -G Unix Makefiles 30 | # or 31 | cmake -B debug -G Ninja # this is faster and more modern 32 | ``` 33 | 4. Invoke your build system 34 | ``` 35 | cmake --build debug 36 | ``` 37 | ### Usage - CMake 38 | 39 | You can add this library to your CMake projects using FetchContent() 40 | or CPM_AddPackage(). 41 | 42 | Here's how to do it with CPM_AddPackage: 43 | 44 | ``` 45 | COMING SOON 46 | ``` 47 | 48 | ### Usage - C++ 49 | 50 | #### 51 | ```cpp 52 | QJsonModel * model = new QJsonModel; 53 | QTreeView * view = new QTreeView; 54 | view->setModel(model); 55 | model->load("example.json") 56 | ``` 57 | -------------------------------------------------------------------------------- /include/QJsonModel.hpp: -------------------------------------------------------------------------------- 1 | /* QJsonModel.hpp 2 | * Copyright (c) 2011 SCHUTZ Sacha 3 | * Copyright © 2024 Saul D. Beniquez 4 | * 5 | * License: 6 | * The MIT License (MIT) 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | #pragma once 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | #include "details/QUtf8.hpp" 36 | 37 | class QJsonModel; 38 | class QJsonItem; 39 | 40 | class QJsonTreeItem { 41 | public: 42 | QJsonTreeItem(QJsonTreeItem *parent = nullptr); 43 | ~QJsonTreeItem(); 44 | void appendChild(QJsonTreeItem *item); 45 | QJsonTreeItem *child(int row); 46 | QJsonTreeItem *parent(); 47 | int childCount() const; 48 | int row() const; 49 | void setKey(const QString &key); 50 | void setValue(const QVariant &value); 51 | void setType(const QJsonValue::Type &type); 52 | QString key() const; 53 | QVariant value() const; 54 | QJsonValue::Type type() const; 55 | 56 | static QJsonTreeItem *load(const QJsonValue &value, 57 | const QStringList &exceptions = {}, 58 | QJsonTreeItem *parent = nullptr); 59 | 60 | protected: 61 | private: 62 | QString mKey; 63 | QVariant mValue; 64 | QJsonValue::Type mType; 65 | QList mChilds; 66 | QJsonTreeItem *mParent = nullptr; 67 | }; 68 | 69 | //--------------------------------------------------- 70 | 71 | class QJsonModel : public QAbstractItemModel { 72 | Q_OBJECT 73 | public: 74 | explicit QJsonModel(QObject *parent = nullptr); 75 | QJsonModel(const QString &fileName, QObject *parent = nullptr); 76 | QJsonModel(QIODevice *device, QObject *parent = nullptr); 77 | QJsonModel(const QByteArray &json, QObject *parent = nullptr); 78 | ~QJsonModel(); 79 | bool load(const QString &fileName); 80 | bool load(QIODevice *device); 81 | bool loadJson(const QByteArray &json); 82 | QVariant data(const QModelIndex &index, int role) const override; 83 | bool setData(const QModelIndex &index, const QVariant &value, 84 | int role = Qt::EditRole) override; 85 | QVariant headerData(int section, Qt::Orientation orientation, 86 | int role) const override; 87 | QModelIndex index(int row, int column, 88 | const QModelIndex &parent = QModelIndex()) const override; 89 | QModelIndex parent(const QModelIndex &index) const override; 90 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 91 | int columnCount(const QModelIndex &parent = QModelIndex()) const override; 92 | Qt::ItemFlags flags(const QModelIndex &index) const override; 93 | QByteArray json(bool compact = false); 94 | QByteArray jsonToByte(QJsonValue jsonValue); 95 | void objectToJson(QJsonObject jsonObject, QByteArray &json, int indent, 96 | bool compact); 97 | void arrayToJson(QJsonArray jsonArray, QByteArray &json, int indent, 98 | bool compact); 99 | void arrayContentToJson(QJsonArray jsonArray, QByteArray &json, int indent, 100 | bool compact); 101 | void objectContentToJson(QJsonObject jsonObject, QByteArray &json, int indent, 102 | bool compact); 103 | void valueToJson(QJsonValue jsonValue, QByteArray &json, int indent, 104 | bool compact); 105 | //! List of tags to skip during JSON parsing 106 | void addException(const QStringList &exceptions); 107 | 108 | private: 109 | QJsonValue genJson(QJsonTreeItem *) const; 110 | QJsonTreeItem *mRootItem = nullptr; 111 | QStringList mHeaders; 112 | //! List of exceptions (e.g. comments). Case insensitive, compairs on 113 | //! "contains". 114 | QStringList mExceptions; 115 | }; 116 | -------------------------------------------------------------------------------- /include/details/QUtf8.hpp: -------------------------------------------------------------------------------- 1 | /* QUtf8.hpp 2 | * Copyright © 2024 Saul D. Beniquez 3 | * License: 4 | * The MIT License (MIT) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the "Software"), 8 | * to deal in the Software without restriction, including without limitation 9 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | * and/or sell copies of the Software, and to permit persons to whom the 11 | * Software is furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included 14 | * in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 20 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 22 | * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | #include 26 | 27 | namespace QUtf8Functions { 28 | /// returns 0 on success; errors can only happen if \a u is a surrogate: 29 | /// Error if \a u is a low surrogate; 30 | /// if \a u is a high surrogate, Error if the next isn't a low one, 31 | /// EndOfString if we run into the end of the string. 32 | template 33 | inline int toUtf8(ushort u, OutputPtr &dst, InputPtr &src, InputPtr end) { 34 | if (!Traits::skipAsciiHandling && u < 0x80) { 35 | // U+0000 to U+007F (US-ASCII) - one byte 36 | Traits::appendByte(dst, uchar(u)); 37 | return 0; 38 | } else if (u < 0x0800) { 39 | // U+0080 to U+07FF - two bytes 40 | // first of two bytes 41 | Traits::appendByte(dst, 0xc0 | uchar(u >> 6)); 42 | } else { 43 | if (!QChar::isSurrogate(u)) { 44 | // U+0800 to U+FFFF (except U+D800-U+DFFF) - three bytes 45 | if (!Traits::allowNonCharacters && QChar::isNonCharacter(u)) 46 | return Traits::Error; 47 | // first of three bytes 48 | Traits::appendByte(dst, 0xe0 | uchar(u >> 12)); 49 | } else { 50 | // U+10000 to U+10FFFF - four bytes 51 | // need to get one extra codepoint 52 | if (Traits::availableUtf16(src, end) == 0) 53 | return Traits::EndOfString; 54 | ushort low = Traits::peekUtf16(src); 55 | if (!QChar::isHighSurrogate(u)) 56 | return Traits::Error; 57 | if (!QChar::isLowSurrogate(low)) 58 | return Traits::Error; 59 | Traits::advanceUtf16(src); 60 | uint ucs4 = QChar::surrogateToUcs4(u, low); 61 | if (!Traits::allowNonCharacters && QChar::isNonCharacter(ucs4)) 62 | return Traits::Error; 63 | // first byte 64 | Traits::appendByte(dst, 0xf0 | (uchar(ucs4 >> 18) & 0xf)); 65 | // second of four bytes 66 | Traits::appendByte(dst, 0x80 | (uchar(ucs4 >> 12) & 0x3f)); 67 | // for the rest of the bytes 68 | u = ushort(ucs4); 69 | } 70 | // second to last byte 71 | Traits::appendByte(dst, 0x80 | (uchar(u >> 6) & 0x3f)); 72 | } 73 | // last byte 74 | Traits::appendByte(dst, 0x80 | (u & 0x3f)); 75 | return 0; 76 | } 77 | inline bool isContinuationByte(uchar b) { return (b & 0xc0) == 0x80; } 78 | /// returns the number of characters consumed (including \a b) in case of 79 | /// success; returns negative in case of error: Traits::Error or 80 | /// Traits::EndOfString 81 | template 82 | inline int fromUtf8(uchar b, OutputPtr &dst, InputPtr &src, InputPtr end) { 83 | int charsNeeded; 84 | uint min_uc; 85 | uint uc; 86 | if (!Traits::skipAsciiHandling && b < 0x80) { 87 | // US-ASCII 88 | Traits::appendUtf16(dst, b); 89 | return 1; 90 | } 91 | if (!Traits::isTrusted && Q_UNLIKELY(b <= 0xC1)) { 92 | // an UTF-8 first character must be at least 0xC0 93 | // however, all 0xC0 and 0xC1 first bytes can only produce overlong 94 | // sequences 95 | return Traits::Error; 96 | } else if (b < 0xe0) { 97 | charsNeeded = 2; 98 | min_uc = 0x80; 99 | uc = b & 0x1f; 100 | } else if (b < 0xf0) { 101 | charsNeeded = 3; 102 | min_uc = 0x800; 103 | uc = b & 0x0f; 104 | } else if (b < 0xf5) { 105 | charsNeeded = 4; 106 | min_uc = 0x10000; 107 | uc = b & 0x07; 108 | } else { 109 | // the last Unicode character is U+10FFFF 110 | // it's encoded in UTF-8 as "\xF4\x8F\xBF\xBF" 111 | // therefore, a byte higher than 0xF4 is not the UTF-8 first byte 112 | return Traits::Error; 113 | } 114 | int bytesAvailable = Traits::availableBytes(src, end); 115 | if (Q_UNLIKELY(bytesAvailable < charsNeeded - 1)) { 116 | // it's possible that we have an error instead of just unfinished bytes 117 | if (bytesAvailable > 0 && !isContinuationByte(Traits::peekByte(src, 0))) 118 | return Traits::Error; 119 | if (bytesAvailable > 1 && !isContinuationByte(Traits::peekByte(src, 1))) 120 | return Traits::Error; 121 | return Traits::EndOfString; 122 | } 123 | // first continuation character 124 | b = Traits::peekByte(src, 0); 125 | if (!isContinuationByte(b)) 126 | return Traits::Error; 127 | uc <<= 6; 128 | uc |= b & 0x3f; 129 | if (charsNeeded > 2) { 130 | // second continuation character 131 | b = Traits::peekByte(src, 1); 132 | if (!isContinuationByte(b)) 133 | return Traits::Error; 134 | uc <<= 6; 135 | uc |= b & 0x3f; 136 | if (charsNeeded > 3) { 137 | // third continuation character 138 | b = Traits::peekByte(src, 2); 139 | if (!isContinuationByte(b)) 140 | return Traits::Error; 141 | uc <<= 6; 142 | uc |= b & 0x3f; 143 | } 144 | } 145 | // we've decoded something; safety-check it 146 | if (!Traits::isTrusted) { 147 | if (uc < min_uc) 148 | return Traits::Error; 149 | if (QChar::isSurrogate(uc) || uc > QChar::LastValidCodePoint) 150 | return Traits::Error; 151 | if (!Traits::allowNonCharacters && QChar::isNonCharacter(uc)) 152 | return Traits::Error; 153 | } 154 | // write the UTF-16 sequence 155 | if (!QChar::requiresSurrogates(uc)) { 156 | // UTF-8 decoded and no surrogates are required 157 | // detach if necessary 158 | Traits::appendUtf16(dst, ushort(uc)); 159 | } else { 160 | // UTF-8 decoded to something that requires a surrogate pair 161 | Traits::appendUcs4(dst, uc); 162 | } 163 | Traits::advanceByte(src, charsNeeded - 1); 164 | return charsNeeded; 165 | } 166 | } // namespace QUtf8Functions 167 | 168 | struct QUtf8BaseTraits { 169 | static const bool isTrusted = false; 170 | static const bool allowNonCharacters = true; 171 | static const bool skipAsciiHandling = false; 172 | static const int Error = -1; 173 | static const int EndOfString = -2; 174 | static bool isValidCharacter(uint u) { return int(u) >= 0; } 175 | static void appendByte(uchar *&ptr, uchar b) { *ptr++ = b; } 176 | static uchar peekByte(const uchar *ptr, int n = 0) { return ptr[n]; } 177 | static qptrdiff availableBytes(const uchar *ptr, const uchar *end) { 178 | return end - ptr; 179 | } 180 | static void advanceByte(const uchar *&ptr, int n = 1) { ptr += n; } 181 | static void appendUtf16(ushort *&ptr, ushort uc) { *ptr++ = uc; } 182 | static void appendUcs4(ushort *&ptr, uint uc) { 183 | appendUtf16(ptr, QChar::highSurrogate(uc)); 184 | appendUtf16(ptr, QChar::lowSurrogate(uc)); 185 | } 186 | static ushort peekUtf16(const ushort *ptr, int n = 0) { return ptr[n]; } 187 | static qptrdiff availableUtf16(const ushort *ptr, const ushort *end) { 188 | return end - ptr; 189 | } 190 | static void advanceUtf16(const ushort *&ptr, int n = 1) { ptr += n; } 191 | // it's possible to output to UCS-4 too 192 | static void appendUtf16(uint *&ptr, ushort uc) { *ptr++ = uc; } 193 | static void appendUcs4(uint *&ptr, uint uc) { *ptr++ = uc; } 194 | }; 195 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dridk/QJsonModel/f5fa5988c0ee52fff39a42973c964d5480951f86/screen.png --------------------------------------------------------------------------------