diff --git a/.gitignore b/.gitignore index b587a14..c45a326 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,155 @@ -# Compiled Object files -*.slo -*.lo +# macOS +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db + +# Folder config file commonly created by Windows Explorer +Desktop.ini + +# Recycle Bin used on file shares and remote volumes +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Thumbnail cache files created by Windows +Thumbs.db +Thumbs.db:encryptable + +# Windows Desktop Search +*.pst +*.ost +*.log + +# Compiled Object files, Static and Dynamic libs *.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod - -# Compiled Static libraries -*.lai +*.lo *.la *.a +*.class +*.so *.lib - -# Executables +*.dll *.exe -*.out -*.app -# Qt -*.pro.user +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +# Java +*.class + +# Eclipse +.project +.classpath +.settings/ + +# IntelliJ +.idea/ + +# Visual Studio Code +.vscode/ +.vscodium/ + +# Node.js +node_modules/ + +# Jupyter Notebook +.ipynb_checkpoints/ + +# Thumbnails +Thumbs/ +Thumbs.db + +# macOS metadata +._* + +# TextMate +*.tmproj +*.tmproject +.tmtags + +# Sublime Text +*.sublime-workspace +*.sublime-project + +# VS Code directories +.vscode/ + +# CodeKit +.codekit-config.json + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Compiled files +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Logs and databases +*.log +*.sql +*.sqlite +*.sqlite3 +*.xml + +# Binary and source packages +*.dmg +*.gz +*.iso +*.jar +*.tar +*.zip +*.rar +*.bin +*.war +*.ear +*.sar +*.bbl +*.pdf +*.xls +*.xlsx +*.ppt +*.pptx + +# Virtual environment +venv/ +env/ + +### Manually Entered +vim-debug/ +**/out/bin +**/out/lib +**/out/share +_deps +.cache/ +compile_commands.json +*.bak +docs/ +*.old + +# clangd cache +.cache/ +.vim/ +build/ +debug/ +realease/ +Release/ +Debug diff --git a/CMake/BuildProperties.cmake b/CMake/BuildProperties.cmake new file mode 100644 index 0000000..f3b3274 --- /dev/null +++ b/CMake/BuildProperties.cmake @@ -0,0 +1,114 @@ +# BuildProperties.cmake +# Copyright (c) 2024 Saul D Beniquez +# License: MIT +# +# This module defines a function prevent_in_source_build() that prevents in-source builds +# and sets a policy for CMake version 3.24.0 and above. + +function(prevent_in_source_build) + # Prevent in-source builds + if (CMAKE_BINARY_DIR STREQUAL CMAKE_SOURCE_DIR) + message(FATAL_ERROR "Source and build directories cannot be the same.") + endif() +endfunction() + +function(disable_deprecated_features) + # Use new timestamp behavior when extracting files archives + if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") + cmake_policy(SET CMP0135 NEW) + endif() +endfunction() + +function(git_setup_submodules) + find_package(Git QUIET) + if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git") + option(GIT_SUBMODULE "Check submodules during build" ON) + if(GIT_SUBMODULE) + message(STATUS "Git submodule update") + execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + RESULT_VARIABLE GIT_SUBMOD_RESULT) + if(NOT GIT_SUBMOD_RESULT EQUAL "0") + message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") + endif() + endif() + endif() +endfunction() + +function(set_artifact_dir path) + # Set local variable, not necessary to be parent scope since it's not used outside this function + set(ARTIFACT_DIR "${path}") + + # Set project-specific artifact directory in parent scope + set(${PROJECT_NAME}_ARTIFACT_DIR "${path}" PARENT_SCOPE) + + # Set output directories in parent scope using the provided path directly + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${path}/lib" PARENT_SCOPE) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${path}/lib" PARENT_SCOPE) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${path}/bin" PARENT_SCOPE) +endfunction() + +function(disable_tests_if_subproject) + if (NOT DEFINED PROJECT_NAME) + option(BUILD_TESTING "Build and run unit tests" ON) + else() + option(BUILD_TESTING "Build and run unit tests" OFF) + endif() +endfunction() + +function(use_ccache) +option(USE_CCACHE + [=[Use ccache compiler cache to speed up builds. +Enabled by default if ccache is found]=] +ON +) +# Search for the code caching compiler wrapper, ccache and enable it +# if found. This will speed up repeated builds. +if (USE_CCACHE) + message(CHECK_START "Detecting cacche") + + find_program(CCACHE_PATH ccache) + if(CCACHE_PATH) + message(CHECK_PASS("found")) + set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE_PATH}) + set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_PATH}) + endif() + + list(APPEND CMAKE_MESSAGE_INDENT " ") + message(STATUS "(set -DUSE_CCACHE=Off to disable)") + list(POP_BACK CMAKE_MESSAGE_INDENT) +endif() + +endfunction() + +function(detect_linkers) + option(USE_MOLD "Use the mold/sold parallel linker for faster builds" OFF) + if(USE_MOLD) + # Determine if the compiler is GCC or Clang + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + message(STATUS "Detected GCC/Clang, checking for mold/sold linker...") + + # Check for mold linker on general systems and ld64.mold on macOS + if(APPLE) + find_program(MOLD_LINKER ld64.mold) + set(CMAKE_LINKER_TYPE SOLD) + else() + find_program(MOLD_LINKER mold) + set(CMAKE_LINKER_TYPE MOLD) + endif() + + if(MOLD_LINKER) + message(STATUS "LINKER_TYPE set to ${CMAKE_LINKER_TYPE} for faster builds") + list(APPEND CMAKE_MESSAGE_INDENT " ") + message(STATUS "(set -DUSE_MOLD=OFF to disable)") + list(POP_BACK CMAKE_MESSAGE_INDENT) + else() + message(STATUS " -- No suitable mold linker found. Using default linker.") + endif() + else() + message(STATUS "Compiler is neither GCC nor Clang. Skipping mold linker check.") + endif() + endif() +endfunction() + +# vim: ts=4 sts=4 sw=4 noet foldmethod=indent : diff --git a/CMake/CPM.cmake b/CMake/CPM.cmake new file mode 100644 index 0000000..cc25ec2 --- /dev/null +++ b/CMake/CPM.cmake @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.38.7) +set(CPM_HASH_SUM "83e5eb71b2bbb8b1f2ad38f1950287a057624e385c238f6087f94cdfc44af9c5") + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} +) + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0a5ce57 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,116 @@ +cmake_minimum_required(VERSION 3.26) + +# Additional paths to search for custom and third-party CMake modules +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/CMake) + +include(BuildProperties) + +prevent_in_source_build() +disable_deprecated_features() + +# When this package is included as a subproject, there's no need to +# build and run the unit-tests. +disable_tests_if_subproject() +#git_setup_submodules() + +project(Qt6JsonModel + VERSION 0.0.1 + LANGUAGES C CXX + # Save this for later: + # HOMEPAGE_URL + DESCRIPTION "QJsonModel is a json tree model class for Qt6/C++17 based on QAbstractItemModel. MIT License." +) + +detect_linkers() + +include(CPM) + +SET(QJsonModel_TOP_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + +# enable compile_commands.json generation for clangd +set(CMAKE_EXPORT_COMPILE_COMMANDS On) + +IF(NOT CMAKE_BUILD_TYPE) + SET( CMAKE_BUILD_TYPE Debug ) +ENDIF() + +set(CMAKE_C_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) + +# Disable GNU compiler extensions +set(CMAKE_C_EXTENSIONS OFF) +set(CMAKE_CXX_EXTENSIONS OFF) + + +find_package(Qt6 COMPONENTS + Core REQUIRED + Widgets REQUIRED + Gui REQUIRED +) +qt_standard_project_setup() + +# Set output directories for build targets +#set_artifact_dir(${CMAKE_BINARY_DIR}/out) + +include(CheckIncludeFile) + +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + add_compile_definitions(-DDEBUG=1 -DQDEBUG=1) +endif() + +qt_add_library(QJsonModel +OBJECT + QJsonModel.cpp +) +set_target_properties(QJsonModel PROPERTIES EXPORT_NAME JsonModel) +add_library(Qt6::JsonModel ALIAS QJsonModel) + +# Since headers are in the include/ directory, going to handle MOC invocation +# manually. +set_target_properties(QJsonModel PROPERTIES + AUTOMOC OFF + POSITION_INDEPENDENT_CODE ON +) + +set(QJsonModel_PUBLIC_INCLUDE_DIR ${CMAKE_BINARY_DIR}/include) +#file(MAKE_DIRECTORY ${Qt6JsonModel_TOP_SOURCE_DIR}) + +add_custom_target(QJsonModel_include_files COMMAND ${CMAKE_COMMAND} -E copy_directory ${QJsonModel_TOP_SOURCE_DIR}/include ${QJsonModel_PUBLIC_INCLUDE_DIR} + DEPENDS ${QJsonModel_TOP_SOURCE_DIR}/include + COMMENT "Copying include files to ${IOCore_PUBLIC_INCLUDE_DIR}" +) + +target_include_directories(QJsonModel +SYSTEM BEFORE +PUBLIC ${QJsonModel_PUBLIC_INCLUDE_DIR} +) +add_dependencies(QJsonModel QJsonModel_include_files) + +# Manually call moc on all header files +file(GLOB Qt6JsonModel_HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/*.hpp) +qt_wrap_cpp(Qt6JsonModel_MOC_SOURCES ${Qt6JsonModel_HEADER_FILES} + TARGET QJsonModel +) + +# Append the MOC files to the source list +target_sources(QJsonModel PRIVATE ${Qt6JsonModel_MOC_SOURCES}) + +add_library(QJsonModelStatic STATIC) +add_library(Qt6::QJsonModelStatic ALIAS QJsonModelStatic ) +set_target_properties(QJsonModelStatic PROPERTIES OUTPUT_NAME "QJsonModel") + +add_library(QJsonModelShared SHARED ) +add_library(Qt6::QJsonModelShared ALIAS QJsonModelShared ) +set_target_properties(QJsonModelShared PROPERTIES OUTPUT_NAME "QJsonModel") + +target_link_libraries(QJsonModelStatic PRIVATE QJsonModel) +target_link_libraries(QJsonModelShared PRIVATE QJsonModel) + +target_link_libraries(QJsonModel +PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets +) + +# vim: ts=2 sw=2 noet foldmethod=indent : diff --git a/qjsonmodel.cpp b/QJsonModel.cpp similarity index 99% rename from qjsonmodel.cpp rename to QJsonModel.cpp index 09f274f..fbad3c1 100644 --- a/qjsonmodel.cpp +++ b/QJsonModel.cpp @@ -22,7 +22,7 @@ * SOFTWARE. */ -#include "qjsonmodel.h" +#include "QJsonModel.hpp" #include #include #include diff --git a/QJsonModel.pro b/QJsonModel.pro deleted file mode 100644 index d9fbd50..0000000 --- a/QJsonModel.pro +++ /dev/null @@ -1,23 +0,0 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2015-01-22T08:20:52 -# -#------------------------------------------------- - -QT += core gui widgets -CONFIG += c++11 -lessThan(QT_MAJOR_VERSION, 5): error("requires Qt 5") - -TARGET = QJsonModel -TEMPLATE = app - -SOURCES += \ - main.cpp \ - qjsonmodel.cpp - -HEADERS += \ - qjsonmodel.h - - - - diff --git a/qjsonmodel.h b/include/QJsonModel.hpp similarity index 100% rename from qjsonmodel.h rename to include/QJsonModel.hpp diff --git a/main.cpp b/main.cpp deleted file mode 100644 index 320ef4b..0000000 --- a/main.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/*********************************************** - Copyright (C) 2014 Schutz Sacha - This file is part of QJsonModel (https://github.com/dridk/QJsonmodel). - - QJsonModel is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - QJsonModel is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with QJsonModel. If not, see . - -**********************************************/ - -#include -#include -#include -#include -#include -#include "qjsonmodel.h" - -int main(int argc, char *argv[]) -{ - QApplication a(argc, argv); - QTreeView *view = new QTreeView; - QJsonModel *model = new QJsonModel; - - view->setModel(model); - - std::string json = R"({ - "firstName": "John", - "lastName": "Smith", - "age": 25, - "far-sighted": true, - "address": - { - "streetAddress": "21 2nd Street", - "city": "New York", - "state": "NY", - "postalCode": "10021" - }, - "phoneNumber": - [ - { - "comment": "This is just a comment!", - "type": "home", - "number": "212 555-1234" - }, - { - "comment1": "Another comment!", - "type": "fax", - "number": "646 555-4567" - } - ] - })"; - - model->addException({"comment"}); - model->loadJson(QByteArray::fromStdString(json)); - view->show(); - - QByteArray mjson = model->json(); - qDebug() << mjson; - - return a.exec(); -} diff --git a/qjsonmodel.py b/qjsonmodel.py deleted file mode 100644 index d9ebcea..0000000 --- a/qjsonmodel.py +++ /dev/null @@ -1,328 +0,0 @@ -"""Python adaptation of https://github.com/dridk/QJsonModel - -Supports Python 2 and 3 with PySide, PySide2, PyQt4 or PyQt5. -Requires https://github.com/mottosso/Qt.py - -Usage: - Use it like you would the C++ version. - - >>> import qjsonmodel - >>> model = qjsonmodel.QJsonModel() - >>> model.load({"key": "value"}) - -Test: - Run the provided example to sanity check your Python, - dependencies and Qt binding. - - $ python qjsonmodel.py - -Changes: - This module differs from the C++ version in the following ways. - - 1. Setters and getters are replaced by Python properties - 2. Objects are sorted by default, disabled via load(sort=False) - 3. load() takes a Python dictionary as opposed to - a string or file handle. - - - To load from a string, use built-in `json.loads()` - >>> import json - >>> document = json.loads("{'key': 'value'}") - >>> model.load(document) - - - To load from a file, use `with open(fname)` - >>> import json - >>> with open("file.json") as f: - ... document = json.load(f) - ... model.load(document) - -""" - -import json - -from Qt import QtWidgets, QtCore, __binding__ - - -class QJsonTreeItem(object): - def __init__(self, parent=None): - self._parent = parent - - self._key = "" - self._value = "" - self._type = None - self._children = list() - - def appendChild(self, item): - self._children.append(item) - - def child(self, row): - return self._children[row] - - def parent(self): - return self._parent - - def childCount(self): - return len(self._children) - - def row(self): - return ( - self._parent._children.index(self) - if self._parent else 0 - ) - - @property - def key(self): - return self._key - - @key.setter - def key(self, key): - self._key = key - - @property - def value(self): - return self._value - - @value.setter - def value(self, value): - self._value = value - - @property - def type(self): - return self._type - - @type.setter - def type(self, typ): - self._type = typ - - @classmethod - def load(self, value, parent=None, sort=True): - rootItem = QJsonTreeItem(parent) - rootItem.key = "root" - - if isinstance(value, dict): - items = ( - sorted(value.items()) - if sort else value.items() - ) - - for key, value in items: - child = self.load(value, rootItem) - child.key = key - child.type = type(value) - rootItem.appendChild(child) - - elif isinstance(value, list): - for index, value in enumerate(value): - child = self.load(value, rootItem) - child.key = index - child.type = type(value) - rootItem.appendChild(child) - - else: - rootItem.value = value - rootItem.type = type(value) - - return rootItem - - -class QJsonModel(QtCore.QAbstractItemModel): - def __init__(self, parent=None): - super(QJsonModel, self).__init__(parent) - - self._rootItem = QJsonTreeItem() - self._headers = ("key", "value") - - def clear(self): - self.load({}) - - def load(self, document): - """Load from dictionary - - Arguments: - document (dict): JSON-compatible dictionary - - """ - - assert isinstance(document, (dict, list, tuple)), ( - "`document` must be of dict, list or tuple, " - "not %s" % type(document) - ) - - self.beginResetModel() - - self._rootItem = QJsonTreeItem.load(document) - self._rootItem.type = type(document) - - self.endResetModel() - - return True - - def json(self, root=None): - """Serialise model as JSON-compliant dictionary - - Arguments: - root (QJsonTreeItem, optional): Serialise from here - defaults to the the top-level item - - Returns: - model as dict - - """ - - root = root or self._rootItem - return self.genJson(root) - - def data(self, index, role): - if not index.isValid(): - return None - - item = index.internalPointer() - - if role == QtCore.Qt.DisplayRole: - if index.column() == 0: - return item.key - - if index.column() == 1: - return item.value - - elif role == QtCore.Qt.EditRole: - if index.column() == 1: - return item.value - - def setData(self, index, value, role): - if role == QtCore.Qt.EditRole: - if index.column() == 1: - item = index.internalPointer() - item.value = str(value) - - if __binding__ in ("PySide", "PyQt4"): - self.dataChanged.emit(index, index) - else: - self.dataChanged.emit(index, index, [QtCore.Qt.EditRole]) - - return True - - return False - - def headerData(self, section, orientation, role): - if role != QtCore.Qt.DisplayRole: - return None - - if orientation == QtCore.Qt.Horizontal: - return self._headers[section] - - def index(self, row, column, parent=QtCore.QModelIndex()): - if not self.hasIndex(row, column, parent): - return QtCore.QModelIndex() - - if not parent.isValid(): - parentItem = self._rootItem - else: - parentItem = parent.internalPointer() - - childItem = parentItem.child(row) - if childItem: - return self.createIndex(row, column, childItem) - else: - return QtCore.QModelIndex() - - def parent(self, index): - if not index.isValid(): - return QtCore.QModelIndex() - - childItem = index.internalPointer() - parentItem = childItem.parent() - - if parentItem == self._rootItem: - return QtCore.QModelIndex() - - return self.createIndex(parentItem.row(), 0, parentItem) - - def rowCount(self, parent=QtCore.QModelIndex()): - if parent.column() > 0: - return 0 - - if not parent.isValid(): - parentItem = self._rootItem - else: - parentItem = parent.internalPointer() - - return parentItem.childCount() - - def columnCount(self, parent=QtCore.QModelIndex()): - return 2 - - def flags(self, index): - flags = super(QJsonModel, self).flags(index) - - if index.column() == 1: - return QtCore.Qt.ItemIsEditable | flags - else: - return flags - - def genJson(self, item): - nchild = item.childCount() - - if item.type is dict: - document = {} - for i in range(nchild): - ch = item.child(i) - document[ch.key] = self.genJson(ch) - return document - - elif item.type == list: - document = [] - for i in range(nchild): - ch = item.child(i) - document.append(self.genJson(ch)) - return document - - else: - return item.value - - -if __name__ == '__main__': - import sys - - app = QtWidgets.QApplication(sys.argv) - view = QtWidgets.QTreeView() - model = QJsonModel() - - view.setModel(model) - - document = json.loads("""\ - { - "firstName": "John", - "lastName": "Smith", - "age": 25, - "address": { - "streetAddress": "21 2nd Street", - "city": "New York", - "state": "NY", - "postalCode": "10021" - }, - "phoneNumber": [ - { - "type": "home", - "number": "212 555-1234" - }, - { - "type": "fax", - "number": "646 555-4567" - } - ] - } - """) - - model.load(document) - model.clear() - model.load(document) - - # Sanity check - assert ( - json.dumps(model.json(), sort_keys=True) == - json.dumps(document, sort_keys=True) - ) - - view.show() - view.resize(500, 300) - app.exec_()