diff --git a/CMakeLists.txt b/CMakeLists.txt index d1798826..1b20b04c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,14 +8,14 @@ cmake_minimum_required(VERSION 3.23.0...3.26.0) # avoided and only used for hotfixes. DON'T USE TRAILING # ZEROS IN VERSIONS project(Qx - VERSION 0.5.8 + VERSION 0.6 LANGUAGES CXX DESCRIPTION "Qt Extensions Library" ) # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("v0.3.7") +fetch_ob_cmake("v0.3.8") # Initialize project according to standard rules include(OB/Project) diff --git a/README.md b/README.md index 343210bf..bfa743a5 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,15 @@ This project makes use of the CMake build system generator for both compilation It is based on Qt 6. -[![Dev Builds](https://github.com/oblivioncth/Qx/actions/workflows/push-reaction.yml/badge.svg?branch=dev)](https://github.com/oblivioncth/Qx/actions/workflows/push-reaction.yml) +[![Dev Builds](https://github.com/oblivioncth/Qx/actions/workflows/build-project.yml/badge.svg?branch=dev)](https://github.com/oblivioncth/Qx/actions/workflows/build-project.yml) ## Documentation: Detailed documentation of this library, facilitated by Doxygen, is available at: https://oblivioncth.github.io/Qx/ ### Highlights: +- [Bindable Properties](https://oblivioncth.github.io/Qx/properties.html) +- [Declarative JSON](https://oblivioncth.github.io/Qx/declarativejson.html) - [Qx::ApplicationLogger](https://oblivioncth.github.io/Qx/classQx_1_1ApplicationLogger.html) - [Qx::AsyncDownloadManager](https://oblivioncth.github.io/Qx/classQx_1_1AsyncDownloadManager.html)/[Qx::SyncDownloadManager](https://oblivioncth.github.io/Qx/classQx_1_1SyncDownloadManager.html) - [Qx::Base85](https://oblivioncth.github.io/Qx/classQx_1_1Base85.html) @@ -20,7 +22,6 @@ Detailed documentation of this library, facilitated by Doxygen, is available at: - [Qx::Table< T >](https://oblivioncth.github.io/Qx/classQx_1_1Table.html)/[Qx::DsvTable](https://oblivioncth.github.io/Qx/classQx_1_1DsvTable.html) - [Qx::Error](https://oblivioncth.github.io/Qx/classQx_1_1Error.html) - [Qx::GroupedProgressManager](https://oblivioncth.github.io/Qx/classQx_1_1GroupedProgressManager.html) -- [Qx::Json](https://oblivioncth.github.io/Qx/qx-json_8h.html) - [Qx::SetOnce< T >](https://oblivioncth.github.io/Qx/classQx_1_1SetOnce.html) - [Qx::TaskbarButton](https://oblivioncth.github.io/Qx/classQx_1_1TaskbarButton.html) - [qx-common-io.h](https://oblivioncth.github.io/Qx/qx-common-io_8h.html) diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 811ec815..a545a3dc 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -15,11 +15,14 @@ set(DOXYGEN_PREDEFINED "QX_DECLARE_ERROR_ADAPTATION(Adaptable, Adapter)=" ) +set(DOXYGEN_EXCLUDE_SYMBOLS + "_QxPrivate" +) + # Setup documentation ob_standard_documentation(${DOC_TARGET_NAME} DOXY_VER 1.9.4 PROJ_NAME "${PROJECT_NAME}" - QT_PREFIX "${Qt_PREFIX_PATH}" QT_MODULES qtconcurrent qtcore diff --git a/doc/cmake/file_templates/mainpage.md.in b/doc/cmake/file_templates/mainpage.md.in index cd3a0cd8..e45c9f55 100644 --- a/doc/cmake/file_templates/mainpage.md.in +++ b/doc/cmake/file_templates/mainpage.md.in @@ -77,10 +77,10 @@ The requirements for building from Git are the same as for using Qx, with the ob If newer to working with Qt, it is easiest to build from within Qt creator as it handles a large portion of environment setup, including adding Qt to CMake's package search list, automatically. Simply make sure that a kit is configured in Qt Creator that uses a compatible version of Qt, open the CMakeLists.txt file as a project, and build with the desired configuration. -The CMake project is designed to be used with multi-configuration generators such as Visual Studio or Ninja Multi-Config (recommended), and may require some tweaking to work with single configuration generators. - If you only need a subset of Qx's components, the **QX_COMPONENTS** cache variable can be set to a semicolon or whitespace separated list of components. Only these components and their required dependencies will be configured, which can save on build time. +The `ninja` generator is recommended. + ### CMake Options: - `QX_DOCS` - Set to `ON` in order to generate the documentation target (OFF) diff --git a/doc/general/majorfeatures.md b/doc/general/majorfeatures.md new file mode 100644 index 00000000..a25679bf --- /dev/null +++ b/doc/general/majorfeatures.md @@ -0,0 +1,7 @@ +Major Features {#majorfeatures} +=============================== + +This page catalogues the most significant features of Qx, which generally form a complete/comprehensive system that can be evaluated independently from the rest of the library. + +- @subpage properties "Bindable Properties System" +- @subpage declarativejson "Declarative JSON" diff --git a/lib/core/CMakeLists.txt b/lib/core/CMakeLists.txt index 5d382754..d771fdd7 100644 --- a/lib/core/CMakeLists.txt +++ b/lib/core/CMakeLists.txt @@ -5,6 +5,7 @@ qx_add_component("Core" qx-algorithm.h qx-array.h qx-base85.h + qx-bimap.h qx-bitarray.h qx-bytearray.h qx-char.h @@ -13,6 +14,7 @@ qx_add_component("Core" qx-dsvtable.h qx-error.h qx-exclusiveaccess.h + qx-flatmultiset.h qx-freeindextracker.h qx-genericerror.h qx-global.h @@ -22,17 +24,22 @@ qx_add_component("Core" qx-iostream.h qx-json.h qx-list.h + qx-lopmap.h qx-processbider.h qx-progressgroup.h + qx-property.h qx-table.h + qx-threadsafesingleton.h qx-versionnumber.h qx-regularexpression.h qx-setonce.h qx-string.h qx-system.h qx-systemerror.h + qx-systemsignalwatcher.h qx-traverser.h __private/qx-internalerror.h + __private/qx-property_detail.h IMPLEMENTATION qx-abstracterror.cpp qx-algorithm.cpp @@ -55,6 +62,8 @@ qx_add_component("Core" qx-processbider.cpp qx-processbider_p.h qx-progressgroup.cpp + qx-property_p.h + qx-property.cpp qx-versionnumber.cpp qx-string.cpp qx-system.cpp @@ -66,6 +75,10 @@ qx_add_component("Core" qx-systemerror.cpp qx-systemerror_linux.cpp qx-systemerror_win.cpp + qx-systemsignalwatcher.cpp + qx-systemsignalwatcher_p.h + __private/qx-generalworkerthread.h + __private/qx-generalworkerthread.cpp __private/qx-internalerror.cpp __private/qx-processwaiter.h __private/qx-processwaiter.cpp @@ -73,18 +86,27 @@ qx_add_component("Core" __private/qx-processwaiter_win.cpp __private/qx-processwaiter_linux.h __private/qx-processwaiter_linux.cpp + __private/qx-signaldaemon.h + __private/qx-signaldaemon_win.h + __private/qx-signaldaemon_win.cpp + __private/qx-signaldaemon_linux.h + __private/qx-signaldaemon_linux.cpp DOC_ONLY + qx-bimap.dox qx-regularexpression.dox qx-bytearray.dox qx-exclusiveaccess.dox + qx-flatmultiset.dox qx-index.dox qx-iostream.dox qx-list.dox + qx-lopmap.dox qx-traverser.dox qx-cumulation.dox qx-array.dox qx-setonce.dox qx-table.dox + qx-threadsafesingleton.dox LINKS PUBLIC ${Qt}::Core diff --git a/lib/core/doc/general/declarativejson.md b/lib/core/doc/general/declarativejson.md new file mode 100644 index 00000000..6f34ed1d --- /dev/null +++ b/lib/core/doc/general/declarativejson.md @@ -0,0 +1,71 @@ +Qx Declarative JSON {#declarativejson} +====================================== + +Qx features a highly flexible, simple to use, declarative mechanism for parsing/serializing JSON data into user structs and other types. + +For example, the following JSON data: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.json} +{ + "title": "Sample JSON Data", + "info": { + "rating": 10, + "cool": true + }, + "reviews": [ + "Wicked!", + "Awesome!", + "Fantastic!" + ] +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +can easily be parsed into a corresponding set of C++ data structures like so: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +#include + +struct Info +{ + int rating; + bool cool; + + QX_JSON_STRUCT(rating, cool); +}; + +struct MyJson +{ + QString title; + Info info; + QList reviews; + + QX_JSON_STRUCT(title, info, reviews); +}; + +int main() +{ + QFile jsonFile("data.json"); + MyJson myJsonDoc; + + // Parse into custom structures + Qx::JsonError je = Qx::parseJson(myJsonDoc, jsonFile); + Q_ASSERT(!je.isValid()); + + ... +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Likewise, the structure can be serialized back out into textual JSON data with: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +int main() +{ + ... + + // Serialize to JSON + je = Qx::serializeJson(jsonFile, myJsonDoc); + Q_ASSERT(!je.isValid()); +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This system is accessed through the qx-json.h header, predominantly with QX_JSON_STRUCT(). \ No newline at end of file diff --git a/lib/core/doc/general/properties.md b/lib/core/doc/general/properties.md new file mode 100644 index 00000000..24bb9f94 --- /dev/null +++ b/lib/core/doc/general/properties.md @@ -0,0 +1,219 @@ +Qx Bindable Properties {#properties} +==================================== + +Bindable properties are properties that enable the establishment of relationships between various properties in a declarative manner. Properties with bindings, which are essentially just C++ functions, are updated automatically whenever one or more other properties that they depend on are changed. These dependencies are established automatically when a property is read during the binding evaluation of another. + +The following is a prime example of their use: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +Qx::Property width(2); +Qx::Property height(2); +Qx::Property area([&]{ return width * height; }); +area.subscribeLifetime([&]{ qDebug() << "Area is:" << area; }); +width = 3; +height = 1; + +// Output +// Area is: 4 +// Area is: 6 +// Area is: 3 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Qx Bindable Properties System can thought of as an alternate implementation of Qt Bindable Properties, and as such its interface is closely modeled after the latter. + +The foundation of the system is Qx::AbstractBindableProperty, which represents the general bindable properties interface, while Qx::Property is the primary implementation of said interface. Additional utilities related to the system are accessible through the qx-property.h header. + +Qx properties can, for the most part, be used interchangeably with Qt in the context of C++ code (QML integration is not supported, and may or may not be attempted at a later time), just with some behavioral and feature set quality of life changes; thus, for brevity this documentation focuses on the differences between the two systems and if you are totally unfamiliar with bindable properties it is recommended to read the documentation for Qt Bindable Properties first. + +**The biggest difference between the two is that Qx Bindable Properties were designed with the motivation that bindings are only ever evaluated when absolutely necessary**, as there are various situations with Qt properties where extra binding evaluations occur. + +What's the Same? +------------ +Pretty much everything between Qx properties and Qt properties are the same, other than what is mentioned under the Advantages and Disadvantages sections below. Regardless, the following is a non-exhaustive list of some key aspects that both systems share that are important to keep in mind: + + - Most methods and the overall API is the same. + - Qx::Bindable, like QBindable exists as a property wrapper that allows for generic access to any property that implements the bindable interface, and can wrap QObject properties (i.e. declared with Q_PROPERTY()) + - You can still group property value changes using Qx::beginPropertyUpdateGroup() and Qx::endPropertyUpdateGroup(). + - Dependency/update cycles are detected. + - Qx properties are not thread safe. In general, only interact with a property through it's owning thread. + - Qt's advice about writing intermediate values to properties and respective class invariants should still be respected + - Observers (i.e. registered callback functions) are not notified of a property change until the entire update-chain in which the change occurred has finished resolving; that is, all dependent properties are first updated before any callbacks are invoked. + +Advantages +------------ +### Absolutely Minimal Binding Evaluation: + +This is the largest advantage, and the main motivation for the creation of this system. + +As impressive as the Qt Bindable Property system is, there is one aspect of it's behavior that can be frustrating and potentially problematic: It often re-evaluates bindings more times that would appear necessary, presumably due to technical limitations. + +Let's take this simple example: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +QProperty x; +QProperty x2([&]{ return std::pow(x, 2.0); }); +QProperty poly([&]{ return x2 + x; }); +auto n = poly.addNotifier([&]{ qDebug() << "Polynomial value:" << poly; }); +x = 1; +x = 2; +x = 3; + +// Output +// Polynomial value: 2 +// Polynomial value: 6 +// Polynomial value: 12 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Going off just the notifier callback output, nothing initially looks amiss. + +Here we have a dependency graph that looks like this: +@cond@ +This isn't as nice looking and has rendering issues with DoxygenAwesome (inverted colors and grey scrollbar) + +@dot +digraph Dependencies { + rankdir=BT; + node [ + shape=circle, + fixedsize=true, + width=0.8, + style=filled, + color="#380000", + fillcolor="#8F1717", + + ]; + splines=ortho; + nodesep=1.5; + ranksep=0.7; + + A [label="poly"]; + B [label="x2"]; + C [label="x"]; + + B -> A; + C -> A; + C -> B; +} +@enddot +@endcond + +![Polynomial property example graph](properties-0.png){html: width=35%} + +Just at a glance we can see that when `x` is changed, `x2` should be updated before `poly` since the latter depends on both `x` and `x2`; however, if we change the example a little to gain some insight into how updates are handled, we see Qt Properties *do not do this*: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +qDebug() << "INITIAL SETUP"; +QProperty x; +QProperty x2([&]{ + qDebug() << "eval x2, x is" << x.valueBypassingBindings(); + return std::pow(x, 2.0); +}); +QProperty poly([&]{ + qDebug() << "eval poly, x is" << x.valueBypassingBindings() << "x2 is" << x2.valueBypassingBindings(); + return x + x2; +}); +auto n = poly.addNotifier([&]{ qDebug() << "Polynomial value:" << poly; }); +qDebug() << "CHANGE START"; +x = 1; +x = 2; +x = 3; + +// Output +// INITIAL SETUP +// eval x2, x is 0 +// eval poly, x is 0 x2 is 0 +// CHANGE START +// eval poly, x is 1 x2 is 0 +// eval x2, x is 1 +// eval poly, x is 1 x2 is 1 +// Polynomial value: 2 +// eval poly, x is 2 x2 is 1 +// eval x2, x is 2 +// eval poly, x is 2 x2 is 4 +// Polynomial value: 6 +// eval poly, x is 3 x2 is 4 +// eval x2, x is 3 +// eval poly, x is 3 x2 is 9 +// Polynomial value: 12 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As shown, when we start updating `x`, the `poly` binding is evaluated first with a stale value of `x2`, then `x2` is updated, and finally `poly` is evaluated again. It's possible that declaration order, or some other details may influence this, but that is largely irrelevant, since ideally evaluation count should be consistent regardless of those factors. The takeaway is that the Qt Bindable Properties system does not *maximally* prioritize minimizing binding evaluations and instead only ensures that the final state of all properties is correct once its update process is finished, while keeping binding evaluations *somewhat* minimal. + +If we then simply change the use of QProperty in the above example to Qx::Property, the output becomes: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +INITIAL SETUP +eval x2, x is 0 +eval poly, x is 0 x2 is 0 +CHANGE START +eval x2, x is 1 +eval poly, x is 1 x2 is 1 +Polynomial value: 2 +eval x2, x is 2 +eval poly, x is 2 x2 is 4 +Polynomial value: 6 +eval x2, x is 3 +eval poly, x is 3 x2 is 9 +Polynomial value: 12 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +which shows that each involved binding is evaluated in a order that prevents any re-evaluations from being required. + +At first, although obviously wasteful, it may not seem like a huge deal; however, consider the case where one of these properties might be checked to see if a particular resource is valid (like a pointer) and the other property wraps the resource itself. If the binding that uses both of these properties was evaluated with a stale value for the "resource is valid" property, it might then try to access an invalid resource and cause your program to crash. + +Another benefit of Qx's approach is that it handles "incomplete dependency" information in bindings cleanly and has looser restrictions compared to Qt's in the sense that not all code paths need to read from all property dependency on every invocation. For example, if you originally had values you wanted to convert to properties that looked like this: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +// Goofy bools +bool round = false; +bool maybeBouncy = round; +bool ball = round && maybeBouncy; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +for the best experience with QProperty you're supposed to do: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +QProperty round (false); +QProperty maybeBouncy([&]{ return round.value(); }); +QProperty ball([&]{ + bool r = round.value(); + bool mb = maybeBouncy.value(); + return r && mb; +}); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +so that both `round ` and `maybeBouncy` are always read within the binding and `ball`'s dependency on both is well-established. + +With Qx::Property, you can simply do: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +Qx::Property round(false); +Qx::Property maybeBouncy([&]{ return round.value(); }); +Qx::Property ball([&]{ return round.value() && maybeBouncy.value(); }); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When `round` is updated for the first time, it will appear like `ball` only depends on `round` since `maybeBouncy` wasn't read due to short-circuiting on the initial binding invocation during `ball`'s construction; however, Qx will handle this gracefully by temporarily "pausing" the evaluation of `ball`'s binding when it sees the dependency on `maybeBouncy` for the first time in order to ensure that property is updated first. Therefore, `ball` will not see a stale value for `maybeBouncy` and `ball`'s binding still only needs to run one time even though it's dependencies suddenly changed! + +Qx's implementation is designed so that bindings should **never** be evaluated more than absolutely necessary. If you've found a scenario in which this isn't true, please open an issue about it on GitHub. + +### Other: + + - More idiomatic const correctness. + - Due to implementation constraints, some methods on some Qt property classes that are "read" in nature (i.e. do not modify principle data) are non-const, making accessing them in non-const contexts impossible, even though you're not writing to the value in any way. + - Use of `[[nodiscard]]` for callback handles to catch subtle bugs in which a callback would be immediately unregistered due to the handle being discarded + - Non-templated callback handles. + - All methods that return a callback function "handle" (i.e. lifecycle handler) return a non-templated type, which makes storing them in a container trivial, unlike `QPropertyChangeHandler` + - "Lifetime" callback registration. + - Additional registration methods that tie the lifetime of the callback to the lifetime of the property and don't require the user to manage a handle object + - More flexible operator->(). + - Implementation of operator->() that allows for chaining through more T types, like basic aggregates. + +Disadvantages +------------ +Given enough motivation, these drawbacks may be reduced or outright eliminated in the future. + + - No tie-in with QML, which is of course the biggest downside currently + - No integration with QMetaObject/MOC + - These bindable properties cannot be queried, or used in a type-erased fashion through QMetaObject, since that would require modifying MOC itself + - No advance error system like QPropertyBindingError that notes the source location of an error and allows reactionary steps to be taken at runtime. + - If something like a cycle is detected, the program simply aborts through an assertion with a diagnostic. + - No equivalent to QObjectBindableProperty for making property data a "built-in" part of the QObject itself, though simply adding Qx::Property to a QObject derived class as a member variable is not much different + - Performance between the two has not been properly profiled. It's conceivable that Qt properties are a bit more efficient given their maturity and pedigree, though the Qx implementation should still be fairly performant due to its approach. Additionally, the fact that Qt's system sometimes re-evaluates bindings when not necessary situationally gives an edge to Qx's system. + - Some other minor facets of Qt Properties that Qx Properties do not have, which make the former a bit more robust \ No newline at end of file diff --git a/lib/core/doc/res/images/properties-0.png b/lib/core/doc/res/images/properties-0.png new file mode 100644 index 00000000..d98a50b5 Binary files /dev/null and b/lib/core/doc/res/images/properties-0.png differ diff --git a/lib/core/doc/res/snippets/qx-json.cpp b/lib/core/doc/res/snippets/qx-json.cpp index 80606f98..be40653c 100644 --- a/lib/core/doc/res/snippets/qx-json.cpp +++ b/lib/core/doc/res/snippets/qx-json.cpp @@ -1,62 +1,3 @@ -//! [0] -{ - "title": "Sample JSON Data", - "info": { - "rating": 10, - "cool": true - }, - "reviews": [ - "Wicked!", - "Awesome!", - "Fantastic!" - ] -} -//! [0] - -//! [1] -#include - -struct Info -{ - int rating; - bool cool; - - QX_JSON_STRUCT(rating, cool); -}; - -struct MyJson -{ - QString title; - Info info; - QList reviews; - - QX_JSON_STRUCT(title, info, reviews); -}; - -int main() -{ - QFile jsonFile("data.json"); - MyJson myJsonDoc; - - // Parse into custom structures - Qx::JsonError je = Qx::parseJson(myJsonDoc, jsonFile); - Q_ASSERT(!je.isValid()); - - ... -} -//! [1] - -//! [2] -int main() -{ - ... - - // Serialize to JSON - je = Qx::serializeJson(jsonFile, myJsonDoc); - Q_ASSERT(!je.isValid()); -} -//! [2] - //! [3] struct MyStruct { diff --git a/lib/core/doc/res/snippets/qx-threadsafesingleton.cpp b/lib/core/doc/res/snippets/qx-threadsafesingleton.cpp new file mode 100644 index 00000000..6bc27ee2 --- /dev/null +++ b/lib/core/doc/res/snippets/qx-threadsafesingleton.cpp @@ -0,0 +1,33 @@ +//! [0] +class MySingleton : Qx::ThreadSafeSingleton +{ + QX_THREAD_SAFE_SINGLETON(MySingleton); +private: + std::string mData; + MySingleton() = default; // Generally should be private + +public: + doStuffSafely() { mData = "I'm for sure set while not being read!"; } + checkStuffSafely() { return mData; // Not being written to when returned } +} + +//... + +void functionInArbitraryThread() +{ + auto singleton = MySingleton::instance(); + // This function now has a exclusive access to MySingleton (i.e. a mutex lock is established) + singleton->doStuffSafely(); + + // Unlocked when 'singleton' goes out of scope, or is manually unlocked. +} + +void functionInAnotherThread() +{ + // Safely lock and read. It's guarenteed that no other thread is using MySingleton + // after the instance is obtained. + auto singleton = MySingleton::instance(); + std::string info = singleton->checkStuffSafely(); + //... +} +//! [0] diff --git a/lib/core/include/qx/core/__private/qx-property_detail.h b/lib/core/include/qx/core/__private/qx-property_detail.h new file mode 100644 index 00000000..d68d0f96 --- /dev/null +++ b/lib/core/include/qx/core/__private/qx-property_detail.h @@ -0,0 +1,183 @@ +#ifndef QX_PROPERTY_DETAIL_H +#define QX_PROPERTY_DETAIL_H + +// Shared Lib Support +#include "qx/core/qx_core_export.h" + +// Standard Library Includes +#include +#include + +// Qt Includes +#include +#include +#include +#include + +// Intra-component Includes +#include "qx/core/qx-threadsafesingleton.h" + +/*! @cond */ +namespace Qx +{ + class PropertyNode; + template class AbstractBindableProperty; +} + +namespace _QxPrivate +{ + +class QX_CORE_EXPORT BindableInterface +{ +//-Instance Variables------------------------------------------------------------- +private: + std::unique_ptr mNode; // Has to be dynamic, in-part, to bypass const issue for Property::value() + +//-Constructor-------------------------------------------------------------------- +protected: + BindableInterface(); + BindableInterface(BindableInterface&& other); + +//-Destructor-------------------------------------------------------------------- +public: + virtual ~BindableInterface(); + +//-Instance Functions------------------------------------------------------------- +protected: + void notifyBindingAdded(); + void notifyBindingRemoved(); + void notifyValueChanged(); + void attachToCurrentEval() const; + +public: + virtual bool callBinding() = 0; // Needs to call binding and return if value actually changed, but do nothing else + virtual void notifyObservers() const = 0; + Qx::PropertyNode* node() const; + +//-Operators------------------------------------------------------------- +protected: + BindableInterface& operator=(BindableInterface&& other); +}; + +class QX_CORE_EXPORT PropertyObserverManager +{ + template + friend class Qx::AbstractBindableProperty; + Q_DISABLE_COPY_MOVE(PropertyObserverManager); +//-Aliases------------------------------------------------------------------ +public: + using ObserverId = quint64; + +//-Inner Classes------------------------------------------------------------- +private: + class Observer + { + Q_DISABLE_COPY(Observer); + + ObserverId mId; + std::function mFunctor; + + public: + template + Observer(ObserverId id, Functor&& f) : + mId(id), + mFunctor(std::forward(f)) + {} + + // For moves, the exchanged id is technically valid, but we don't ever touch the old instance + Observer(Observer&& other) = default; + Observer& operator=(Observer&& other) = default; + + ObserverId id() const { return mId; } + void invoke() const { mFunctor(); } + }; + +//-Instance Variables------------------------------------------------------------- +private: + /* ID use is isolated to a per-manager basis, so collision would require the same + * property to have cycled through 2^64 properties... effectively 0% chance + */ + ObserverId mNextId = 0; + std::vector mObservers; + +//-Constructor------------------------------------------------------------- +private: + PropertyObserverManager(); + +//-Instance Functions------------------------------------------------------------- +public: + template + ObserverId add(Functor&& f) { mObservers.emplace_back(mNextId++, std::forward(f)); return mObservers.back().id(); } + void remove(ObserverId id); + void invokeAll() const; +}; + +class ObjectPropertyAdapterRegistry : private Qx::ThreadSafeSingleton +{ + QX_THREAD_SAFE_SINGLETON(ObjectPropertyAdapterRegistry); + template + friend class ObjectPropertyAdapter; +//-Types-------------------------------------------------------------- +private: + /* It would be more sane to store the adapters here using the common base class BindableInterface, + * but that class is inherited privately by AbstractProperty so ObjectPropertyAdapter cannot decay to + * that base type, nor convert back to itself without introducing some kind of empty base on top of + * BindalbeInterface that inherits from the latter privately, but then is inherited from + * (by AbstractProperty) using protected inheritance and storing that instead; or, introducing some + * kind of static function to BindableInterface that acts as a wrapper to static_cast and has it + * handle the casting of itself to whatever, but that is a little leaky (thought it could be private, + * with friend used for ObjectPropertyAdapter, but that's bleh). + * + * So... until a better method is settled on, we just use void* for now since the class retrieving any stored + * adapters will always be the right type. + */ + using AdapterList = QList; + using AdapterMap = QHash; + +//-Instance Variables------------------------------------------------------------- +private: + AdapterMap mStorage; + +//-Constructor------------------------------------------------------------- +private: + ObjectPropertyAdapterRegistry() = default; + +//-Instance Functions------------------------------------------------------------- +public: + void* retrieve(const QObject* obj, const QMetaProperty& property); + void store(const QObject* obj, const QMetaProperty& property, void* adapter); + void remove(const QObject* obj, const QMetaProperty& property); +}; + +class ObjectPropertyAdapterLiaison : public QObject +{ + template + friend class ObjectPropertyAdapter; + +//-Instance Variables------------------------------------------------------------- +private: + bool mIgnoreUpdates = false; + + Q_OBJECT; +//-Constructor------------------------------------------------------------- +private: + ObjectPropertyAdapterLiaison() = default; + +//-Instance Functions------------------------------------------------------------- +public: + bool configure(const QObject* o, QMetaProperty p); + void setIgnoreUpdates(bool ignore); + +//-Signals & Slots------------------------------------------------------------- +private slots: + void handleNotify(); + +signals: + void propertyNotified(); + void objectDeleted(); +}; + +} +/*! @endcond */ + +#endif // QX_PROPERTY_DETAIL_H diff --git a/lib/core/include/qx/core/qx-abstracterror.h b/lib/core/include/qx/core/qx-abstracterror.h index c01c763c..38317467 100644 --- a/lib/core/include/qx/core/qx-abstracterror.h +++ b/lib/core/include/qx/core/qx-abstracterror.h @@ -105,7 +105,7 @@ friend class Error; public: bool operator==(const AbstractError& other) const = default; bool operator!=(const AbstractError& other) const = default; - operator bool() const { return deriveValue() > 0; }; + explicit operator bool() const { return deriveValue() > 0; }; }; /* TODO: Get string of the type automatically when it becomes diff --git a/lib/core/include/qx/core/qx-bimap.h b/lib/core/include/qx/core/qx-bimap.h new file mode 100644 index 00000000..d200fc31 --- /dev/null +++ b/lib/core/include/qx/core/qx-bimap.h @@ -0,0 +1,399 @@ +#ifndef QX_BIMAP_H +#define QX_BIMAP_H + +#include + +// Qt Includes +#include + +// Extra-component Includes +#include + +namespace Qx +{ + +template +class Bimap; + +template +concept asymmetric_bimap = !std::same_as; + +template +concept bimap_iterator_predicate = defines_call_for_s::const_iterator>; + +template +concept bimap_pair_predicate = defines_call_for_s>; + +template +concept bimap_predicate = bimap_iterator_predicate || bimap_pair_predicate; + +} + +/*! @cond */ +namespace _QxBimapPrivate +{ + +template +qsizetype _erase_if(Qx::Bimap& bm, Predicate& pred) +{ + // This could be moved to a private file and made more generic for any other container types if needed + using ConstIterator = typename Qx::Bimap::const_iterator; + + qsizetype result = 0; + + ConstIterator it = bm.cbegin(); + const ConstIterator end = bm.cend(); + while (it != end) + { + if constexpr(Qx::bimap_iterator_predicate) + { + if(pred(it)) + { + it = bm.erase(it); + result++; + } + else + it++; + } + else if constexpr(Qx::bimap_pair_predicate) + { + if (pred(std::move(*it))) + { + it = bm.erase(it); + result++; + } else + it++; + } + else + { + // Delayed evaluation trick to prevent immediate static_assert failure for older compilers from before the resolution of CWG 2518 + static_assert(sizeof(Qx::Bimap) == 0, "Invalid Predicate"); + } + } + + return result; +} + +} +/*! @endcond */ + +namespace Qx +{ + +template +class Bimap +{ +//-Inner Classes---------------------------------------------------------------------------------------------------- +public: + //class iterator; Would cause internal iterator invalidation with no way to recover AFAICT, with current impl. + class const_iterator + { + friend class Bimap; + //-Aliases------------------------------------------------------------------------------------------------------ + private: + using PIterator = typename QHash::const_iterator; + + //-Instance Variables------------------------------------------------------------------------------------------- + private: + PIterator mPItr; + + //-Constructor-------------------------------------------------------------------------------------------------- + private: + const_iterator(const PIterator& pitr) : mPItr(pitr) {} + + public: + const_iterator() {} + + //-Instance Functions------------------------------------------------------------------------------------------- + public: + const Left& left() const { return mPItr.key(); } + const Right& right() const { return *mPItr.value(); } + + //-Operators--------------------------------------------------------------------------------------------- + public: + bool operator==(const const_iterator& other) const = default; + std::pair operator*() const { return std::pair(left(), right()); } + const_iterator& operator++() { mPItr++; return *this; } + + const_iterator operator++(int) + { + auto cur = *this; + mPItr++; + return cur; + } + }; + +//-Aliases------------------------------------------------------------------------------------------------------ +public: + using iterator = const_iterator; + using left_type = Left; + using right_type = Right; + using ConstIterator = const_iterator; + using difference_type = typename QHash::difference_type; + using size_type = typename QHash::size_type; + +//-Instance Variables------------------------------------------------------------------------------------------- +private: + QHash mL2R; + QHash mR2L; + +//-Constructor-------------------------------------------------------------------------------------------------- +public: + Bimap() {} + + Bimap(std::initializer_list> list) + { + reserve(list.size()); + for(auto it = list.begin(); it != list.end(); ++it) + insert(it->first, it->second); + } + +//-Class Functions------------------------------------------------------------------------------------------- +private: + template + void removeCrossReference(AMap& am, BMap& bm, const V& v) + { + if constexpr(std::same_as) + Q_ASSERT(&am != &bm); // Ensure different maps are used if the types are the same + + if(am.contains(v)) + bm.remove(*am[v]); + } + + template + bool remove(AMap& am, BMap& bm, const V& v) + { + if(!am.contains(v)) + return false; + + bm.remove(*am[v]); + am.remove(v); + return true; + } + +//-Instance Functions------------------------------------------------------------------------------------------- +private: + const_iterator existingRelation(const Left& l, const Right& r) const + { + auto itr = mL2R.constFind(l); + return (itr != mL2R.cend() && *itr.value() == r) ? const_iterator(itr) : const_iterator(); + } + + void removeCrossReferences(const Left& l, const Right& r) + { + // Remove to-be stale relations + removeCrossReference(mL2R, mR2L, l); + removeCrossReference(mR2L, mL2R, r); + } + + const_iterator addOrUpdateRelation(const Left& l, const Right& r) + { + auto lItr = mL2R.insert(l, nullptr); + auto rItr = mR2L.insert(r, nullptr); + lItr.value() = &rItr.key(); + rItr.value() = &lItr.key(); + return const_iterator(lItr); + } + +public: + iterator begin() { return iterator(mL2R.begin()); } + const_iterator begin() const { return constBegin(); } + const_iterator cbegin() const { return constBegin(); } + const_iterator constBegin() const { return const_iterator(mL2R.cbegin()); } + iterator end() { return iterator(mL2R.end()); } + const_iterator end() const { return constEnd(); } + const_iterator cend() const { return constEnd(); } + const_iterator constEnd() const { return const_iterator(mL2R.cend()); } + const_iterator constFind(const Left& l) const requires asymmetric_bimap { return constFindLeft(l); } + const_iterator constFind(const Right& r) const requires asymmetric_bimap { return constFindRight(r); } + const_iterator constFindLeft(const Left& l) const { return const_iterator(mL2R.constFind(l)); } + + const_iterator constFindRight(const Right& r) const + { + auto rItr = mR2L.constFind(r); + return const_iterator(rItr != mR2L.cend() ? mL2R.constFind(*(*rItr)) : mL2R.cend()); + } + + iterator find(const Left& l) requires asymmetric_bimap { return findLeft(l); } + const_iterator find(const Left& l) const requires asymmetric_bimap { return findLeft(l); } + iterator find(const Right& r) requires asymmetric_bimap { return findRight(r); } + const_iterator find(const Right& r) const requires asymmetric_bimap { return findRight(r); } + iterator findLeft(const Left& l) { return iterator(constFindLeft(l)); } + const_iterator findLeft(const Left& l) const { return const_iterator(constFindLeft(l)); } + iterator findRight(const Right& r) { return iterator(constFindRight(r)); } + const_iterator findRight(const Right& r) const { return const_iterator(constFindRight(r)); } + + const_iterator erase(const_iterator pos) + { + auto lItr = pos.mPItr; + auto rItr = mR2L.constFind(pos.right()); + mR2L.erase(rItr); + return const_iterator(mL2R.erase(lItr)); + } + + void insert(const Bimap& other) + { + if(this == &other) + return; + + for(auto it = other.begin(); it != other.end(); it++) + insert(it.left(), it.right()); + } + + const_iterator insert(const Left& l, const Right& r) + { + if(auto itr = existingRelation(l, r); itr != cend()) + return itr; + + removeCrossReferences(l, r); + return addOrUpdateRelation(l, r); + } + + bool containsLeft(const Left& l) const { return mL2R.contains(l); } + bool containsRight(const Right& r) const { return mR2L.contains(r); } + + Right fromLeft(const Left& l) const + { + return mL2R.contains(l) ? *mL2R[l] : Right(); + } + + Right fromLeft(const Left& l, const Right& defaultValue) const + { + return mL2R.contains(l) ? *mL2R[l] : defaultValue; + } + + Left fromRight(const Right& l) const + { + return mR2L.contains(l) ? *mR2L[l] : Left(); + } + + Left fromRight(const Right& l, const Left& defaultValue) const + { + return mR2L.contains(l) ? *mR2L[l] : defaultValue; + } + + Right from(const Left& l) const requires asymmetric_bimap { return fromLeft(l); } + Right from(const Left& l, const Right& defaultValue) const requires asymmetric_bimap { return fromLeft(l, defaultValue); } + Left from(const Right& r) const requires asymmetric_bimap { return fromRight(r); } + Left from(const Right& r, const Left& defaultValue) const requires asymmetric_bimap { return fromRight(r, defaultValue); } + + Left toLeft(const Right& r) const { return fromRight(r); } + Left toLeft(const Right& r, const Left& defaultValue) const { return fromRight(r, defaultValue); } + Right toRight(const Left& l) const { return fromLeft(l); } + Right toRight(const Left& l, const Right& defaultValue) const { return fromLeft(l, defaultValue); } + + bool remove(const Left& l) { return removeLeft(l); } + bool remove(const Right& r) { return removeRight(r); } + bool removeLeft(const Left& l) { return remove(mL2R, mR2L, l); } + bool removeRight(const Right& r) { return remove(mR2L, mL2R, r); } + + template + requires bimap_predicate + qsizetype removeIf(Predicate pred) { return _QxBimapPrivate::_erase_if(*this, pred); } + + Right takeRight(const Left& l) + { + Right r = fromLeft(l); + removeLeft(l); + return r; + } + + Left takeLeft(const Right& r) + { + Left l = fromRight(r); + removeRight(r); + return l; + } + + Right take(const Left& l) requires asymmetric_bimap { return takeRight(l); } + Left take(const Right& r) requires asymmetric_bimap { return takeLeft(r); } + + void swap(Bimap& other) + { + mL2R.swap(other.mL2R); + mR2L.swap(other.mR2L); + } + + qsizetype size() const { return mL2R.size(); } + qsizetype count() const { return size(); } + bool isEmpty() const { return size() == 0; } + bool empty() const { return isEmpty(); } + float load_factor() const { return mL2R.load_factor(); } + + qsizetype capacity() const { return mL2R.capacity(); } + void clear() { mL2R.clear(); mR2L.clear(); } + void reserve(qsizetype size) { mL2R.reserve(size); mR2L.reserve(size); } + void squeeze() { mL2R.squeeze(); mR2L.squeeze(); } + + QList lefts() const { return mL2R.keys(); } + QList rights() const { return mR2L.keys(); } + + // Doc'ed here cause doxygen struggles with this one being separate + /*! + * Returns a list containing all of relationships in the bimap, in an arbitrary order. + * + * This function creates a new list, in linear time. The time and memory use that entails can be avoided + * by iterating from begin() to end(). + * + * @sa lefts() and rights(). + */ + QList> relationships() const + { + QList> rel; + for(auto [k, v] : mL2R.asKeyValueRange()) + rel.append(std::make_pair(k, *v)); + } + +//-Operators--------------------------------------------------------------------------------------------- +public: + /* TODO: Having non-const versions of these that return a reference would require + * const_cast<>'ing away the constness of the key of the "other" map, and I'm not + * sure if modifying that reference directly instead of using QHash's methods would + * break the hash or not. + */ + Right operator[](const Left& l) const requires asymmetric_bimap + { + /* Alternatively these [] operators could insert a default constructed pair opposite if the + * key is not found, like QHash::operator[]() does by using our insert function (to handle + * both maps), but for now we do this. + */ + if(!mL2R.contains(l)) + throw std::invalid_argument("Access into bimap with a value it does not contain!"); + return *mL2R[l]; + } + + Left operator[](const Right& r) const requires asymmetric_bimap + { + if(!mR2L.contains(r)) + throw std::invalid_argument("Access into bimap with a value it does not contain!"); + return *mR2L[r]; + } + + bool operator==(const Bimap& other) const + { + const auto& oL2R = other.mL2R; + for (auto [l, rp] : mL2R.asKeyValueRange()) + if(!oL2R.contains(l) || *rp != *oL2R[l]) + return false; + + return true; + } + + bool operator!=(const Bimap& other) const = default; +}; + +// Doc'ed here cause doxygen struggles with this one being separate +/*! + * Removes all elements for which the predicate pred returns true from the bimap. + * + * The function supports predicates which take either an argument of type Bimap::const_iterator, + * or an argument of type std::pair. + * + * Returns the number of elements removed, if any. + */ +template +qsizetype erase_if(Bimap& bimap, Predicate pred) { return _QxBimapPrivate::_erase_if(bimap, pred); } + +} + +#endif // QX_BIMAP_H diff --git a/lib/core/include/qx/core/qx-error.h b/lib/core/include/qx/core/qx-error.h index 65b97e44..ac5268fc 100644 --- a/lib/core/include/qx/core/qx-error.h +++ b/lib/core/include/qx/core/qx-error.h @@ -114,7 +114,7 @@ class QX_CORE_EXPORT Error public: bool operator==(const Error& other) const = default; bool operator!=(const Error& other) const = default; - operator bool() const; + explicit operator bool() const; //-Friend Functions------------------------------------------------------------------------------------------------ friend QTextStream& ::operator<<(QTextStream& ts, const Error& e); diff --git a/lib/core/include/qx/core/qx-exclusiveaccess.h b/lib/core/include/qx/core/qx-exclusiveaccess.h index 6839f847..105c85dc 100644 --- a/lib/core/include/qx/core/qx-exclusiveaccess.h +++ b/lib/core/include/qx/core/qx-exclusiveaccess.h @@ -10,7 +10,7 @@ class QRecursiveMutex; namespace Qx { -template +template requires any_of class ExclusiveAccess { diff --git a/lib/core/include/qx/core/qx-flatmultiset.h b/lib/core/include/qx/core/qx-flatmultiset.h new file mode 100644 index 00000000..e6c4b0a5 --- /dev/null +++ b/lib/core/include/qx/core/qx-flatmultiset.h @@ -0,0 +1,297 @@ +#ifndef QX_FLATMULTISET_H +#define QX_FLATMULTISET_H + +// Standard Library Includes +#include +#include + +// Qt Includes +#include + +namespace Qx +{ + +template> + requires std::predicate +class FlatMultiSet; + +template +concept flatmultiset_predicate = std::predicate; + +} + +/*! @cond */ +namespace _QxPrivate +{ + +template +qsizetype _erase_if(Qx::FlatMultiSet& fms, Predicate& pred) +{ + // TODO: Collect this and other copies in the various containers into one reuseable function/file + using Iterator = typename Qx::FlatMultiSet::iterator; + + typename Qx::FlatMultiSet::size_type result = 0; + + Iterator it = fms.begin(); + while(it != fms.end()) + { + if(pred(*it)) + { + ++result; + it = fms.erase(it); + } + else + ++it; + } + + return result; +} + +} +/*! @endcond */ + +namespace Qx +{ + +template + requires std::predicate +class FlatMultiSet +{ +//-Aliases---------------------------------------------------------------------------------------------------------- +private: + using Container = QList; // Makes it easier to move to a user-provided container later, if desired + +public: + using const_iterator = typename Container::const_iterator; + using iterator = const_iterator; // No modifications allowed + using ConstIterator = const_iterator; + using Iterator = iterator; + using const_pointer = typename Container::const_pointer; + using const_reference = typename Container::const_reference; + using const_reverse_iterator = typename Container::const_reverse_iterator; + using ConstReverseIterator = const_reverse_iterator; + using difference_type = typename Container::difference_type; + using pointer = typename Container::pointer; + using reference = typename Container::reference; + using reverse_iterator = const_reverse_iterator; // No modifications allowed + using ReverseIterator = reverse_iterator; + using size_type = typename Container::size_type; + using key_type = typename Container::value_type; + using value_type = typename Container::value_type; + +//-Instance Variables------------------------------------------------------------------------------------------- +private: + Compare mCompare; + Container mContainer; + +//-Constructor-------------------------------------------------------------------------------------------------- +public: + FlatMultiSet() = default; + + FlatMultiSet(std::initializer_list list) + { + mContainer.reserve(list.size()); + for(const auto& e : list) + insert(e); + } + + template + FlatMultiSet(InputIterator first, InputIterator last) + { + mContainer.reserve(std::distance(first, last)); + while(first != last) + { + insert(*first); + ++first; + } + } + + // Query/Info + bool contains(const FlatMultiSet& other) const + { + for(const auto& e : other) + if(!contains(e)) + return false; + + return true; + } + + bool contains(const T& value) const { return std::binary_search(cbegin(), cend(), value); } + qsizetype count() const { return size(); } + qsizetype size() const { return mContainer.size(); } + bool empty() const { return isEmpty(); } + bool isEmpty() const { return mContainer.isEmpty(); } + //bool intersects(const FlatMultiSet& other) const { IMPLEMENT; } // Questionable for multi-set + const T& first() const { return constFirst(); } + const T& constFirst() const { return mContainer.constFirst(); } + const T& last() const { return constLast(); } + const T& constLast() const { return mContainer.constLast(); } + + // Optimization + qsizetype capacity() const { return mContainer.capacity(); } + void reserve(qsizetype size) { mContainer.reserve(size); } + void squeeze() { mContainer.squeeze(); } + + // Iterators + iterator begin() const { return constBegin(); } + const_iterator cbegin() const { return constBegin(); } + const_iterator constBegin() const { return mContainer.cbegin(); } + iterator end() const { return constEnd(); } + const_iterator cend() const { return constEnd(); } + const_iterator constEnd() const { return mContainer.cend(); } + reverse_iterator rbegin() const { return constReverseBegin(); } + const_reverse_iterator crbegin() const { return constReverseBegin(); } + const_reverse_iterator constReverseBegin() const { return mContainer.crbegin(); } + iterator rend() const { return constReverseEnd(); } + const_reverse_iterator crend() const { return constReverseEnd(); } + const_reverse_iterator constReverseEnd() const { return mContainer.crend(); } + const_iterator find(const T& value) const { return constFind(value); } + + const_iterator constFind(const T& value) const + { + /* Weidly, there is no std binary search that returns an iterator. + * There is std::lower_bound but that's not the same as it will + * take extra steps to ensure it finds the first occurance of the + * value, whereas here we just want to return the first, in terms + * of search progression, match, if any. + */ + auto first = cbegin(); + auto last = cend(); + + while(first < last) + { + auto mid = first + (last - first) / 2; + if(mCompare(*mid, value)) + first = mid + 1; // Nugde towards upper bound + else if(mCompare(value, *mid)) + last = mid; // Nudge towards lower bound + else + mid; // Match + } + + return cend(); // No match + } + + iterator erase(const_iterator pos) { return mContainer.erase(pos); } + + std::pair equal_range(const T& value) const + { + return std::make_pair(lowerBound(), upperBound()); + } + + const_iterator lowerBound(const T& value) const { return std::lower_bound(cbegin(), cend(), value, mCompare); } + const_iterator upperBound(const T& value) const { return std::upper_bound(cbegin(), cend(), value, mCompare); } + + // Modification + void clear() { mContainer.clear(); } + iterator insert(const T& value) { return emplace(value); } + iterator insert(const_iterator pos, const T& value) { return emplace(pos, value); } + + // TODO: Move insert/other methods with move arg + template + iterator emplace(Args&&... args) + { + T value(std::forward(args)...); + mContainer.insert(upperBound(value), std::move(value)); + } + + template + iterator emplace(const_iterator pos, Args&&... args) + { + T value(std::forward(args)...); + + if(mContainer.isEmpty()) + return mContainer.insert(0, std::move(value)); + + // 'pos' is supposed to be the position just after the insert + bool nextAfter = pos == cend() || mCompare(value, *pos); + bool previousBeforeOrSame = pos == cbegin() || !mCompare(value, *std::prev(pos)); + + // If hint is wrong, find real pos using the hint as a starting point + if(!nextAfter || !previousBeforeOrSame) + { + /* - If !nextAfter, hint is too early, so check from there to end. + * - If !previousBeforeOrSame, hint is too late, check from begining to there. + */ + Q_ASSERT(nextAfter || previousBeforeOrSame); // Both should never fault simultaneously, or else the container is unsorted + pos = !nextAfter ? std::upper_bound(pos, cend(), value, mCompare) : std::upper_bound(cbegin(), pos, value, mCompare); + } + + return mContainer.insert(pos, std::move(value)); + } + + //FlatMultiSet& intersect(const FlatMultiSet& other); // Quetionable for multi-set + + qsizetype remove(const T& value) + { + qsizetype removed = 0; + + auto itr = lowerBound(value); + while(*itr == value) + { + itr = erase(itr); + ++removed; + } + + return removed; + } + + template + requires flatmultiset_predicate + qsizetype removeIf(Predicate pred) { return _QxPrivate::_erase_if(*this, pred); } + + //FlatMultiSet& subtract(const FlatMultiSet& other); // Quetionable for multi-set + + void swap(FlatMultiSet& other) { mContainer.swap(other.mContainer); } + + //FlatMultiSet& unite(const FlatMultiSet& other); // Quetionable for multi-set + + QList values() const { return mContainer; }; + + // Operators + inline bool operator==(const FlatMultiSet& other) const = default; + + // These don't necessarily make sense for a multi-set + // inline FlatMultiSet& operator&=(const FlatMultiSet& other) { return intersect(other); } + + // inline FlatMultiSet& operator&=(const T& value) + // { + // FlatMultiSet result; + // if(contains(value)) + // result.insert(value); + // return (*this = result); + // } + // inline FlatMultiSet& operator+=(const FlatMultiSet& other) { return unite(other); } + // inline FlatMultiSet& operator+=(const T& value) { insert(value); return *this; } + // inline FlatMultiSet& operator-=(const FlatMultiSet& other) { return subtract(other); } + // inline FlatMultiSet& operator-=(const T& value) { remove(value); return *this; } + // inline FlatMultiSet& operator<<(const T& value) { insert(value); return *this; } + // inline FlatMultiSet& operator|=(const FlatMultiSet& other) { return unite(other); } + // inline FlatMultiSet& operator|=(const T& value) { insert(value); return *this; } + + // friend FlatMultiSet operator|(const FlatMultiSet& lhs, const FlatMultiSet& rhs) { return FlatMultiSet(lhs) |= rhs; } + // friend FlatMultiSet operator|(FlatMultiSet&& lhs, const FlatMultiSet& rhs) { lhs |= rhs; return std::move(lhs); } + // friend FlatMultiSet operator&(const FlatMultiSet& lhs, const FlatMultiSet& rhs) { return FlatMultiSet(lhs) &= rhs; } + // friend FlatMultiSet operator&(FlatMultiSet&& lhs, const FlatMultiSet& rhs) { lhs &= rhs; return std::move(lhs); } + // friend FlatMultiSet operator+(const FlatMultiSet& lhs, const FlatMultiSet& rhs) { return FlatMultiSet(lhs) += rhs; } + // friend FlatMultiSet operator+(FlatMultiSet&& lhs, const FlatMultiSet& rhs) { lhs += rhs; return std::move(lhs); } + // friend FlatMultiSet operator-(const FlatMultiSet& lhs, const FlatMultiSet& rhs) { return FlatMultiSet(lhs) -= rhs; } + // friend FlatMultiSet operator-(FlatMultiSet&& lhs, const FlatMultiSet& rhs) { lhs -= rhs; return std::move(lhs); } +}; + +// Doc'ed here cause doxygen struggles with this one being separate +/*! + * Removes all elements for which the predicate pred returns true from the lopmap. + * + * The function supports predicates which take either an argument of type Lopmap::const_iterator, + * or an argument of type std::pair. + * + * Returns the number of elements removed, if any. + */ +template + requires flatmultiset_predicate +qsizetype erase_if(FlatMultiSet& flatmultiset, Predicate pred) { return _QxPrivate::_erase_if(flatmultiset, pred); } + +} + +#endif // QX_FLATMULTISET_H diff --git a/lib/core/include/qx/core/qx-index.h b/lib/core/include/qx/core/qx-index.h index 14fb1ca5..578c4bd1 100644 --- a/lib/core/include/qx/core/qx-index.h +++ b/lib/core/include/qx/core/qx-index.h @@ -51,7 +51,7 @@ class Index break; default: - qCritical("Invalid extent"); + qFatal("Invalid extent"); } } diff --git a/lib/core/include/qx/core/qx-json.h b/lib/core/include/qx/core/qx-json.h index 79b254ea..aa2ee475 100644 --- a/lib/core/include/qx/core/qx-json.h +++ b/lib/core/include/qx/core/qx-json.h @@ -154,7 +154,7 @@ namespace Qx class QX_CORE_EXPORT JsonError final : public AbstractError<"Qx::JsonError", 5> { - //-Class Enums------------------------------------------------------------- +//-Class Enums------------------------------------------------------------- public: enum Form { @@ -162,37 +162,39 @@ class QX_CORE_EXPORT JsonError final : public AbstractError<"Qx::JsonError", 5> MissingKey, TypeMismatch, EmptyDoc, + InvalidValue, MissingFile, InaccessibleFile, FileReadError, FileWriteError }; - //-Class Variables------------------------------------------------------------- +//-Class Variables------------------------------------------------------------- private: static inline const QHash ERR_STRINGS{ {NoError, u""_s}, {MissingKey, u"The key does not exist."_s}, {TypeMismatch, u"Value type mismatch."_s}, {EmptyDoc, u"The document is empty."_s}, + {InvalidValue, u"Invalid value for type."_s}, {MissingFile, u"File does not exist."_s}, {InaccessibleFile, u"Cannot open the file."_s}, {FileReadError, u"File read error."_s}, {FileWriteError, u"File write error."_s} }; - //-Instance Variables------------------------------------------------------------- +//-Instance Variables------------------------------------------------------------- private: QString mAction; Form mForm; QList mContext; - //-Constructor----------------------------------------------------------------- +//-Constructor----------------------------------------------------------------- public: JsonError(); JsonError(const QString& a, Form f); - //-Instance Functions------------------------------------------------------------- +//-Instance Functions------------------------------------------------------------- private: quint32 deriveValue() const override; QString derivePrimary() const override; @@ -209,57 +211,6 @@ class QX_CORE_EXPORT JsonError final : public AbstractError<"Qx::JsonError", 5> } // namespace Qx -/*! @cond */ -namespace QxJsonPrivate -{ -//-Namespace Variables--------------------------------------------------- -static inline const QString ERR_CONV_TYPE = u"JSON Error: Converting value to %1"_s; -static inline const QString ERR_NO_KEY = u"JSON Error: Could not retrieve key '%1'."_s; -static inline const QString ERR_PARSE_DOC = u"JSON Error: Could not parse JSON document."_s; -static inline const QString ERR_READ_FILE = u"JSON Error: Could not read JSON file."_s; -static inline const QString ERR_WRITE_FILE = u"JSON Error: Could not write JSON file."_s; - -//-Structs--------------------------------------------------------------- -template -struct MemberMetadata -{ - constexpr static Qx::StringLiteral M_NAME = MemberN; - typedef MemberT M_TYPE; - MemberT Struct::* mPtr; -}; - -//-Functions------------------------------------------------------------- -template -constexpr MemberMetadata makeMemberMetadata(T S::*memberPtr) -{ - return {memberPtr}; -} - -template [[maybe_unused]] static inline QString typeString() = delete; -template [[maybe_unused]] static inline bool isType(const QJsonValue& v) = delete; -template [[maybe_unused]] static inline T toType(const QJsonValue& v) = delete; - -template<> inline QString typeString() { return u"bool"_s; }; -template<> inline QString typeString() { return u"double"_s; }; -template<> inline QString typeString() { return u"string"_s; }; -template<> inline QString typeString() { return u"array"_s; }; -template<> inline QString typeString() { return u"object"_s; }; - -template<> inline bool isType(const QJsonValue& v) { return v.isBool(); }; -template<> inline bool isType(const QJsonValue& v) { return v.isDouble(); }; -template<> inline bool isType(const QJsonValue& v) { return v.isString(); }; -template<> inline bool isType(const QJsonValue& v) { return v.isArray(); }; -template<> inline bool isType(const QJsonValue& v) { return v.isObject(); }; - -template<> inline bool toType(const QJsonValue& v) { return v.toBool(); }; -template<> inline double toType(const QJsonValue& v) { return v.toDouble(); }; -template<> inline QString toType(const QJsonValue& v) { return v.toString(); }; -template<> inline QJsonArray toType(const QJsonValue& v) { return v.toArray(); }; -template<> inline QJsonObject toType(const QJsonValue& v) { return v.toObject(); }; - -} // namespace QxJsonPrivate -/*! @endcond */ - namespace QxJson { //-Structs--------------------------------------------------------------- @@ -333,6 +284,51 @@ concept json_optional = Qx::specializes && /*! @cond */ namespace QxJsonPrivate { +//-Namespace Variables--------------------------------------------------- +static inline const QString ERR_CONV_TYPE = u"JSON Error: Converting value to %1"_s; +static inline const QString ERR_NO_KEY = u"JSON Error: Could not retrieve key '%1'."_s; +static inline const QString ERR_PARSE_DOC = u"JSON Error: Could not parse JSON document."_s; +static inline const QString ERR_READ_FILE = u"JSON Error: Could not read JSON file."_s; +static inline const QString ERR_WRITE_FILE = u"JSON Error: Could not write JSON file."_s; + +//-Structs--------------------------------------------------------------- +template +struct MemberMetadata +{ + constexpr static Qx::StringLiteral M_NAME = MemberN; + typedef MemberT M_TYPE; + MemberT Struct::* mPtr; +}; + +//-Functions------------------------------------------------------------- +template +constexpr MemberMetadata makeMemberMetadata(T S::*memberPtr) +{ + return {memberPtr}; +} + +template [[maybe_unused]] static inline QString typeString() = delete; +template [[maybe_unused]] static inline bool isType(const QJsonValue& v) = delete; +template [[maybe_unused]] static inline T toType(const QJsonValue& v) = delete; + +template<> inline QString typeString() { return u"bool"_s; }; +template<> inline QString typeString() { return u"double"_s; }; +template<> inline QString typeString() { return u"string"_s; }; +template<> inline QString typeString() { return u"array"_s; }; +template<> inline QString typeString() { return u"object"_s; }; + +template<> inline bool isType(const QJsonValue& v) { return v.isBool(); }; +template<> inline bool isType(const QJsonValue& v) { return v.isDouble(); }; +template<> inline bool isType(const QJsonValue& v) { return v.isString(); }; +template<> inline bool isType(const QJsonValue& v) { return v.isArray(); }; +template<> inline bool isType(const QJsonValue& v) { return v.isObject(); }; + +template<> inline bool toType(const QJsonValue& v) { return v.toBool(); }; +template<> inline double toType(const QJsonValue& v) { return v.toDouble(); }; +template<> inline QString toType(const QJsonValue& v) { return v.toString(); }; +template<> inline QJsonArray toType(const QJsonValue& v) { return v.toArray(); }; +template<> inline QJsonObject toType(const QJsonValue& v) { return v.toObject(); }; + //-Functions------------------------------------------------------------- /* These helpers are required as a form on indirection within * diff --git a/lib/core/include/qx/core/qx-lopmap.h b/lib/core/include/qx/core/qx-lopmap.h new file mode 100644 index 00000000..1727979b --- /dev/null +++ b/lib/core/include/qx/core/qx-lopmap.h @@ -0,0 +1,477 @@ +#ifndef QX_LOPMAP_H +#define QX_LOPMAP_H + +// Standard Library Includes +#include +#include + +// Extra-component Includes +#include + +// TODO: When C++23 is used, this is a good candidate for flat_multiset over multiset + +namespace Qx +{ + +template> + requires std::predicate +class Lopmap; + +template +concept lopmap_iterator_predicate = defines_call_for_s::const_iterator>; + +template +concept lopmap_pair_predicate = defines_call_for_s>; + +template +concept lopmap_predicate = lopmap_iterator_predicate || lopmap_pair_predicate; + +} + +/*! @cond */ +namespace _QxPrivate +{ + +template +qsizetype _erase_if(Qx::Lopmap& lm, Predicate& pred) +{ + // This could be moved to a private file and made more generic for any other container types if needed, + // though its tricky since here for the pair predicate we pass a const T + using Iterator = typename Qx::Lopmap::iterator; + + typename Qx::Lopmap::size_type result = 0; + + Iterator it = lm.cbegin(); + const Iterator end = lm.cend(); + while(it != end) + { + if constexpr(Qx::lopmap_iterator_predicate) + { + if(pred(it)) + { + it = lm.erase(it); + result++; + } + else + it++; + } + else if constexpr(Qx::lopmap_pair_predicate) + { + if(pred(std::move(*it))) + { + it = lm.erase(it); + result++; + } + else + it++; + } + else + { + // Delayed evaluation trick to prevent immediate static_assert failure for older compilers from before the resolution of CWG 2518 + static_assert(sizeof(Qx::Lopmap) == 0, "Invalid Predicate"); + } + } + + return result; +} + +} +/*! @endcond */ + +namespace Qx +{ + +template + requires std::predicate +class Lopmap +{ +//-Inner Classes---------------------------------------------------------------------------------------------------- +private: + struct Data + { + const Key* keyPtr; + T value; + }; + + struct DataCompare + { + Compare cmp; + bool operator()(const Data& lhs, const Data& rhs) const { return cmp(lhs.value, rhs.value); } + }; + +//-Aliases---------------------------------------------------------------------------------------------------------- +private: + using StorageContainer = std::multiset; + using StorageItr = typename StorageContainer::const_iterator; + using StorageRevItr = typename StorageContainer::const_reverse_iterator; + using LookupContainer = std::unordered_map; + +public: + class const_iterator + { + friend class Lopmap; + //-Aliases------------------------------------------------------------------------------------------------------ + public: + using iterator_category = typename StorageItr::iterator_category; + + //-Instance Variables------------------------------------------------------------------------------------------- + private: + StorageItr mStorageItr; + + //-Constructor-------------------------------------------------------------------------------------------------- + private: + const_iterator(const StorageItr& sItr) : mStorageItr(sItr) {} + + public: + const_iterator() {} + + //-Instance Functions------------------------------------------------------------------------------------------- + public: + const Key& key() const { return *mStorageItr->keyPtr; } + const T& value() const { return mStorageItr->value; } + + //-Operators--------------------------------------------------------------------------------------------- + public: + bool operator==(const const_iterator& other) const = default; + const T& operator*() const { return value(); } + const_iterator& operator++() { mStorageItr++; return *this; } + + const_iterator operator++(int) + { + auto cur = *this; + mStorageItr++; + return cur; + } + + const_iterator& operator--() { mStorageItr--; return *this; } + + const_iterator operator--(int) + { + auto cur = *this; + mStorageItr--; + return cur; + } + + const T* operator->() const { return &mStorageItr->value; } + }; + + // Could be greatly simplified with the above by using a base type, but that makes documentation hard... + class const_reverse_iterator + { +/*! @cond */ + friend class Lopmap; + //-Aliases------------------------------------------------------------------------------------------------------ + public: + using iterator_category = typename StorageRevItr::iterator_category; + + //-Instance Variables------------------------------------------------------------------------------------------- + private: + StorageRevItr mStorageItr; + + //-Constructor-------------------------------------------------------------------------------------------------- + private: + const_reverse_iterator(const StorageRevItr& sItr) : mStorageItr(sItr) {} + + public: + const_reverse_iterator() {} + + //-Instance Functions------------------------------------------------------------------------------------------- + public: + const Key& key() const { return *mStorageItr->keyPtr; } + const T& value() const { return mStorageItr->value; } + + //-Operators--------------------------------------------------------------------------------------------- + public: + bool operator==(const const_reverse_iterator& other) const = default; + const T& operator*() const { return value(); } + const_reverse_iterator& operator++() { mStorageItr++; return *this; } + + const_reverse_iterator operator++(int) + { + auto cur = *this; + mStorageItr++; + return cur; + } + + const_reverse_iterator& operator--() { mStorageItr--; return *this; } + + const_reverse_iterator operator--(int) + { + auto cur = *this; + mStorageItr--; + return cur; + } + + const T* operator->() const { return &mStorageItr->value; } +/*! @endcond */ + }; + +//-Aliases (cont.)------------------------------------------------------------------------------------------------- +public: + using iterator = const_iterator; + using ConstIterator = const_iterator; + using Iterator = iterator; + using reverse_iterator = const_reverse_iterator; + using ConstReverseIterator = const_reverse_iterator; + using ReverseIterator = reverse_iterator; + using difference_type = typename StorageContainer::difference_type; + using key_type = Key; + using mapped_Type = T; + using size_type = typename StorageContainer::size_type; + using value_compare = Compare; + +//-Instance Variables------------------------------------------------------------------------------------------- +private: + StorageContainer mStorage; + LookupContainer mLookup; + +//-Constructor-------------------------------------------------------------------------------------------------- +public: + Lopmap() {} + + Lopmap(std::initializer_list> list) + { + for(auto it = list.begin(); it != list.end(); ++it) + insert(it->first, it->second); + } + +//-Instance Functions------------------------------------------------------------------------------------------- +private: + StorageItr lookupStorage(const Key& key) const + { + auto lItr = mLookup.find(key); + return lItr != mLookup.end() ? lItr->second : mStorage.cend(); + } + + iterator insert_impl(const Key& key, const T& value, const StorageItr* hint) + { + /* Insert key to lookup map if missing, and get iterator to the element regardless. The + * inserted storage iterator is null since it will be updated in a moment either way. + */ + auto [lItr, isNew] = mLookup.emplace(key, StorageItr()); + + if(!isNew) + { + // Don't do anything if the value is the same as the current, to avoid iterator invalidation + if(lItr->second->value == value) + return iterator(lItr->second); + + // Otherwise, we have to erase and re-insert, which unfortunately invalidates iterators pointing to key + mStorage.erase(lItr->second); + } + + // Store the new data + auto sItr = hint ? mStorage.emplace_hint(*hint, &lItr->first, value) : mStorage.emplace(&lItr->first, value); + + // Synchronize map + lItr->second = sItr; + + return iterator(sItr); + } + +public: + iterator begin() { return iterator(mStorage.begin()); } + const_iterator begin() const { return constBegin(); } + const_iterator cbegin() const { return constBegin(); } + const_iterator constBegin() const { return const_iterator(mStorage.cbegin()); } + iterator end() { return iterator(mStorage.end()); } + const_iterator end() const { return constEnd(); } + const_iterator cend() const { return constEnd(); } + const_iterator constEnd() const { return const_iterator(mStorage.cend()); } + reverse_iterator rbegin() { return reverse_iterator(mStorage.rbegin()); } + const_reverse_iterator rbegin() const { return constReverseBegin(); } + const_reverse_iterator crbegin() const { return constReverseBegin(); } + const_reverse_iterator constReverseBegin() const { return const_reverse_iterator(mStorage.crbegin()); } + reverse_iterator rend() { return reverse_iterator(mStorage.rend()); } + const_reverse_iterator rend() const { return constReverseEnd(); } + const_reverse_iterator crend() const { return constReverseEnd(); } + const_reverse_iterator constReverseEnd() const { return const_reverse_iterator(mStorage.crend()); } + + iterator find(const Key& key) { return iterator(lookupStorage(key)); } + const_iterator find(const Key& key) const { return constFind(key); } + const_iterator constFind(const Key& key) const { return const_iterator(lookupStorage(key)); } + iterator lowerBound(const T& value) { return std::as_const(*this).lowerBound(value); } + + const_iterator lowerBound(const T& value) const + { + // Use dummy key since only value is used for ordering + return const_iterator(mStorage.lower_bound(Data{nullptr, value})); + } + + iterator upperBound(const T& value) { return std::as_const(*this).upperBound(value); } + + const_iterator upperBound(const T& value) const + { + // Use dummy key since only value is used for ordering + return const_iterator(mStorage.upper_bound(Data{nullptr, value})); + } + + std::pair equal_range(const T& value) { return std::as_const(*this).equal_range(value); } + + std::pair equal_range(const T& value) const + { + // Use dummy key since only value is used for ordering + auto sItrPair = mStorage.equal_range(Data{nullptr, value}); + return std::make_pair(sItrPair.first, sItrPair.second); + } + + const T& first() const { Q_ASSERT(!mStorage.empty()); return mStorage.cbegin()->value; } + const Key& firstKey() const { Q_ASSERT(!mStorage.empty()); return *mStorage.cbegin()->keyPtr; } + + const T& last() const { Q_ASSERT(!mStorage.empty()); return mStorage.cend()->value; } + const Key& lastKey() const { Q_ASSERT(!mStorage.empty()); return *mStorage.cend()->keyPtr; } + + Key key(const T& value, const Key& defaultKey = Key()) const + { + for(const auto& data : std::as_const(mStorage)) + if(data.value == value) + return *data.keyPtr; + + return defaultKey; + } + + iterator erase(const_iterator pos) + { + Q_ASSERT(pos != constEnd()); + mLookup.erase(pos.key()); + return iterator(mStorage.erase(pos.mStorageItr)); + } + + iterator erase(const_iterator first, const_iterator last) + { + Q_ASSERT(first != constEnd() && last != constEnd()); + + // Have to iterate manually here to keep containers synced since + // mLookup is traversed in an unspecified order + StorageItr sItr = first.mStorageItr; + while(sItr != last.mStorageItr) + { + mLookup.erase(*sItr->keyPtr); + sItr = mStorage.erase(sItr); + } + + return iterator(sItr); + } + + void insert(Lopmap&& other) + { + // Have to copy due to underlying iterator being const, but original container is emptied + insert(std::as_const(other)); + + other.mStorage.clear(); + other.mLookup.clear(); + } + + void insert(const Lopmap& other) + { + if(this == &other) + return; + + for(auto itr = other.mStorage.begin(); itr != other.mStorage.end(); itr++) + insert(*itr->keyPtr, itr->value); + } + + iterator insert(const Key& key, const T& value) { return insert_impl(key, value, nullptr); } + iterator insert(const_iterator pos, const Key& key, const T& value) { return insert_impl(key, value, &pos.mStorageItr); } + bool contains(const Key& key) const { return mLookup.contains(key); } + + size_type remove(const Key& key) + { + auto lItr = mLookup.find(key); + if(lItr != mLookup.cend()) + { + auto sItr = lItr->second; + mLookup.erase(lItr); + mStorage.erase(sItr); + return 1; + } + + return 0; + } + + template + requires lopmap_predicate + qsizetype removeIf(Predicate pred) { return _QxPrivate::_erase_if(*this, pred); } + + T take(const Key& key) + { + auto lItr = mLookup.find(key); + if(lItr != mLookup.cend()) + { + auto sItr = lItr->second; + T t = sItr->value; + mLookup.erase(lItr); + mStorage.erase(sItr); + return t; + } + + return T(); + } + + void swap(Lopmap& other) + { + mLookup.swap(other.mLookup); + mStorage.swap(other.mStorage); + } + + qsizetype size() const { return mStorage.size(); } + qsizetype count() const { return size(); } + bool isEmpty() const { return size() == 0; } + bool empty() const { return isEmpty(); } + + void clear() { mLookup.clear(); mStorage.clear(); } + + T value(const Key& key, const T& defaultValue = T()) const + { + auto sItr = lookupStorage(key); + return sItr != mStorage.cend() ? sItr->value : defaultValue; + } + + QList keys() const + { + QList ks; + for(const auto& data : std::as_const(mStorage)) + ks.append(*data.keyPtr); + return ks; + } + + QList keys(const T& value) const + { + QList ks; + for(const auto& data : std::as_const(mStorage)) + if(data.value == value) + ks.append(*data.keyPtr); + return ks; + } + + QList values() const + { + QList vs; + for(const auto& data : std::as_const(mStorage)) + vs.append(data.value); + return vs; + } + +//-Operators--------------------------------------------------------------------------------------------- +public: + T operator[](const Key& key) const { return value(key); } + + bool operator==(const Lopmap& other) const { return mStorage == other.mStorage; } + bool operator!=(const Lopmap& other) const = default; +}; + +// Doc'ed here cause doxygen struggles with this one being separate +/*! + * Removes all elements for which the predicate pred returns true from the lopmap. + * + * The function supports predicates which take either an argument of type Lopmap::const_iterator, + * or an argument of type std::pair. + * + * Returns the number of elements removed, if any. + */ +template +qsizetype erase_if(Lopmap& lopmap, Predicate pred) { return _QxPrivate::_erase_if(lopmap, pred); } + +} + +#endif // QX_LOPMMAP_H diff --git a/lib/core/include/qx/core/qx-property.h b/lib/core/include/qx/core/qx-property.h new file mode 100644 index 00000000..17ed4648 --- /dev/null +++ b/lib/core/include/qx/core/qx-property.h @@ -0,0 +1,753 @@ +#ifndef QX_PROPERTY_H +#define QX_PROPERTY_H + +// Shared Lib Support +#include "qx/core/qx_core_export.h" + +// Unit Includes +#include "__private/qx-property_detail.h" + +// Standard Library Includes +#include +#include +#include + +// Qt Includes +#include + +// Extra-component Includes +#include "qx/utility/qx-concepts.h" +#include "qx/utility/qx-helpers.h" + +/* TODO: In general, utilize more non-template base types to reduce code bloat + * and hide away implementation details like how the Qt Bindable Property system + * does + */ + +namespace Qx +{ + +template +class Bindable; + +/* TODO: Ideally, this class should be marked as nodiscard directly, as it prevents the need to repeat the + * diagnostic string on each function that requires an instance of this, and ensures that the diagnostic + * is used in a discard situation even for user functions; however, we must use the C++11 style attribute + * for that here, and that conflicts with our export macro as you cannot mix the old GNU attribute + * syntax with the newer C++11 standard syntax. Unfortunately, CMake's GenerateExportHeader module does not + * support using the newer syntax so for now we're SOL unless we start generating the export header ourselves, + * which should be avoided in the hopes that eventually CMake adds this functionality. + * + * If we really get desperate a hacky workaround could be adding an extra step to search/replace the older + * syntax in the initial header with the newer equivalent, though that's also complicated. + * + * For now we just use nodiscard on each method that returns the type, which dodges the issue since they are + * all in a template class (no export required). + */ +class QX_CORE_EXPORT PropertyNotifier +{ + template + friend class AbstractBindableProperty; + Q_DISABLE_COPY(PropertyNotifier); +//-Aliases------------------------------------------------------------- +private: + using ManagerPtr = std::shared_ptr<_QxPrivate::PropertyObserverManager>; + using WeakManagerPtr = std::weak_ptr<_QxPrivate::PropertyObserverManager>; + using ObserverId = _QxPrivate::PropertyObserverManager::ObserverId; + +//-Instance Variables------------------------------------------------------------- +private: + WeakManagerPtr mManager; + ObserverId mId; + +//-Constructor------------------------------------------------------------- +private: + PropertyNotifier(const ManagerPtr& manager, ObserverId id); + +public: + PropertyNotifier(PropertyNotifier&& other) noexcept; + +//-Destructor------------------------------------------------------------- +public: + ~PropertyNotifier(); + +//-Operators------------------------------------------------------------- +public: + PropertyNotifier& operator=(PropertyNotifier&& other) noexcept; +}; + +template +class PropertyBinding +{ + /* This is basically just a wrapper around std::function for a more application + * specific way to pass around binding function if desired. + */ +//-Instance Variables------------------------------------------------------------- +private: + std::function mFunctor; + +//-Constructor-------------------------------------------------------------------- +public: + PropertyBinding() = default; + + template + PropertyBinding(Functor&& f) : + mFunctor(std::forward(f)) + {} + +//-Instance Functions----------------------------------------------------------- +public: + bool isNull() const { return !static_cast(mFunctor); } + +//-Operator---------------------------------------------------------------------- +public: + explicit operator bool() const { return !isNull(); } + T operator()() const { return mFunctor(); } +}; + +template +class AbstractBindableProperty : private _QxPrivate::BindableInterface +{ + Q_DISABLE_COPY(AbstractBindableProperty); +//-Aliases--------------------------------------------------------------------- +private: + using ObserverManager = _QxPrivate::PropertyObserverManager; + +//-Instance Variables------------------------------------------------------------- +private: + PropertyBinding mBinding; + std::shared_ptr mObserverManager; + // ^ This being dynamic keeps its address stable even when 'this' is moved + +//-Constructor----------------------------------------------------------------- +protected: + /* We cannot handle the intake of a possible binding from a + * derived ctor here as notifyBindingAdded() will lead to + * valueBypassingBindings() being called, which is a call of a + * polymorphic function from within the base class where that function + * is declared. This is UB since construction of the derived instance + * won't have begun at that point yet and so the vtable for the instance + * will just point to the Base and cannot see the derived overload. + * + * So instead, each derived needs to intake the binding and call + * setBinding() manually if it has a ctor that takes a binding function + * + * template> + * AbstractBindableProperty(BindingT&& f = {}) : + * mObserverManager(new ObserverManager), + * mBinding(std::forward(f)) + * { + * if(mBinding) + * notifyBindingAdded(); + * } + */ + AbstractBindableProperty() : + mObserverManager(new ObserverManager) + {} + + AbstractBindableProperty(AbstractBindableProperty&& other) noexcept = default; + +//-Instance Functions----------------------------------------------------------- +private: + inline bool valueSame(const T& value) const + { + if constexpr(defines_equality_s) + return value == valueBypassingBindings(); + + return false; + } + + template + requires std::same_as, T> + bool updateIfDifferent(U&& newValue) + { + if(valueSame(newValue)) + return false; + + setValueBypassingBindings(std::forward(newValue)); + return true; + } + + template + PropertyBinding cycleBinding(Binding&& b) + { + auto oldBinding = std::exchange(mBinding, std::forward(b)); + if(oldBinding) + notifyBindingRemoved(); + if(mBinding) + notifyBindingAdded(); + + return oldBinding; + } + + bool callBinding() override + { + Q_ASSERT(mBinding); + return updateIfDifferent(mBinding()); + } + + void notifyObservers() const override { mObserverManager->invokeAll(); } + +public: + virtual void setValueBypassingBindings(T&& v) = 0; + + /* This is a bit of a blemish. Originally, this was a pure virtual function like the T&& version + * in order to let implementations optimize their copy assignment to the greatest extent possible; + * however, as soon as the method is virtual the compiler tries to instantiate it derivatives always, + * regardless of T, which means that properties would stop working with move-only types (unacceptable). + * Ideally we'd just constrain the virtual method, but even though technically it's possible (GCC allows it), + * it's not allowed by the standard. So instead we implement it here as a manual copy/move, delegating to the + * T&& version. In most cases the compiler should be able to optimize this to be equivalent to a direct copy + * assignment (assuming that's what the derived class did), and in other cases it should be barely more + * expensive. Regardless, this is something to try and work around as soon as any other class hierarchy or + * approach changes make a better way possible, or if the standard every allows constraining virtual methods. + */ + void setValueBypassingBindings(const T& v) requires std::copyable { setValueBypassingBindings(T(v)); } + + virtual const T& valueBypassingBindings() const = 0; + + PropertyBinding binding() const { return mBinding; } + [[nodiscard]] PropertyBinding takeBinding() { return cycleBinding(PropertyBinding()); } + void removeBinding() { if(hasBinding()) Q_UNUSED(takeBinding()); } + + template + PropertyBinding setBinding(Functor&& f) { return cycleBinding(std::forward(f)); } + + PropertyBinding setBinding(const PropertyBinding& binding) { return cycleBinding(binding); } + bool hasBinding() const { return !mBinding.isNull(); } + + const T& value() const + { + attachToCurrentEval(); + return valueBypassingBindings(); + } + + void setValue(const T& newValue) + { + removeBinding(); + if(updateIfDifferent(newValue)) + notifyValueChanged(); + } + + void setValue(T&& newValue) + { + removeBinding(); + if(updateIfDifferent(std::move(newValue))) + notifyValueChanged(); + } + + template + [[nodiscard("The functor will never be called if PropertyNotifier is discarded!")]] PropertyNotifier addNotifier(Functor&& f) const + { + auto id = mObserverManager->add(std::forward(f)); + return PropertyNotifier(mObserverManager, id); + } + + template + void addLifetimeNotifier(Functor&& f) const { mObserverManager->add(std::forward(f)); } + + template + [[nodiscard("The functor will never be called if PropertyNotifier is discarded!")]] PropertyNotifier subscribe(Functor&& f) const + { + f(); + return addNotifier(std::forward(f)); + } + + template + void subscribeLifetime(Functor&& f) const + { + f(); + addLifetimeNotifier(std::forward(f)); + } + +//-Operators------------------------------------------------------------- +protected: + AbstractBindableProperty& operator=(AbstractBindableProperty&& other) noexcept = default; + +public: + decltype(auto) operator->() const requires arrowable_container_type + { + return container_arrow_operator(value()); + } + + const T& operator*() const { return value(); } + operator const T&() const { return value(); } +}; + +} // namespace Qx + +namespace _QxPrivate +{ + +template +class ObjectPropertyAdapter final : private Qx::AbstractBindableProperty +{ + friend class Qx::Bindable; + Q_DISABLE_COPY_MOVE(ObjectPropertyAdapter); + +//-Base Forwards------------------------------------------------------------------ +private: + using Qx::AbstractBindableProperty::setValue; + +//-Instance Variables------------------------------------------------------------- +private: + QObject* mObject; + QMetaProperty mProperty; + ObjectPropertyAdapterLiaison mLiaison; + T mCache; + bool mBlockPropertyUpdate; + + /* NOTE: The guards used here set themselves, perform the operation, and then + * unset themselves. This blocks recursive updates to values (i.e. a second + * update comes in before the fist has unwound and then is blocked), which + * might be of a type that shouldn't actually be blocked( this would be most + * likely to happen due to a user installed callback that fires at the end + * of an update wave); however, I believe that any case in which this occurs + * would be a property dependency cycle, which is not allowed and caught anyway. + * + * But, if it turns out there are valid cases where recursive updates should + * happen here, just switch to a "ignore once" model where the flag is cleared + * right after its checked (if it was high), instead of being cleared by the initial + * caller that set it high (as is now). + */ + +//-Constructor----------------------------------------------------------------- +private: + ObjectPropertyAdapter(QObject* obj, const QMetaProperty& property) : + mObject(nullptr), + mProperty(property), + mBlockPropertyUpdate(false) + { + // Checks + auto adaptedMetaType = QMetaType::fromType(); // works even if the type isn't registered + auto propertyMetaType = property.metaType(); + if(propertyMetaType != adaptedMetaType) + { + qWarning("Qx::ObjectPropertyAdapter: The type of property %s, %s, is not the same as the" + "adapter type, %s.", property.name(), propertyMetaType.name(), adaptedMetaType.name()); + return; + } + + if(!property.hasNotifySignal()) + { + qWarning("Qx::ObjectPropertyAdapter: Property %s has no notify signal.", property.name()); + return; + } + + // Setup + if(Q_UNLIKELY(!mLiaison.configure(obj, property))) + return; + + QObject::connect(&mLiaison, &ObjectPropertyAdapterLiaison::objectDeleted, &mLiaison, [this]{ + /* NOTE: We die when the object we're adapting dies. This means we should + * never leak since the destroyed() signals is never blocked. + * + * SINCE WE SELF-DELETE HERE, DO NOT USE THE OBJECT AFTER THIS IN ANY WAY + */ + delete this; + }); + QObject::connect(&mLiaison, &ObjectPropertyAdapterLiaison::propertyNotified, &mLiaison, [this]{ + handleExternalUpdate(); + }); + + mObject = obj; + mCache = readProperty(); + } + +//-Destructor----------------------------------------------------------------- +public: + ~ObjectPropertyAdapter() + { + if(isValid()) + ObjectPropertyAdapterRegistry::instance()->remove(mObject, mProperty); + } + +//-Class Functions----------------------------------------------------------- +private: + static bool basicInputValidation(QObject* obj, const QMetaProperty& property) + { + /* This does not validate that the property is fully valid to be a bindable, + * but confirms that the inputs are present and that the property at least + * belongs to the object. This is so that we can be sure the inputs are valid + * enough for the purposes of checking if an adapter is already in the store, + * which means we don't need to perform all validation steps if an adapter for + * these inputs was already created. + */ + if(!obj) + { + qWarning("Qx::ObjectPropertyAdapter: Null object provided."); + return false; + } + + if(!property.isValid()) + { + qWarning("Qx::ObjectPropertyAdapter: Invalid property provided."); + return false; + } + + /* Since there is only one MetaObject per type, the address for the provided properties name + * should be identical to the name address if we look it up through the provided object. This + * proves that the provided property is one of the objects properties + */ + if(obj->metaObject()->property(property.propertyIndex()).name() != property.name()) + { + qWarning("Qx::ObjectPropertyAdapter: The provided property does not belong to the provided object."); + return false; + } + + return true; + } + + static ObjectPropertyAdapter* get(QObject* obj, const QMetaProperty& property) + { + if(!basicInputValidation(obj, property)) + return nullptr; + + auto man = ObjectPropertyAdapterRegistry::instance(); + ObjectPropertyAdapter* adptr = static_cast(man->retrieve(obj, property)); + if(!adptr) + { + auto newAdptr = new ObjectPropertyAdapter(obj, property); + if(newAdptr->isValid()) + { + man->store(obj, property, newAdptr); + adptr = newAdptr; + } + else + delete newAdptr; + } + + return adptr; + } + +//-Instance Functions----------------------------------------------------------- +private: + bool isValid() const { return mObject; } + T readProperty() + { + QVariant value = mProperty.read(mObject); + Q_ASSERT(value.isValid() && value.canConvert()); + return value.value(); + } + + void writeProperty(const T& value) + { + if(mBlockPropertyUpdate) + return; + + // When we write to the property, we want to ignore property update notifications obviously + mLiaison.setIgnoreUpdates(true); + bool wrote = mProperty.write(mObject, value); + Q_ASSERT(wrote); + mLiaison.setIgnoreUpdates(false); + } + + void handleExternalUpdate() + { + /* Treat the external update as if one directly use setValue() on this property using the new value, + * but skip updating the underlying Q_PROPERTY + */ + mBlockPropertyUpdate = true; + setValue(readProperty()); + mBlockPropertyUpdate = false; + } + +public: + using Qx::AbstractBindableProperty::setValueBypassingBindings; + void setValueBypassingBindings(T&& v) override + { + mCache = std::move(v); + writeProperty(mCache); + } + + const T& valueBypassingBindings() const override { return mCache; } + bool isPropertyWriteable() const { return mProperty.isWritable(); } +}; + +} // namespace _QxPrivate + +namespace Qx +{ + +template +class Bindable +{ + /* AbstractBindableProperty does the heavy lifting, this is basically just a shell + * that forwards method calls. A little silly, but worth it in order to have a unified + * interface object that does not rely on using pointers in user code (and of course + * this mirrors Qt properties :)). + * + * To be fair, it also accounts for the peculiarities of QObject based properties + * which can be invalid, read-only, etc, and has a specialized constructor that + * hides the QObject specific adapter internally. + */ + +//-Aliases------------------------------------------------------------- +private: + using WrappedProperty = AbstractBindableProperty; + +//-Instance Variables------------------------------------------------------------- +private: + WrappedProperty* mBindable; + bool mReadOnly; + +//-Constructor----------------------------------------------------------------- +public: + Bindable() : + mBindable(nullptr), + mReadOnly(true) + {} + + Bindable(AbstractBindableProperty& bp) : + mBindable(&bp), + mReadOnly(false) + {} + + Bindable(const AbstractBindableProperty& bp) : + mBindable(const_cast*>(&bp)), + mReadOnly(true) + {} + + Bindable(QObject* obj, const QMetaProperty& property) : + mBindable(nullptr), + mReadOnly(true) + { + auto adptr = _QxPrivate::ObjectPropertyAdapter::get(obj, property); + if(!adptr) + return; + + mReadOnly = !adptr->isPropertyWriteable(); + mBindable = adptr; + } + + Bindable(QObject* obj, const char* property) : Bindable(obj, [=]{ + if (!obj) + return QMetaProperty{}; + auto propertyIndex = obj->metaObject()->indexOfProperty(property); + if (propertyIndex < 0) + { + qWarning("Qx::Bindable: No property named %s for QObject bindable (obj = %p).", property, obj); + return QMetaProperty{}; + } + return obj->metaObject()->property(propertyIndex); + }()) + {} + +//-Instance Functions------------------------------------------------------------- +private: + bool mutableCheck() const + { + if(mReadOnly) + { + qWarning("Qx::Bindable: Attempt to write/mutate read-only property through Bindable (%p).", this); + return false; + } + + return true; + } + +public: + // Forwards + void setValueBypassingBindings(const T& v) + { + Q_ASSERT(mBindable); + if(!mutableCheck()) + return; + + mBindable->setValueBypassingBindings(v); + } + + void setValueBypassingBindings(T&& v) + { + Q_ASSERT(mBindable); + if(!mutableCheck()) + return; + + mBindable->setValueBypassingBindings(std::forward(v)); + } + + const T& valueBypassingBindings() const { Q_ASSERT(mBindable); return mBindable->valueBypassingBindings(); } + PropertyBinding binding() const { Q_ASSERT(mBindable); return mBindable->binding(); } + + [[nodiscard]] PropertyBinding takeBinding() + { + Q_ASSERT(mBindable); + if(!mutableCheck()) + return {}; + + return mBindable->takeBinding(); + } + + void removeBinding() + { + Q_ASSERT(mBindable); + if(!mutableCheck()) + return; + + mBindable->removeBinding(); + } + + template + PropertyBinding setBinding(Functor&& f) + { + Q_ASSERT(mBindable); + if(!mutableCheck()) + return {}; + + return mBindable->setBinding(std::forward(f)); + } + + PropertyBinding setBinding(const PropertyBinding& binding) + { + Q_ASSERT(mBindable); + if(!mutableCheck()) + return {}; + + return mBindable->setBinding(binding); + } + + bool hasBinding() const { Q_ASSERT(mBindable); return mBindable->hasBinding(); } + const T& value() const { Q_ASSERT(mBindable); return mBindable->value(); } + + void setValue(const T& newValue) + { + Q_ASSERT(mBindable); + if(!mutableCheck()) + return; + + mBindable->setValue(newValue); + } + + void setValue(T&& newValue) + { + Q_ASSERT(mBindable); + if(!mutableCheck()) + return; + + mBindable->setValue(std::forward(newValue)); + } + + template + [[nodiscard("The functor will never be called if PropertyNotifier is discarded!")]] PropertyNotifier addNotifier(Functor&& f) const + { + Q_ASSERT(mBindable); + return mBindable->addNotifier(std::forward(f)); + } + + template + void addLifetimeNotifier(Functor&& f) const { Q_ASSERT(mBindable); mBindable->addLifetimeNotifier(std::forward(f)); } + + template + [[nodiscard("The functor will never be called if PropertyNotifier is discarded!")]] PropertyNotifier subscribe(Functor&& f) const + { + Q_ASSERT(mBindable); + return mBindable->subscribe(std::forward(f)); + } + + template + void subscribeLifetime(Functor&& f) const { Q_ASSERT(mBindable); mBindable->subscribeLifetime(std::forward(f)); } + + // Bindable specific stuff + bool isValid() const { return mBindable; } + bool isReadOnly() const { return mReadOnly; } + +//-Operators------------------------------------------------------------- +public: + decltype(auto) operator->() const requires defines_member_ptr + { + Q_ASSERT(mBindable); + return mBindable->operator->(); + } + + const T& operator*() const { Q_ASSERT(mBindable); return mBindable->operator*(); } + operator const T&() const { Q_ASSERT(mBindable); return static_cast(*mBindable); } + + Bindable& operator=(T&& newValue) noexcept + { + Q_ASSERT(mBindable); + if(!mutableCheck()) + return *this; + + mBindable->setValue(std::forward(newValue)); + return *this; + } + + Bindable& operator=(const T& newValue) noexcept + { + Q_ASSERT(mBindable); + if(!mutableCheck()) + return *this; + + mBindable->setValue(newValue); + return *this; + } +}; + +template +class Property : public AbstractBindableProperty +{ + // Basic property, basically just wraps data + Q_DISABLE_COPY(Property); +//-Instance Variables------------------------------------------------------------- +private: + T mData; + +//-Constructor----------------------------------------------------------------- +public: + Property() : mData(T()) {} + + // TODO: QProperty can't be moved, should we disallow this? + Property(Property&& other) noexcept { *this = std::move(other); } + + template + Property(Functor&& f) { AbstractBindableProperty::setBinding(std::forward(f)); } + + Property(const PropertyBinding& binding) { AbstractBindableProperty::setBinding(binding); } + + Property(T&& initialValue) : + mData(std::forward(initialValue)) + {} + + Property(const T& initialValue) : + mData(initialValue) + {} + +//-Instance Functions------------------------------------------------------------- +public: + using AbstractBindableProperty::setValueBypassingBindings; + void setValueBypassingBindings(T&& v) override { mData = std::move(v); } + const T& valueBypassingBindings() const override { return mData; } + +//-Operators------------------------------------------------------------- +public: + Property& operator=(Property&& other) noexcept + { + if(&other != this) + { + AbstractBindableProperty::operator=(std::move(other)); // Move base + mData = std::exchange(other.mData, {}); + } + + return *this; + } + + Property& operator=(T&& newValue) { AbstractBindableProperty::setValue(std::forward(newValue)); return *this; } + Property& operator=(const T& newValue) { AbstractBindableProperty::setValue(newValue); return *this; } +}; + +//-Namespace Functions------------------------------------------------------------- +QX_CORE_EXPORT void beginPropertyUpdateGroup(); +QX_CORE_EXPORT void endPropertyUpdateGroup(); + +//-Classes (cont.)---------------------------------------------------------------- +class ScopedPropertyUpdateGroup +{ + Q_DISABLE_COPY_MOVE(ScopedPropertyUpdateGroup); +public: + Q_NODISCARD_CTOR ScopedPropertyUpdateGroup() { beginPropertyUpdateGroup(); } + ~ScopedPropertyUpdateGroup() noexcept(false) { endPropertyUpdateGroup(); } +}; + +} + +#endif // QX_PROPERTY_H diff --git a/lib/core/include/qx/core/qx-setonce.h b/lib/core/include/qx/core/qx-setonce.h index 45e51b6c..60ac4cf6 100644 --- a/lib/core/include/qx/core/qx-setonce.h +++ b/lib/core/include/qx/core/qx-setonce.h @@ -2,7 +2,7 @@ #define QX_SETONCE_H // Standard Library Includes -#include +#include // Extra-component Includes #include @@ -10,20 +10,77 @@ namespace Qx { -template> - requires std::is_assignable_v && Qx::defines_call_for_s -class SetOnce +/* I'd prefer doing this in a way where we don't need to repeat the common implementation + * for each class, but using a base can screw up doxygen, and having optional data members + * is fuggy as of C++20, so for now we just duplicate as needed. + */ + +template +class SetOnce; + +/*! @cond */ + +template + requires std::assignable_from +class SetOnce +{ +//-Instance Members---------------------------------------------------------------------------------------------------- +private: + bool mSet; + T mDefaultValue; + T mValue; + +//-Constructor------------------------------------------------------------------------------------------------------- +public: + SetOnce(T initial = T()) : + mSet(false), + mDefaultValue(initial), + mValue(initial) + {} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +public: + bool isSet() const { return mSet; } + const T& value() const { return mValue; } + + void reset() + { + mSet = false; + mValue = mDefaultValue; + } + + SetOnce& operator=(const T& value) + { + if(!mSet) + { + mValue = value; + mSet = true; + } + + return *this; + } + +//-Operators-------------------------------------------------------------------------------------------------- + const T& operator*() const { return mValue; } + const T* operator->() const { return &mValue; } + explicit operator bool() const { return mSet; } +}; +/*! @endcond */ + +template + requires std::assignable_from && comparator +class SetOnce { //-Instance Members---------------------------------------------------------------------------------------------------- private: - CompareEq mComparator; + C mComparator; bool mSet; T mDefaultValue; T mValue; //-Constructor------------------------------------------------------------------------------------------------------- public: - SetOnce(T initial, const CompareEq& comp = CompareEq()) : + SetOnce(T initial, C&& comp = C()) : mComparator(comp), mSet(false), mDefaultValue(initial), @@ -41,7 +98,7 @@ class SetOnce mValue = mDefaultValue; } - SetOnce& operator=(const T& value) + SetOnce& operator=(const T& value) { if(!mSet && !mComparator(mDefaultValue, value)) { @@ -51,8 +108,15 @@ class SetOnce return *this; } + +//-Operators-------------------------------------------------------------------------------------------------- + const T& operator*() const { return mValue; } + const T* operator->() const { return &mValue; } + explicit operator bool() const { return mSet; } }; + + } #endif // QX_SETONCE_H diff --git a/lib/core/include/qx/core/qx-string.h b/lib/core/include/qx/core/qx-string.h index 2d7984ce..5393b4b4 100644 --- a/lib/core/include/qx/core/qx-string.h +++ b/lib/core/include/qx/core/qx-string.h @@ -64,6 +64,8 @@ class QX_CORE_EXPORT String static QString trimLeading(const QStringView string); static QString trimTrailing(const QStringView string); + + static QString mapArg(QAnyStringView s, const QMap& map, Qt::CaseSensitivity cs = Qt::CaseSensitive); }; } diff --git a/lib/core/include/qx/core/qx-systemsignalwatcher.h b/lib/core/include/qx/core/qx-systemsignalwatcher.h new file mode 100644 index 00000000..b86a0cda --- /dev/null +++ b/lib/core/include/qx/core/qx-systemsignalwatcher.h @@ -0,0 +1,67 @@ +#ifndef QX_SYSTEMSIGNALWATCHER_H +#define QX_SYSTEMSIGNALWATCHER_H + +// Shared Lib Support +#include "qx/core/qx_core_export.h" + +// Qt Includes +#include + +namespace Qx +{ + +class SystemSignalWatcherPrivate; + +class QX_CORE_EXPORT SystemSignalWatcher : public QObject +{ + Q_OBJECT; + Q_DECLARE_PRIVATE(SystemSignalWatcher); + +//-Class Enums------------------------------------------------------------------------------------------------- +public: + /* For now we only support signals that can be mapped cross-platform. In the future we could support more + * with a doc note that the additional signals will never be received outside of Linux + */ + enum Signal + { + None = 0x0, + Interrupt = 0x1, + HangUp = 0x2, + Quit = 0x4, + Terminate = 0x8, + Abort = 0x10, + }; + Q_DECLARE_FLAGS(Signals, Signal); + + +//-Instance Variables------------------------------------------------------------------------------------------------- +private: + std::unique_ptr d_ptr; + +//-Constructor------------------------------------------------------------------------------------------------- +public: + SystemSignalWatcher(); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + ~SystemSignalWatcher(); // Required for d_ptr destructor to compile + +//-Instance Functions-------------------------------------------------------------------------------------------- +public: + void watch(Signals s); + void stop(); + void yield(); + + Signals watching() const; + bool isWatching() const; + bool isRegistered() const; + +//-Signals & Slots------------------------------------------------------------------------------------------------ +signals: + void signaled(Signal s, bool* handled); +}; +Q_DECLARE_OPERATORS_FOR_FLAGS(SystemSignalWatcher::Signals); + +} + +#endif // QX_SYSTEMSIGNALWATCHER_H diff --git a/lib/core/include/qx/core/qx-threadsafesingleton.h b/lib/core/include/qx/core/qx-threadsafesingleton.h new file mode 100644 index 00000000..9da7969d --- /dev/null +++ b/lib/core/include/qx/core/qx-threadsafesingleton.h @@ -0,0 +1,45 @@ +#ifndef QX_THREAD_SAFE_SINGLETON_H +#define QX_THREAD_SAFE_SINGLETON_H + +// Qt Includes +#include +#include + +// Intra-component Includes +#include "qx/core/qx-exclusiveaccess.h" + +// Extra-component Includes +#include "qx/utility/qx-concepts.h" + +namespace Qx +{ + +template + requires any_of +class ThreadSafeSingleton +{ +//-Class Members--------------------------------------------------------------------------------------------- +private: + // Needs to be static so it can be locked before the the singleton is created, or else a race in instance() could occur. + static inline constinit Mutex smMutex; + +//-Constructor---------------------------------------------------------------------------------------------- +protected: + ThreadSafeSingleton() = default; + +//-Class Functions---------------------------------------------------------------------------------------------- +public: + static Qx::ExclusiveAccess instance() + { + static Singleton s; + return Qx::ExclusiveAccess(&s, &smMutex); // Provides locked access to singleton, that unlocks when destroyed + } +}; + +} + +//-Macros---------------------------------------------------------------------------------------------------------- +// Macro to be used in all derivatives +#define QX_THREAD_SAFE_SINGLETON(...) friend ThreadSafeSingleton<__VA_ARGS__> + +#endif // QX_THREAD_SAFE_SINGLETON_H diff --git a/lib/core/src/__private/qx-generalworkerthread.cpp b/lib/core/src/__private/qx-generalworkerthread.cpp new file mode 100644 index 00000000..52efcf99 --- /dev/null +++ b/lib/core/src/__private/qx-generalworkerthread.cpp @@ -0,0 +1,87 @@ +// Unit Include +#include "qx-generalworkerthread.h" + +// Qt Includes +#include + +/*! @cond */ +namespace Qx +{ + +//=============================================================================================================== +// GeneralWorkerThread +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------- +//Public: +GeneralWorkerThread::GeneralWorkerThread() : + mWorkerCount(0) +{ + /* mThread is the only QObject member of this class. We need to move it to the main thread because + * the manager can be created in any thread since it's done by RAII and UB occurs if a Object (in this case + * the QThread) continues to be used if the thread it belongs to is shutdown. moveToThread() already checks + * if this is the main thread and results in a no-op if so. Also, QThread's public methods are protected + * by a mutex, so it's safe to interact with it from which ever thread is accessing the manager. + */ + QThread* mainThread = QCoreApplication::instance()->thread(); + if(!mainThread) [[unlikely]] + { + // It's documented that you're not supposed to use QObjects before QCoreAppliation is created, + // but check explicitly anyway + qFatal("Cannot use QObject's before QCoreApplication is created!"); + } + + mThread.moveToThread(mainThread); +} + +//-Destructor-------------------------------------------------------------------- +//Public: +GeneralWorkerThread::~GeneralWorkerThread() +{ + if(mThread.isRunning()) + stopThread(true); +} + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: +void GeneralWorkerThread::startThread() +{ + Q_ASSERT(!mThread.isRunning()); + mThread.start(QThread::LowPriority); // The work here should be on the lighter side +} + +void GeneralWorkerThread::stopThread(bool wait) +{ + Q_ASSERT(mThread.isRunning()); + mThread.quit(); + if(wait) + mThread.wait(); +} + +void GeneralWorkerThread::workerDestroyed() +{ + if(!--mWorkerCount) + stopThread(); +} + +//Public: +void GeneralWorkerThread::moveTo(QObject* object) +{ + if(!mWorkerCount++) + startThread(); + + object->moveToThread(&mThread); + + // Worker management + + /* Have worker killed if it still exists when thread is being shutdown; + * + * QThread docs note that deferred deletions still occur after finished is emitted, so this is possible + * (this use case is explicitly mentioned). + */ + QObject::connect(&mThread, &QThread::finished, object, &QObject::deleteLater); + QObject::connect(object, &QObject::destroyed, object, []{ GeneralWorkerThread::instance()->workerDestroyed(); }); // Notify of destruction +} + +} +/*! @endcond */ diff --git a/lib/core/src/__private/qx-generalworkerthread.h b/lib/core/src/__private/qx-generalworkerthread.h new file mode 100644 index 00000000..32559499 --- /dev/null +++ b/lib/core/src/__private/qx-generalworkerthread.h @@ -0,0 +1,53 @@ +#ifndef QX_GENERALWORKERTHREAD_H +#define QX_GENERALWORKERTHREAD_H + +// Qt Includes +#include + +// Intra-component Includes +#include "qx/core/qx-threadsafesingleton.h" + +/*! @cond */ +namespace Qx +{ + +/* A dedicated thread for Qx worker objects so that we can be sure the thread they run on is never blocked + * for long periods. Automatically starts up when objects are added, and stops when the last one is removed. + * + * Although this is called "GeneralWorkerThread", it's more so its manager. The real thread is created + * by QThread. So, this class can be called anywhere, at anytime; therefore, we make it TSS + */ +class GeneralWorkerThread : public ThreadSafeSingleton +{ + QX_THREAD_SAFE_SINGLETON(GeneralWorkerThread); +//-Instance Variables------------------------------------------------------------------------------------------------- +private: + QThread mThread; + uint mWorkerCount; + +//-Constructor------------------------------------------------------------------------------------------------- +private: + GeneralWorkerThread(); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + ~GeneralWorkerThread(); + +//-Class Functions--------------------------------------------------------------------------------------------- +public: + + +//-Instance Functions-------------------------------------------------------------------------------------------- +private: + void startThread(); + void stopThread(bool wait = false); + void workerDestroyed(); + +public: + void moveTo(QObject* object); +}; + +} +/*! @endcond */ + +#endif // QX_GENERALWORKERTHREAD_H diff --git a/lib/core/src/__private/qx-processwaiter.cpp b/lib/core/src/__private/qx-processwaiter.cpp index e35d3de2..2bb21895 100644 --- a/lib/core/src/__private/qx-processwaiter.cpp +++ b/lib/core/src/__private/qx-processwaiter.cpp @@ -50,7 +50,7 @@ void AbstractProcessWaiter::waitForDead(std::chrono::milliseconds timeout, std:: mDeadWaitTimer.stop(); postDeadWait(true); } - }, Qt::ConnectionType(Qt::DirectConnection | Qt::SingleShotConnection)); + }, Qt::ConnectionType(Qt::DirectConnection | Qt::SingleShotConnection)); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) mDeadWaitTimer.start(timeout, this); } diff --git a/lib/core/src/__private/qx-signaldaemon.h b/lib/core/src/__private/qx-signaldaemon.h new file mode 100644 index 00000000..337340ad --- /dev/null +++ b/lib/core/src/__private/qx-signaldaemon.h @@ -0,0 +1,30 @@ +#ifndef QX_SIGNALDAEMON_H +#define QX_SIGNALDAEMON_H + +// Qt Includes +#include + +// Inter-component Includes +#include "qx/core/qx-systemsignalwatcher.h" + +/*! @cond */ +namespace Qx +{ + +class AbstractSignalDaemon +{ +//-Aliases-------------------------------------------------------------------------------------------------- +protected: + using Signal = SystemSignalWatcher::Signal; + +//-Instance Functions-------------------------------------------------------------------------------------------- +public: + virtual void addSignal(Signal signal) = 0; + virtual void removeSignal(Signal signal) = 0; + virtual void callDefaultHandler(Signal signal) = 0; +}; + +} +/*! @endcond */ + +#endif // QX_SIGNALDAEMON_H diff --git a/lib/core/src/__private/qx-signaldaemon_linux.cpp b/lib/core/src/__private/qx-signaldaemon_linux.cpp new file mode 100644 index 00000000..0a930fc8 --- /dev/null +++ b/lib/core/src/__private/qx-signaldaemon_linux.cpp @@ -0,0 +1,176 @@ +// Unit Include +#include "qx-signaldaemon_linux.h" + +// Qt Includes +#include + +// Inter-component Includes +#include "qx-systemsignalwatcher_p.h" +#include "qx-generalworkerthread.h" + +// System Includes +#include +#include + +/*! @cond */ +namespace Qx +{ + +//=============================================================================================================== +// SignalDaemon +//=============================================================================================================== + +//-Destructor------------------------------------------------------------------------------------------------- +//Public: +SignalDaemon::~SignalDaemon() { if(mNotifier) shutdownNotifier(); } + +//-Class Functions---------------------------------------------------------------------------------------------- +//Private: +void SignalDaemon::handler(int signal) +{ + /* This will be called by the system in a random thread of its choice (which is bad since it could + * block an important thread). We write to the "write end" of a socket pair (a cheap operation) to wake + * our notifier in a dedicated thread (which listens to the "read end") in order to quickly escape this one. + * + * There are also severe limits to what functions you can call in POSIX signal handlers: + * https://doc.qt.io/qt-6/unix-signals.html + */ + Q_ASSERT(smHandlerFds[0] && smHandlerFds[1]); + ssize_t bytes = sizeof(signal); + ssize_t bytesW = write(smHandlerFds[0], &signal, bytes); + Q_ASSERT(bytesW == bytes); +} + +SignalDaemon* SignalDaemon::instance() { static SignalDaemon d; return &d; } + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: +void SignalDaemon::installHandler(int sig) +{ + struct sigaction sigact = {}; + sigact.sa_handler = SignalDaemon::handler; + sigemptyset(&sigact.sa_mask); // Might not be needed since struct is value-initialized + sigact.sa_flags |= SA_RESTART; + if(sigaction(sig, &sigact, NULL) != 0) + qWarning("SignalDaemon: Failed to install sigaction! System signal monitoring will not function. %s", strerror(errno)); +} + +void SignalDaemon::restoreDefaultHandler(int sig) +{ + struct sigaction sigact = {}; + sigact.sa_handler = SIG_DFL; + sigemptyset(&sigact.sa_mask); // Might not be needed since struct is value-initialized + if(sigaction(sig, &sigact, NULL) != 0) + qWarning("SignalDaemon: Failed to restore default signal handler!. %s", strerror(errno)); +} + +void SignalDaemon::startupNotifier() +{ + Q_ASSERT(!mNotifier); + + // Create local socket pair + if(socketpair(AF_UNIX, SOCK_STREAM, 0, smHandlerFds) != 0) + { + qWarning("SignalDaemon: Failed to create socket pair! System signal monitoring will not function. %s", strerror(errno)); + return; + } + + // Setup notifier to watch read end of pair + mNotifier = new QSocketNotifier(smHandlerFds[1], QSocketNotifier::Read); + QObject::connect(mNotifier, &QSocketNotifier::activated, mNotifier, [](QSocketDescriptor socket, QSocketNotifier::Type type){ + // This all occurs within a dedicated thread + Q_ASSERT(type == QSocketNotifier::Read); + + // Read signal from fd + int signal; + ssize_t bytes = sizeof(signal); + ssize_t bytesR = read(socket, &signal, sizeof(signal)); + Q_ASSERT(bytesR == bytes); + + // Trigger daemon + auto daemon = SignalDaemon::instance(); + daemon->processNativeSignal(signal); + }); + mNotifier->setEnabled(true); + + // Move notifier to dedicated thread + auto gwt = GeneralWorkerThread::instance(); + gwt->moveTo(mNotifier); +} +void SignalDaemon::shutdownNotifier() +{ + Q_ASSERT(mNotifier); + + /* Closing the "write end" of the socketpair will cause EOF to be sent to the "read end", and therefore trigger + * our socket notifier, which we don't want, so we have to disable it first. Since the notifier lives in another + * thread we can't cause the change directly and instead have to invoke the slot via an event. We don't need to + * block here for that (Qt::BlockingQueuedConnection), but just make sure that the invocation of the setEnabled() + * slot is queued up before the socket is closed (so that the EOF is ignored when that event is processed). + * + * Lambda is used because arguments with invokeMethod() weren't added until Qt 6.7. + */ + QMetaObject::invokeMethod(mNotifier, [noti = mNotifier]{ noti->setEnabled(false); }); + + // Close sockets and zero out + if(close(smHandlerFds[0]) != 0) + qWarning("SignalDaemon: Failed to close write-end of socket. %s", strerror(errno)); + if(close(smHandlerFds[1]) != 0) + qWarning("SignalDaemon: Failed to close read-end of socket. %s", strerror(errno)); + smHandlerFds[0] = 0; + smHandlerFds[1] = 0; + + // Kill notifier + mNotifier->deleteLater(); + mNotifier = nullptr; +} + +//Public: +void SignalDaemon::addSignal(Signal signal) +{ + int nativeSignal = SIGNAL_MAP.from(signal); + Q_ASSERT(!mActiveSigs.contains(nativeSignal)); + mActiveSigs.insert(nativeSignal); + if(mActiveSigs.size() == 1) + startupNotifier(); + + installHandler(nativeSignal); +} + +void SignalDaemon::removeSignal(Signal signal) +{ + int nativeSignal = SIGNAL_MAP.from(signal); + Q_ASSERT(mActiveSigs.contains(nativeSignal)); + restoreDefaultHandler(nativeSignal); + mActiveSigs.remove(nativeSignal); + if(mActiveSigs.isEmpty()) + shutdownNotifier(); +} + +void SignalDaemon::callDefaultHandler(Signal signal) +{ + int nativeSig = SIGNAL_MAP.from(signal); + bool active = mActiveSigs.contains(nativeSig); + if(active) + restoreDefaultHandler(nativeSig); + if(raise(nativeSig) != 0) // Triggers default action, doesn't return until it's finished + qWarning("SignalDaemon: Failed to raise signal for default signal handler!"); + if(active) + installHandler(nativeSig); // If for some reason we're still alive, put back the custom handler +} + +void SignalDaemon::processNativeSignal(int sig) const +{ + /* Acquiring a lock on manager also acts like a mutex for this class as the dedicated notifier thread + * will block when it hits this if the manager is modifying this singleton + */ + auto manager = SswManager::instance(); + + /* Always forward, we should only ever get signals we're watching. Technically, one could have been + * removed at the last minute while trying to get the above lock, but we send the signal forward anyway + * as otherwise the signal would go entirely unhandled + */ + manager->processSignal(SIGNAL_MAP.from(sig)); +} + +} +/*! @endcond */ diff --git a/lib/core/src/__private/qx-signaldaemon_linux.h b/lib/core/src/__private/qx-signaldaemon_linux.h new file mode 100644 index 00000000..a144d3c4 --- /dev/null +++ b/lib/core/src/__private/qx-signaldaemon_linux.h @@ -0,0 +1,73 @@ +#ifndef QX_SIGNALDAEMON_LINUX_H +#define QX_SIGNALDAEMON_LINUX_H + +// Qt Includes +#include + +// Inter-component Includes +#include "qx-signaldaemon.h" +#include "qx/core/qx-bimap.h" + +// System Includes +#include + +/*! @cond */ +class QSocketNotifier; + +namespace Qx +{ + +/* Can't use thread-safe singleton with this without resorting to the somewhat costly QRecursiveMutex as + * the class gets reentered by the same thread while locked; however we get around that and the need for + * manual mutex since with a trick in processNativeSignal. + */ +class SignalDaemon : public AbstractSignalDaemon +{ +//-Class Variables------------------------------------------------------------------------------------------------- +private: + static inline const Bimap SIGNAL_MAP{ + {Signal::HangUp, SIGHUP}, + {Signal::Interrupt, SIGINT}, + {Signal::Terminate, SIGTERM}, + {Signal::Quit, SIGQUIT}, + {Signal::Abort, SIGABRT}, + }; + + // Process local sockets for escaping the signal handler + static inline int smHandlerFds[2] = {0, 0}; + +//-Instance Variables------------------------------------------------------------------------------------------------- +private: + QSocketNotifier* mNotifier; + QSet mActiveSigs; + +//-Destructor------------------------------------------------------------------------------------------------- +public: + ~SignalDaemon(); + +//-Class Functions---------------------------------------------------------------------------------------------- +private: + static void handler(int signal); + +public: + static SignalDaemon* instance(); + +//-Instance Functions-------------------------------------------------------------------------------------------- +private: + void installHandler(int sig); + void restoreDefaultHandler(int sig); + void startupNotifier(); + void shutdownNotifier(); + +public: + void addSignal(Signal signal) override; + void removeSignal(Signal signal) override; + void callDefaultHandler(Signal signal) override; + + void processNativeSignal(int sig) const; +}; + +} +/*! @endcond */ + +#endif // QX_SIGNALDAEMON_LINUX_H diff --git a/lib/core/src/__private/qx-signaldaemon_win.cpp b/lib/core/src/__private/qx-signaldaemon_win.cpp new file mode 100644 index 00000000..1d134327 --- /dev/null +++ b/lib/core/src/__private/qx-signaldaemon_win.cpp @@ -0,0 +1,103 @@ +// Unit Include +#include "qx-signaldaemon_win.h" + +// Inter-component Includes +#include "qx-systemsignalwatcher_p.h" + +/*! @cond */ +namespace Qx +{ + +//=============================================================================================================== +// SignalDaemon +//=============================================================================================================== + +//-Destructor------------------------------------------------------------------------------------------------- +//Public: +SignalDaemon::~SignalDaemon() { if(!mActiveCtrlTypes.isEmpty()) removeHandler(); } + +//-Class Functions---------------------------------------------------------------------------------------------- +//Private: +BOOL SignalDaemon::handler(DWORD dwCtrlType) +{ + // Everything within this function (and what it calls) occurs in a separate system thread + auto daemon = SignalDaemon::instance(); + return daemon->processNativeSignal(dwCtrlType); +} + +SignalDaemon* SignalDaemon::instance() { static SignalDaemon d; return &d; } + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: +void SignalDaemon::installHandler() +{ + if(!SetConsoleCtrlHandler(&SignalDaemon::handler, TRUE)) + { + DWORD err = GetLastError(); + qWarning("Failed to install SignalWatcher native handler! 0x%X", err); + } + +} +void SignalDaemon::removeHandler() +{ + if(!SetConsoleCtrlHandler(&SignalDaemon::handler, FALSE)) + { + DWORD err = GetLastError(); + qWarning("Failed to uninstall SignalWatcher native handler! 0x%X", err); + } +} + +//Public: +void SignalDaemon::addSignal(Signal signal) +{ + DWORD ctrlType = SIGNAL_MAP.from(signal); + Q_ASSERT(!mActiveCtrlTypes.contains(ctrlType)); + mActiveCtrlTypes.insert(ctrlType); + if(mActiveCtrlTypes.size() == 1) + installHandler(); +} + +void SignalDaemon::removeSignal(Signal signal) +{ + DWORD ctrlType = SIGNAL_MAP.from(signal); + Q_ASSERT(mActiveCtrlTypes.contains(ctrlType)); + mActiveCtrlTypes.remove(ctrlType); + if(mActiveCtrlTypes.isEmpty()) + removeHandler(); +} + +void SignalDaemon::callDefaultHandler(Signal signal) +{ + Q_UNUSED(signal); + /* Unfortunately there is no way to get the address of the default handler, nor a way + * to proc it for any signal, so we have to repeat the behavior here. + * + * This is what it seems to do for every signal. + */ + ExitProcess(STATUS_CONTROL_C_EXIT); +} + +bool SignalDaemon::processNativeSignal(DWORD dwCtrlType) const +{ + /* We won't always use this (see next check), but this indirectly acts like a mutex for this class. + * SswManager is the only class that accesses this one other than its own handler. So, if we make sure + * to get a lock here immediately, we guarantee that this classes data members are in a valid state + * for the handler thread to read. It technically causes a touch of slow down in the case where the + * false branch of the below 'if' is taken but that likely means the program is exiting anyway so + * it's really no problem. + */ + auto manager = SswManager::instance(); + + if(mActiveCtrlTypes.contains(dwCtrlType)) + { + manager->processSignal(SIGNAL_MAP.from(dwCtrlType)); + return true; + } + + // Ctrl type we aren't handling + return false; +} + + +} +/*! @endcond */ diff --git a/lib/core/src/__private/qx-signaldaemon_win.h b/lib/core/src/__private/qx-signaldaemon_win.h new file mode 100644 index 00000000..8a60674f --- /dev/null +++ b/lib/core/src/__private/qx-signaldaemon_win.h @@ -0,0 +1,66 @@ +#ifndef QX_SIGNALDAEMON_WIN_H +#define QX_SIGNALDAEMON_WIN_H + +// Qt Includes +#include + +// Inter-component Includes +#include "__private/qx-signaldaemon.h" +#include "qx/core/qx-bimap.h" + +// Windows Includes +#define WIN32_LEAN_AND_MEAN +#include "windows.h" + +/*! @cond */ +namespace Qx +{ + +/* Can't use thread-safe singleton with this without resorting to the somewhat costly QRecursiveMutex as + * the class gets reentered by the same thread while locked; however we get around that and the need for + * manual mutex since with a trick in processNativeSignal. + */ +class SignalDaemon : public AbstractSignalDaemon +{ +//-Class Variables------------------------------------------------------------------------------------------------- +private: + static inline const Bimap SIGNAL_MAP{ + {Signal::HangUp, CTRL_CLOSE_EVENT}, + {Signal::Interrupt, CTRL_C_EVENT}, + {Signal::Terminate, CTRL_SHUTDOWN_EVENT}, + {Signal::Quit, CTRL_BREAK_EVENT}, + {Signal::Abort, CTRL_LOGOFF_EVENT}, + }; + +//-Instance Variables------------------------------------------------------------------------------------------------- +private: + QSet mActiveCtrlTypes; + +//-Destructor------------------------------------------------------------------------------------------------- +public: + ~SignalDaemon(); + +//-Class Functions---------------------------------------------------------------------------------------------- +private: + static BOOL handler(DWORD dwCtrlType); + +public: + static SignalDaemon* instance(); + +//-Instance Functions-------------------------------------------------------------------------------------------- +private: + void installHandler(); + void removeHandler(); + +public: + void addSignal(Signal signal) override; + void removeSignal(Signal signal) override; + void callDefaultHandler(Signal signal) override; + + bool processNativeSignal(DWORD dwCtrlType) const; +}; + +} +/*! @endcond */ + +#endif // QX_SIGNALDAEMON_WIN_H diff --git a/lib/core/src/qx-bimap.dox b/lib/core/src/qx-bimap.dox new file mode 100644 index 00000000..ab35122a --- /dev/null +++ b/lib/core/src/qx-bimap.dox @@ -0,0 +1,732 @@ +namespace Qx +{ + +/*! + * @concept asymmetric_bimap + * @brief Specifies that a bimap has different Left and Right types. + * + * Satisfied if @c Left is not the same as @c Right. + */ + +/*! + * @concept bimap_iterator_predicate + * @brief Specifies that a predicate is a valid, iterator based predicate for a bimap. + * + * Satisfied if the predicate takes a Qx::Bimap::const_iterator and returns @c bool. + */ + +/*! + * @concept bimap_pair_predicate + * @brief Specifies that a predicate is a valid, pair based predicate for a bimap. + * + * Satisfied if the predicate takes a std::pair and returns @c bool. + */ + +/*! + * @concept bimap_predicate + * @brief Specifies that a predicate is a valid predicate for a bimap. + * + * Satisfied if the predicate satisfies bimap_iterator_predicate or bimap_pair_predicate. + */ + +//=============================================================================================================== +// Bimap +//=============================================================================================================== + +/*! + * @class Bimap qx/core/qx-bimap.h + * @ingroup qx-core + * + * @brief The Bimap template class offers a rudimentary bi-directional associative map. + * + * Qx::Bimap is like QHash, except that instead of Key and Value there is + * Left and Right, meaning that no neither type in any specialization of the container is more + * significant than the other. Lookup of one of the "side's" values using the other is possible + * via fromLeft(), fromRight(), and other similarly named functions. + * + * iterator is simply an alias for const_iterator as values cannot be modified through + * bimap iterators due to technical limitations. + * + * Both the Left and Right types must provide operator==() and a global qHash() overload. + */ + +//-Aliases-------------------------------------------------------------------------------------------------- +//Public: +/*! + * @typedef Bimap::iterator + * + * Typedef for const_iterator. + */ + +/*! + * @typedef Bimap::left_type + * + * Typedef for Left. + */ + +/*! + * @typedef Bimap::right_type + * + * Typedef for Right. + */ + +/*! + * @typedef Bimap::ConstIterator + * + * Qt-style synonym for Bimap::const_iterator. + */ + +/*! + * @typedef Bimap::difference_type + * + * Typedef for ptrdiff_t. Provided for STL compatibility. + */ + +/*! + * @typedef Bimap::size_type + * + * Typedef for int. Provided for STL compatibility. + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn Bimap::Bimap() + * + * Creates an empty bimap. + * + * @sa clear(). + */ + +/*! + * @fn Bimap::Bimap(std::initializer_list> list) + * + * Creates a bimap with a copy of each of the elements in the initializer list @a list. + */ + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn iterator Bimap::begin() + * + * Same as constBegin(). + */ + +/*! + * @fn const_iterator Bimap::begin() const + * + * @overload + */ + +/*! + * @fn const_iterator Bimap::cbegin() const + * + * Same as constBegin(). + */ + +/*! + * @fn const_iterator Bimap::constBegin() const + * + * Returns an STL-style iterator pointing to the first relationship in the bimap. + * + * @warning Returned iterators/references should be considered invalidated the next time you call + * a non-const function on the bimap, or when the bimap is destroyed. + * + * @sa constEnd(). + */ + +/*! + * @fn iterator Bimap::end() + * + * Same as constEnd(). + */ + +/*! + * @fn const_iterator Bimap::end() const + * + * @overload + */ + +/*! + * @fn const_iterator Bimap::cend() const + * + * Same as constEnd(). + */ + +/*! + * @fn const_iterator Bimap::constEnd() const + * + * Returns an STL-style iterator pointing to the last relationship in the bimap. + * + * @warning Returned iterators/references should be considered invalidated the next time you call + * a non-const function on the bimap, or when the bimap is destroyed. + * + * @sa constBegin(). + */ + +/*! + * @fn const_iterator Bimap::constFind(const Left& l) const + * + * Same as constFindLeft(). + */ + +/*! + * @fn const_iterator Bimap::constFind(const Right& l) const + * + * Same as constFindRight(). + */ + +/*! + * @fn const_iterator Bimap::constFindLeft(const Left& l) const + * + * Returns an iterator pointing to the relationship with the Left value @a l in the bimap. + * + * If the bimaps contains no relationship with the Left value, the function returns constEnd(). + * + * @warning Returned iterators/references should be considered invalidated the next time you call + * a non-const function on the bimap, or when the bimap is destroyed. + */ + +/*! + * @fn const_iterator Bimap::constFindRight(const Left& l) const + * + * Returns an iterator pointing to the relationship with the Right value @a r in the bimap. + * + * If the bimaps contains no relationship with the Right value, the function returns constEnd(). + * + * @warning Returned iterators/references should be considered invalidated the next time you call + * a non-const function on the bimap, or when the bimap is destroyed. + */ + +/*! + * @fn iterator Bimap::find(const Left& l) + * + * Same as constFindLeft(). + */ + +/*! + * @fn const_iterator Bimap::find(const Left& l) const + * + * @overload + */ + +/*! + * @fn iterator Bimap::find(const Right& r) + * + * Same as constFindRight(). + */ + +/*! + * @fn const_iterator Bimap::find(const Right& r) const + * + * @overload + */ + +/*! + * @fn iterator Bimap::findLeft(const Left& r) + * + * Same as constFindLeft(). + */ + +/*! + * @fn const_iterator Bimap::findLeft(const Left& r) const + * + * @overload + */ + +/*! + * @fn iterator Bimap::findRight(const Right& r) + * + * Same as constFindRight(). + */ + +/*! + * @fn const_iterator Bimap::findRight(const Right& r) const + * + * @overload + */ + +/*! + * @fn const_iterator Bimap::erase(const_iterator pos) + * + * Removes the (Left, Right) pair associated with the iterator @a pos from the bimap, + * and returns an iterator to the next relationship in the bimap. + * + * @warning Returned iterators/references should be considered invalidated the next time you call + * a non-const function on the bimap, or when the bimap is destroyed. + * + * @sa remove(), take() and find(). + */ + +/*! + * @fn void Bimap::insert(const Bimap& other) + * + * Inserts all the relationships in the other bimap into this bimap. + * + * If a Left or Right value from any relationship is common to both bimaps, the relationship containing + * then will be replaced with the relation that contains said value(s) stored in other. + */ + +/*! + * @fn const_iterator Bimap::insert(const Left& l, const Right& r) + * + * Inserts a new relationship between the Left value @a l and Right value @a r. + * + * If there is already a relationship for either value, that relationship is + * removed, effectively replacing it with the new relationship. + * + * Returns an iterator pointing to the new relationship. + * + * @warning Returned iterators/references should be considered invalidated the next time you call + * a non-const function on the bimap, or when the bimap is destroyed. + */ + +/*! + * @fn bool Bimap::containsLeft(const Left& l) const + * + * Returns @c true if the bimap contains a relationship with the Left value @a l; otherwise, + * returns @c false. + * + * @sa containsRight() and count(). + */ + +/*! + * @fn bool Bimap::containsRight(const Right& r) const + * + * Returns @c true if the bimap contains a relationship with the Right value @a r; otherwise, + * returns @c false. + * + * @sa containsLeft() and count(). + */ + +/*! + * @fn Right Bimap::fromLeft(const Left& l) const + * + * Returns the Right value associated with Left value @a l. + * + * If the bimap does not contain a relationship with @a l, a default constructed Right + * value is returned. + * + * @sa fromRight(). + */ + +/*! + * @fn Right Bimap::fromLeft(const Left& l, const Right& defaultValue) const + * + * @overload + * + * Returns the Right value associated with Left value @a l. + * + * If the bimap does not contain a relationship with @a l, @a defaultValue is returned. + */ + +/*! + * @fn Left Bimap::fromRight(const Right& r) const + * + * Returns the Left value associated with Right value @a r. + * + * If the bimap does not contain a relationship with @a r, a default constructed Left + * value is returned. + * + * @sa fromLeft(). + */ + +/*! + * @fn Left Bimap::fromRight(const Right& r, const Left& defaultValue) const + * + * @overload + * + * Returns the Left value associated with Right value @a r. + * + * If the bimap does not contain a relationship with @a r, @a defaultValue is returned. + */ + +/*! + * @fn Right Bimap::from(const Left& l) const + * + * Same as fromLeft(). + */ + +/*! + * @fn Right Bimap::from(const Left& l, const Right& defaultValue) const + * + * Same as fromLeft(const Left&, const Right&). + */ + +/*! + * @fn Left Bimap::from(const Right& r) const + * + * Same as fromRight(). + */ + +/*! + * @fn Left Bimap::from(const Right& r, const Left& defaultValue) const + * + * Same as fromLeft(const Right&, const Left&). + */ + +/*! + * @fn Left Bimap::toLeft(const Right& r) const + * + * Same as fromRight(). + * + * @sa toRight(). + */ + +/*! + * @fn Left Bimap::toLeft(const Right& r, const Left& defaultValue) const + * + * @overload + */ + +/*! + * @fn Right Bimap::toRight(const Left& l) const + * + * Same as fromLeft(). + * + * @sa toLeft(). + */ + +/*! + * @fn Right Bimap::toRight(const Left& l, const Right& defaultValue) const + * + * @overload + */ + +/*! + * @fn bool Bimap::remove(const Left& l) + * + * Same as removeLeft(). + */ + +/*! + * @fn bool Bimap::remove(const Right& r) + * + * Same as removeRight(). + */ + +/*! + * @fn bool Bimap::removeLeft(const Left& l) + * + * Removes the relationship containing the Left value @a l from the bimap if present and + * returns @c true; otherwise, returns @c false. + * + * @sa removeRight() and clear(). + */ + +/*! + * @fn bool Bimap::removeRight(const Right& r) + * + * Removes the relationship containing the Right value @a r from the bimap if present and + * returns @c true; otherwise, returns @c false. + * + * @sa removeLeft() and clear(). + */ + +/*! + * @fn qsizetype Bimap::removeIf(Predicate pred) + * + * Removes all elements for which the predicate pred returns true from the bimap. + * + * The function supports predicates which take either an argument of type Bimap::const_iterator, + * or an argument of type std::pair. + * + * Returns the number of elements removed, if any. + * + * @sa clear() and take(). + */ + +/*! + * @fn Right Bimap::takeRight(const Left& l) + * + * Removes the relationship with the Left value @a l from the bimap and returns the Right value + * associated with it. + * + * If such a relationship does not exist in the bimap, the function simply returns a default-constructed value + * + * If you don't use the return value, remove() is more efficient. + * + * @sa remove(). + */ + +/*! + * @fn Left Bimap::takeLeft(const Right& r) + * + * Removes the relationship with the Right value @a r from the bimap and returns the Left value + * associated with it. + * + * If such a relationship does not exist in the bimap, the function simply returns a default-constructed value + * + * If you don't use the return value, remove() is more efficient. + * + * @sa remove(). + */ + +/*! + * @fn Right Bimap::take(const Left& l) + * + * Same as takeRight(). + */ + +/*! + * @fn Left Bimap::take(const Right& r) + * + * Same as takeLeft(). + */ + + +/*! + * @fn void Bimap::swap(Bimap& other) + * + * Swaps bimap @a other with this bimap. This operation is very fast and never fails. + */ + +/*! + * @fn qsizetype Bimap::size() const + * + * Returns the number of relations in the bimap. + * + * @sa isEmpty() and count(). + */ + +/*! + * @fn qsizetype Bimap::count() const + * + * Same as size(). + */ + +/*! + * @fn bool Bimap::isEmpty() const + * + * Returns @c true if the bimap contains no relations; otherwise, returns @c false. + * + * @sa size(). + */ + +/*! + * @fn bool Bimap::empty() const + * + * Same as isEmpty(). + */ + +/*! + * @fn float Bimap::load_factor() const + * + * Returns the current load factor of the Bimap's internal table. This is the same as + * capacity()/size(). The implementation used will aim to keep the load factor + * between 0.25 and 0.5. This avoids having too many table collisions that would + * degrade performance. + * + * Even with a low load factor, the implementation of the bimap table has a very low memory overhead. + * + * This method purely exists for diagnostic purposes and you should rarely need to call it yourself. + * + * @sa reserve() and squeeze(). + */ + +/*! + * @fn qsizetype Bimap::capacity() const + * + * Returns the number of buckets in the bimap's internal table. + * + * The sole purpose of this function is to provide a means of fine tuning Bimap's memory + * usage. In general, you will rarely ever need to call this function. If you want to know + * how many items are in the bimap, call size(). + * + * @sa reserve() and squeeze(). + */ + +/*! + * @fn void Bimap::clear() + * + * Removes all relations from the bimap and frees up all memory used by it. + * + * @sa remove(). + */ + +/*! + * @fn void Bimap::reserve() + * + * Ensures that the bimap's internal table has space to store at least @a size items without + * having to grow the table. + * + * This function is useful for code that needs to build a huge bimap and wants to avoid repeated + * reallocation. + * + * In general, you will rarely ever need to call this function. Bimap's internal table + * automatically grows to provide good performance without wasting too much memory. + * + * @sa squeeze() and capacity(). + */ + +/*! + * @fn void Bimap::squeeze() + * + * Reduces the size of the Bimap's internal table to save memory. + * + * The sole purpose of this function is to provide a means of fine tuning Bimap's memory usage. + * In general, you will rarely ever need to call this function. + * + * @sa reserve() and capacity(). + */ + +/*! + * @fn QList Bimap::lefts() const + * + * Returns a list containing all of the Left values in the bimap, in an arbitrary order. + * + * This function creates a new list, in linear time. The time and memory use that entails can be avoided + * by iterating from begin() to end(). + * + * @sa rights() and fromRight(). + */ + +/*! + * @fn QList Bimap::rights() const + * + * Returns a list containing all of the Right values in the bimap, in an arbitrary order. + * + * This function creates a new list, in linear time. The time and memory use that entails can be avoided + * by iterating from begin() to end(). + * + * @sa lefts() and fromLeft(). + */ + +//-Operators--------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn Right Bimap::operator[](const Left& l) const + * + * Returns the Right value associated with the Left value @a l. + * + * Throws `std::invalid_argument()` if the bimap does not contain a relationship with the Left value @a l. + * + * @sa fromLeft() and fromRight(). + */ + +/*! + * @fn Left Bimap::operator[](const Right& r) const + * + * Returns the Left value associated with the Right value @a r. + * + * Throws `std::invalid_argument()` if the bimap does not contain a relationship with the Right value @a r. + * + * @sa fromLeft() and fromRight(). + */ + +/*! + * @fn bool Bimap::operator==(const Bimap& other) const + * + * Returns @c true if @a other is equal to this bimap; otherwise, returns @c false. + * + * Two bimap's are considered equal if they contain the same (right, left) relationships. + * + * This function requires the Right and Left types to implement operator==(). + * + * @sa operator!=(). + */ + +/*! + * @fn bool Bimap::operator!=(const Bimap& other) const + * + * Returns @c true if @a other is not equal to this bimap; otherwise, returns @c false. + * + * Two bimap's are considered equal if they contain the same (right, left) relationships. + * + * This function requires the Right and Left types to implement operator==(). + * + * @sa operator==(). + */ + +//=============================================================================================================== +// Bimap::const_iterator +//=============================================================================================================== + +/*! + * @class Bimap::const_iterator qx/core/qx-bimap.h + * @ingroup qx-core + * + * @brief The Bimap::const_iterator class provides an STL-style const iterator for Bimap. + * + * Bimap::const_iterator allows you to iterate over a Bimap. + * + * The default Bimap::const_iterator constructor creates an uninitialized iterator. You must initialize it using + * a Bimap function like Bimap::cbegin(), Bimap::cend(), or Bimap::constFind() before you can start iterating. + * + * Bimap stores its relationships in an arbitrary order. + * + * Multiple iterators can be used on the same bimap. However, be aware that any modification performed directly on + * the Bimap (inserting and removing items) can cause the iterators to become invalid. + * + * Inserting relationships into the bimap or calling methods such as Bimap::reserve() or Bimap::squeeze() can + * invalidate all iterators pointing into the bimap. Iterators are guaranteed to stay valid only as long as the + * Bimap doesn't have to grow/shrink its internal table. Using any iterator after a rehashing operation has + * occurred will lead to undefined behavior. + * + * You can however safely use iterators to remove entries from the bimap using the Bimap::erase() method. + * This function can safely be called while iterating, and won't affect the order of items in the bimap. + * + * @warning Iterators on this container do not work exactly like STL-iterators. You should avoid copying a bimap + * while iterators are active on it. For more information, read Qt's "Implicit sharing iterator problem". + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn Bimap::const_iterator::const_iterator() + * + * Constructs an uninitialized iterator. + * + * Functions like left(), right(), and operator++() must not be called on an uninitialized iterator. + * Use operator=() to assign a value to it before using it. + * + * @sa Bimap::constBegin() and Bimap::constEnd(). + */ + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn const Left& Bimap::const_iterator::left() const + * + * Returns the current relationship's Left value. + */ + +/*! + * @fn const Right& Bimap::const_iterator::right() const + * + * Returns the current relationship's Right value. + */ + +//-Operators--------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn bool Right& Bimap::const_iterator::operator==(const const_iterator& other) const + * + * Returns @c true if @a other points to the same relationship as this iterator; otherwise, returns @c false. + */ + +/*! + * @fn std::pair Bimap::const_iterator::operator*() const + * + * Returns the current relationship. + */ + +/*! + * @fn const_iterator Bimap::const_iterator::operator++() + * + * The prefix @c ++ operator @c (++i) advances the iterator to the next relationship in the bimap and + * returns an iterator to the new current relationship. + * + * Calling this function on Bimap::constEnd() leads to undefined results. + */ + +/*! + * @fn const_iterator Bimap::const_iterator::operator++(int) + * + * The postfix @c ++ operator @c (i++) advances the iterator to the next relationship in the bimap and + * returns an iterator to the previously current relationship. + * + * Calling this function on Bimap::constEnd() leads to undefined results. + */ + +} diff --git a/lib/core/src/qx-char.cpp b/lib/core/src/qx-char.cpp index e7a5cfe4..8fedf768 100644 --- a/lib/core/src/qx-char.cpp +++ b/lib/core/src/qx-char.cpp @@ -72,16 +72,11 @@ int Char::compare(QChar cOne, QChar cTwo, Qt::CaseSensitivity cs) // Equalize case if case-insensitive if(cs == Qt::CaseInsensitive) { - cOne = cOne.toLower(); - cTwo = cTwo.toLower(); + cOne = cOne.toCaseFolded(); + cTwo = cTwo.toCaseFolded(); } - if(cOne < cTwo) - return -1; - else if(cOne > cTwo) - return 1; - else - return 0; + return cOne.unicode() - cTwo.unicode(); } } diff --git a/lib/core/src/qx-exclusiveaccess.dox b/lib/core/src/qx-exclusiveaccess.dox index be1533df..c55d71a0 100644 --- a/lib/core/src/qx-exclusiveaccess.dox +++ b/lib/core/src/qx-exclusiveaccess.dox @@ -23,7 +23,7 @@ namespace Qx //-Constructor---------------------------------------------------------------------------------------------- //Public: /*! - * @fn ExclusiveAccess::ExclusiveAccess(AccessType* data, Mutex* mutex) + * @fn ExclusiveAccess::ExclusiveAccess(AccessType* data, Mutex* mutex) * * Constructs an ExclusiveAccess and locks @a mutex. The mutex will be unlocked when the ExclusiveAccess is * destroyed. If @a mutex is @c nullptr, ExclusiveAccess only provides access to @a data. @@ -32,7 +32,7 @@ namespace Qx */ /*! - * @fn ExclusiveAccess::ExclusiveAccess(ExclusiveAccess&& other) + * @fn ExclusiveAccess::ExclusiveAccess(ExclusiveAccess&& other) * * Move-constructs and ExclusiveAccess from @a other. The mutex, data pointer, and state of @a other is * transferred to the newly constructed instance. After the move, @a other will no longer manage the mutex, @@ -44,28 +44,28 @@ namespace Qx //-Destructor------------------------------------------------------------------------------------------------ //Public: /*! - * @fn ExclusiveAccess::~ExclusiveAccess() + * @fn ExclusiveAccess::~ExclusiveAccess() * * Destroys the ExclusiveAccess and unlocks the mutex provided by the constructor if it's still locked. */ -//-Class Functions---------------------------------------------------------------------------------------------- +//-Instance Functions---------------------------------------------------------------------------------------------- //Public: /*! - * @fn void ExclusiveAccess::isLocked() const + * @fn void ExclusiveAccess::isLocked() const * * Returns @c true if this ExclusiveAccess is currently locking its associated mutex; otherwise, returns * @c false. */ /*! - * @fn void ExclusiveAccess::mutex() const + * @fn void ExclusiveAccess::mutex() const * * Returns the mutex on which the ExclusiveAccess is operating. */ /*! - * @fn void ExclusiveAccess::relock() + * @fn void ExclusiveAccess::relock() * * Relocks an unlocked ExclusiveAccess. * @@ -73,14 +73,14 @@ namespace Qx */ /*! - * @fn void ExclusiveAccess::swap(ExclusiveAccess& other) + * @fn void ExclusiveAccess::swap(ExclusiveAccess& other) * * Swaps the mutex, data pointer, and state of this ExclusiveAccess with @a other. This operation * is very fast and never fails. */ /*! -* @fn void ExclusiveAccess::unlock() +* @fn void ExclusiveAccess::unlock() * * Unlocks this ExclusiveAccess. You can use relock() to lock it again. It does not need to be * locked when destroyed. @@ -89,43 +89,43 @@ namespace Qx */ /*! -* @fn AccessType* ExclusiveAccess::access() +* @fn AccessType* ExclusiveAccess::access() * * Returns a pointer to the data the ExclusiveAccess is providing access to. */ /*! -* @fn const AccessType* ExclusiveAccess::access() const +* @fn const AccessType* ExclusiveAccess::access() const * * @overload */ /*! -* @fn AccessType& ExclusiveAccess::operator*() +* @fn AccessType& ExclusiveAccess::operator*() * * Returns a reference to the data the ExclusiveAccess is providing access to. */ /*! -* @fn const AccessType& ExclusiveAccess::operator*() const +* @fn const AccessType& ExclusiveAccess::operator*() const * * @overload */ /*! -* @fn AccessType* ExclusiveAccess::operator->() +* @fn AccessType* ExclusiveAccess::operator->() * * Provides convenient access to the members of @a DataType for the accessible data. */ /*! -* @fn const AccessType* ExclusiveAccess::operator->() const +* @fn const AccessType* ExclusiveAccess::operator->() const * * @overload */ /*! -* @fn void ExclusiveAccess::operator=(ExclusiveAccess&& other) +* @fn void ExclusiveAccess::operator=(ExclusiveAccess&& other) * * Move-assigns @a other onto this ExclusiveAccess. If this ExclusiveAccess was holding onto a * locked mutex before the assignment, the mutex will be unlocked. The mutex, data pointer, and diff --git a/lib/core/src/qx-flatmultiset.dox b/lib/core/src/qx-flatmultiset.dox new file mode 100644 index 00000000..5e65a32b --- /dev/null +++ b/lib/core/src/qx-flatmultiset.dox @@ -0,0 +1,452 @@ +namespace Qx +{ + +/*! + * @concept flatmultiset_predicate + * @brief Specifies that a predicate is a valid predicate for a FlatMultiSet. + * + * Satisfied if the predicate takes const T& and returns @c bool. + */ + +//=============================================================================================================== +// FlatMultiSet +//=============================================================================================================== + +/*! + * @class FlatMultiSet qx/core/qx-flatmultiset.h + * @ingroup qx-core + * + * @brief The FlatMultiSet class is a container whose elements are always sorted. + * + * Essentially a more Qt aligned version of C++23's std::flat_multiset, backed by a QList. + * + * iterator is an alias for const_iterator since modifying an element in-place could affect ordering. + * + * Iterator invalidation rules are the same as for QList. + */ + +//-Aliases-------------------------------------------------------------------------------------------------- +//Public: +/*! + * @typedef FlatMultiSet::const_iterator + * The container's regular iterator type, an alias for QList::const_iterator. + * + * @typedef FlatMultiSet::iterator + * Typedef for const_iterator. + * + * @typedef FlatMultiSet::ConstIterator + * Qt-style synonym for FlatMultiSet::const_iterator. + * + * @typedef FlatMultiSet::Iterator + * Qt-style synonym for FlatMultiSet::iterator. + * + * @typedef FlatMultiSet::const_pointer + * Provided for STL compatibility. + * + * @typedef FlatMultiSet::const_reference + * Provided for STL compatibility. + * + * @typedef FlatMultiSet::const_reverse_iterator + * The container's reverse iterator type, an alias for QList::const_reverse_iterator. + * + * @typedef FlatMultiSet::ConstReverseIterator + * Qt-style synonym for const_reverse_iterator. + * + * @typedef FlatMultiSet::difference_type + * Provided for STL compatibility. + * + * @typedef FlatMultiSet::pointer + * Provided for STL compatibility. + * + * @typedef FlatMultiSet::reference + * Provided for STL compatibility. + * + * @typedef FlatMultiSet::reverse_iterator + * Typedef for const_reverse_iterator. + * + * @typedef FlatMultiSet::ReverseIterator + * Qt-style synonym for reverse_iterator. + * + * @typedef FlatMultiSet::key_type + * Typedef for T. + * + * @typedef FlatMultiSet::size_type + * Typedef for the container's size_type, usually std::size_t. + * + * @typedef FlatMultiSet::value_type + * Typedef for T. + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn FlatMultiSet::FlatMultiSet() + * + * Creates an empty flat-multiset. + * + * @sa clear(). + */ + +/*! + * @fn FlatMultiSet::FlatMultiSet(std::initializer_list list) + * + * Creates a FlatMultiSet with a copy of each of the elements in the initializer list @a list. + */ + +/*! + * @fn FlatMultiSet::FlatMultiSet(InputIterator first, InputIterator last) + * + * Creates a FlatMultiSet with a copy of each of the elements between [first, last). + */ + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn bool FlatMultiSet::contains(const FlatMultiSet& other) const + * + * Returns @c true if the FlatMultiSet contains all of the items in @a other; otherwise, + * returns @c false. + * + * @sa count(). + */ + +/*! + * @fn bool FlatMultiSet::contains(const T& value) const + * + * Returns @c true if the FlatMultiSet contains @a value; otherwise, + * returns @c false. + * + * @sa count(). + */ + +/*! + * @fn qsizetype FlatMultiSet::count() const + * + * Same as size(). + */ + +/*! + * @fn qsizetype FlatMultiSet::size() const + * + * Returns the number of items in the FlatMultiSet. + * + * @sa isEmpty() and count(). + */ + +/*! + * @fn bool FlatMultiSet::empty() const + * + * Same as isEmpty(). + */ + +/*! + * @fn bool FlatMultiSet::isEmpty() const + * + * Returns @c true if the FlatMultiSet contains no items; otherwise, returns @c false. + * + * @sa size(). + */ + +/*! + * @fn const T& FlatMultiSet::first() const + * + * Same as constFirst(). + */ + +/*! + * @fn const T& FlatMultiSet::constFirst() const + * + * Returns a reference to the first item in the set. This function assumes that the set isn't empty. + * + * @sa last() and isEmpty(). + */ + +/*! + * @fn const T& FlatMultiSet::last() const + * + * Same as constLast(). + */ + +/*! + * @fn const T& FlatMultiSet::constLast() const + * + * Returns a reference to the last item in the set. This function assumes that the set isn't empty. + * + * @sa first() and isEmpty(). + */ + +/*! + * @fn qsizetype FlatMultiSet::capacity() const + * + * Returns the maximum number of items that can be stored in the flat-multiset without forcing a reallocation. + * + * The sole purpose of this function is to provide a means of fine tuning FlatMultiSet's memory usage. In general, + * you will rarely ever need to call this function. If you want to know how many items are in the flat-multiset, + * call size(). + * + * @note a statically allocated flat-multiset will report a capacity of 0, even if it's not empty. + * + * @sa reserve() and squeeze(). + */ + +/*! + * @fn void FlatMultiSet::reserve() + * + * Attempts to allocate memory for at least size elements. + * + * If you know in advance how large the flat-multiset will be, you should call this function to prevent reallocations + * and memory fragmentation. If you resize the flat-multiset often, you are also likely to get better performance. + * + * If in doubt about how much space shall be needed, it is usually better to use an upper bound as size, or a + * high estimate of the most likely size, if a strict upper bound would be much bigger than this. If size is an + * underestimate, the flat-multiset will grow as needed once the reserved size is exceeded, which may lead to a larger + * allocation than your best overestimate would have and will slow the operation that triggers it. + * + * @warning reserve() reserves memory but does not change the size of the flat-multiset. Accessing data beyond the + * current end of the flat-multiset is undefined behavior. + * + * @sa capacity() and squeeze(). + */ + +/*! + * @fn void FlatMultiSet::squeeze() + * + * Releases any memory not required to store the items. + * + * The sole purpose of this function is to provide a means of fine tuning FlatMultiSet's memory usage. In general, + * you will rarely ever need to call this function. + * + * @sa reserve() and capacity(). + */ + +/*! + * @fn iterator FlatMultiSet::begin() const + * + * Same as constBegin(). + */ + +/*! + * @fn const_iterator FlatMultiSet::cbegin() const + * + * Same as constBegin(). + */ + +/*! + * @fn const_iterator FlatMultiSet::constBegin() const + * + * Returns an STL-style iterator pointing to the first item in the FlatMultiSet. + * + * @sa constEnd(). + */ + +/*! + * @fn iterator FlatMultiSet::end() const + * + * Same as constEnd(). + */ + +/*! + * @fn const_iterator FlatMultiSet::cend() const + * + * Same as constEnd(). + */ + +/*! + * @fn const_iterator FlatMultiSet::constEnd() const + * + * Returns an STL-style iterator pointing to the imaginary item after the last item in the FlatMultiSet. + * + * @sa constBegin(). + */ + +/*! + * @fn reverse_iterator FlatMultiSet::rbegin() const + * + * Same as constReverseBegin(). + */ + +/*! + * @fn const_reverse_iterator FlatMultiSet::crbegin() const + * + * Same as constReverseBegin(). + */ + +/*! + * @fn const_reverse_iterator FlatMultiSet::constReverseBegin() const + * + * Returns an STL-style iterator pointing to the last item in the FlatMultiSet. + * + * @sa constReverseEnd(). + */ + +/*! + * @fn reverse_iterator FlatMultiSet::rend() const + * + * Same as constReverseEnd(). + */ + +/*! + * @fn const_reverse_iterator FlatMultiSet::crend() const + * + * Same as constReverseEnd(). + */ + +/*! + * @fn const_reverse_iterator FlatMultiSet::constReverseEnd() const + * + * Returns an STL-style iterator pointing to the imaginary item after the first item in the FlatMultiSet. + * + * @sa constReverseBegin(). + */ + +/*! + * @fn iterator FlatMultiSet::find(const T& value) const + * + * Same as constFind(). + */ + +/*! + * @fn const_iterator FlatMultiSet::constFind(const T& value) const + * + * Returns an iterator positioned at the item @a value in the set. If the set contains multiple + * items of @a value, it is unspecified to which the iterator points. + * + * If the set contains no item @a value, the function returns constEnd(). + */ + +/*! + * @fn iterator FlatMultiSet::erase(const_iterator pos) + * + * Removes the item pointed to by the iterator @a pos from the FlatMultiSet, and returns an + * iterator to the next item in the set. + * + * @note The iterator @a pos @e must be valid and dereferenceable. + * + * @sa remove(). + */ + +/*! + * @fn std::pair FlatMultiSet::equal_range(const T& value) const + * + * Returns a pair of iterators delimiting the range of values [first, second), that are stored with @a value. + */ + + +/*! + * @fn const_iterator FlatMultiSet::lowerBound(const T& value) const + * + * Returns an iterator pointing to the first item with value @a value in the FlatMultiSet. If the set contains no + * item with value @a value, the function returns an iterator to the nearest item with a greater value, + * or constEnd() if there is none. + * + * @sa upperBound() and find(). + */ + +/*! + * @fn const_iterator FlatMultiSet::upperBound(const T& value) const + * + * Returns an iterator pointing to the item that immediately follows the last item with value @a value in the + * FlatMultiSet. If the set contains no item with value @a value, the function returns an iterator to the nearest + * item with a greater value, or constEnd() if there is none. + * + * @sa lowerBound() and find(). + */ + +/*! + * @fn void FlatMultiSet::clear() + * + * Removes all items from the FlatMultiSet. + * + * @sa remove(). + */ + +/*! + * @fn iterator FlatMultiSet::insert(const T& value) + * + * Inserts an item with the value @a value into the FlatMultiSet. + * + * If the container has items with the same value, inserts at the upper bound of that range. + * + * Returns an iterator pointing to the new item. + */ + +/*! + * @fn iterator FlatMultiSet::insert(const_iterator pos, const T& value) + * + * Inserts an item with the value @a value into the FlatMultiSet, using @a pos as a hint as to + * where the item should be inserted. If the hint is correct, the item is inserted directly + * to that position; otherwise, this function is equivalent to insert(const T& value), which + * first has to search for the correct position. + * + * Returns an iterator pointing to the new element. + */ + +/*! + * @fn iterator FlatMultiSet::emplace(Args&&... args) + * + * Inserts an item, constructed in-place using the arguments @a args, into the FlatMultiSet. + * + * If the container has items with the same value, inserts at the upper bound of that range. + * + * Returns an iterator pointing to the new item. + */ + +/*! + * @fn iterator FlatMultiSet::emplace(const_iterator pos, Args&&... args) + * + * Inserts an item, constructed in-place using the arguments @a args, into the FlatMultiSet, + * using @a pos as a hint as to where the item should be inserted. If the hint is correct, + * the item is inserted directly to that position; otherwise, this function is equivalent to + * insert(const T& value), which first has to search for the correct position. + * + * Returns an iterator pointing to the new element. + */ + + +/*! + * @fn size_type FlatMultiSet::remove(const T& value) + * + * Removes all items with the value @a value from the FlatMultiSet and returns the number of + * items removed, if any. + * + * @sa clear(). + */ + +/*! + * @fn qsizetype FlatMultiSet::removeIf(Predicate pred) + * + * Removes all items for which the predicate @a pred returns true from the FlatMultiSet. + * + * Returns the number of elements removed, if any. + * + * @sa clear() and. + */ + +/*! + * @fn void FlatMultiSet::swap(FlatMultiSet& other) + * + * Swaps FlatMultiSet @a other with this FlatMultiSet. This operation is very fast and never fails. + */ + +/*! + * @fn QList FlatMultiSet::values() const + * + * Returns a list containing all of the values in the FlatMultiSet in order. + * + * This function creates a new list, in linear time. The time and memory use that entails can be + * avoided by iterating from begin() to end(). + */ + +//-Operators--------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn bool FlatMultiSet::operator==(const FlatMultiSet& other) const + * + * Returns @c true if @a other is equal to this FlatMultiSet; otherwise, returns @c false. + * + * Two FlatMultiSet's are considered equal if they contain the same items, and only the same items. + * + * This function requires T to implement @c operator==(). + */ + +} diff --git a/lib/core/src/qx-json.cpp b/lib/core/src/qx-json.cpp index 42bf88c4..5cfd9080 100644 --- a/lib/core/src/qx-json.cpp +++ b/lib/core/src/qx-json.cpp @@ -19,19 +19,9 @@ * @file qx-json.h * @ingroup qx-core * - * @brief The qx-json header file provides various utilities for JSON data manipulation. + * @brief The qx-json header file offers various utilities for JSON data manipulation. * - * The mechanisms of this file introduce a highly flexible, simple to use, declarative - * mechanism for parsing/serializing JSON data into user structs and other types. - * - * For example, the following JSON data: - * @snippet qx-json.cpp 0 - * - * can easily be parsed into a corresponding set of C++ data structures like so: - * @snippet qx-json.cpp 1 - * - * Likewise, the structure can be serialized back out into textual JSON data with: - * @snippet qx-json.cpp 2 + * Most significantly this file provides access to @ref declarativejson "Qx Declarative JSON". * * @sa QX_JSON_STRUCT(), and QxJson. */ @@ -143,6 +133,9 @@ namespace Qx * @var JsonError::Form JsonError::EmptyDoc * The provided JSON document is empty. * + * @var JsonError::Form JsonError::InvalidValue + * The value of a type was invalid for that type. + * * @var JsonError::Form JsonError::MissingFile * The JSON containing file was not found. * diff --git a/lib/core/src/qx-lopmap.dox b/lib/core/src/qx-lopmap.dox new file mode 100644 index 00000000..b4a9cb45 --- /dev/null +++ b/lib/core/src/qx-lopmap.dox @@ -0,0 +1,691 @@ +namespace Qx +{ + +/*! + * @concept lopmap_iterator_predicate + * @brief Specifies that a predicate is a valid, iterator based predicate for a lopmap. + * + * Satisfied if the predicate takes a Qx::Lopmap::const_iterator and returns @c bool. + */ + +/*! + * @concept lopmap_pair_predicate + * @brief Specifies that a predicate is a valid, pair based predicate for a lopmap. + * + * Satisfied if the predicate takes a std::pair and returns @c bool. + */ + +/*! + * @concept lopmap_predicate + * @brief Specifies that a predicate is a valid predicate for a lopmap. + * + * Satisfied if the predicate satisfies lopmap_iterator_predicate or lopmap_pair_predicate. + */ + +//=============================================================================================================== +// Lopmap +//=============================================================================================================== + +/*! + * @class Lopmap qx/core/qx-lopmap.h + * @ingroup qx-core + * + * @brief The Lopmap class is a template class that provides an "lopsided" associative array. + * + * Qx::Lopmap is like QMap, except that values in the map are sorted + * by value instead of key, with an order dictated by Compare. + * + * Unlike QMap, iterator is simply an alias for const_iterator as values cannot be modified through lopmap iterators + * since that would affect ordering. Additionally, some methods that would traditional take Key as an argument, + * instead take T. + * + * The value type of Lopmap must provide operator<(), or a custom Compare object must be + * provided specifying a total order. + */ + +//-Aliases-------------------------------------------------------------------------------------------------- +//Public: +/*! + * @typedef Lopmap::iterator + * Typedef for const_iterator. + * + * @typedef Lopmap::ConstIterator + * Qt-style synonym for Lopmap::const_iterator. + * + * @typedef Lopmap::Iterator + * Qt-style synonym for Lopmap::iterator. + * + * @typedef Lopmap::reverse_iterator + * Typedef for const_reverse_iterator. + * + * @typedef Lopmap::ConstReverseIterator + * Qt-style synonym for const_reverse_iterator. + * + * @typedef Lopmap::ReverseIterator + * Qt-style synonym for reverse_iterator. + * + * @typedef Lopmap::difference_type + * Typedef for the container's difference type, usually std::ptrdiff_t. + * + * @typedef Lopmap::key_type + * Typedef for Key. + * + * @typedef Lopmap::mapped_Type + * Typedef for T. + * + * @typedef Lopmap::size_type + * Typedef for the container's size_type, usually std::size_t. + * + * @typedef Lopmap::value_compare + * Typedef for Compare. + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn Lopmap::Lopmap() + * + * Creates an empty lopmap. + * + * @sa clear(). + */ + +/*! + * @fn Lopmap::Lopmap(std::initializer_list> list) + * + * Creates a lopmap with a copy of each of the elements in the initializer list @a list. + */ + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn iterator Lopmap::begin() + * + * Same as constBegin(). + */ + +/*! + * @fn iterator Lopmap::begin() const + * + * @overload + */ + +/*! + * @fn const_iterator Lopmap::cbegin() const + * + * Same as constBegin(). + */ + +/*! + * @fn const_iterator Lopmap::constBegin() const + * + * Returns an STL-style iterator pointing to the first item in the lopmap. + * + * @sa constEnd(). + */ + +/*! + * @fn iterator Lopmap::end() + * + * Same as constEnd(). + */ + +/*! + * @fn const_iterator Lopmap::end() const + * + * @overload + */ + +/*! + * @fn const_iterator Lopmap::cend() const + * + * Same as constEnd(). + */ + +/*! + * @fn const_iterator Lopmap::constEnd() const + * + * Returns an STL-style iterator pointing to the imaginary item after the last item in the lopmap. + * + * @sa constBegin(). + */ + +/*! + * @fn reverse_iterator Lopmap::rbegin() + * + * Same as constReverseBegin(). + */ + +/*! + * @fn const_reverse_iterator Lopmap::rbegin() const + * + * @overload + */ + +/*! + * @fn const_reverse_iterator Lopmap::crbegin() const + * + * Same as constReverseBegin(). + */ + +/*! + * @fn const_reverse_iterator Lopmap::constReverseBegin() const + * + * Returns an STL-style iterator pointing to the last item in the lopmap. + * + * @sa constReverseEnd(). + */ + +/*! + * @fn reverse_iterator Lopmap::rend() + * + * Same as constReverseEnd(). + */ + +/*! + * @fn const_reverse_iterator Lopmap::rend() const + * + * @overload + */ + +/*! + * @fn const_reverse_iterator Lopmap::crend() const + * + * Same as constReverseEnd(). + */ + +/*! + * @fn const_reverse_iterator Lopmap::constReverseEnd() const + * + * Returns an STL-style iterator pointing to the imaginary item after the first item in the lopmap. + * + * @sa constReverseBegin(). + */ + +/*! + * @fn iterator Lopmap::find(const Key& key) + * + * Same as constFind(). + */ + +/*! + * @fn const_iterator Lopmap::find(const Key& key) const + * + * @overload + */ + +/*! + * @fn const_iterator Lopmap::constFind(const Key& key) const + * + * Returns an iterator pointing to the item with the key @a key in the lopmap. + * + * If the lopmaps contains no item with the key @a key, the function returns constEnd(). + */ + +/*! + * @fn iterator Lopmap::lowerBound(const T& value) + * + * Returns an iterator pointing to the first item with value @a value in the lopmap. If the map contains no + * item with value @a value, the function returns an iterator to the nearest item with a greater value, + * or constEnd() if there is none. + * + * @sa upperBound() and find(). + */ + +/*! + * @fn const_iterator Lopmap::lowerBound(const T& value) const + * + * @overload + */ + +/*! + * @fn iterator Lopmap::upperBound(const T& value) + * + * Returns an iterator pointing to the item that immediately follows the last item with value @a value in the + * lopmap. If the map contains no item with value @a value, the function returns an iterator to the nearest + * item with a greater value, or constEnd() if there is none. + * + * @sa lowerBound() and find(). + */ + +/*! + * @fn const_iterator Lopmap::upperBound(const T& value) const + * + * @overload + */ + +/*! + * @fn std::pair Lopmap::equal_range(const T& value) + * + * Returns a pair of iterators delimiting the range of values [first, second), that are stored with @a value. + */ + +/*! + * @fn std::pair Lopmap::equal_range(const T& value) const + * + * @overload + */ + +/*! + * @fn const T& Lopmap::first() const + * + * Returns a reference to the first value in the lopmap. + * + * This function assumes that the map is not empty. + * + * @sa last(), firstKey(), and isEmpty(). + */ + +/*! + * @fn const Key& Lopmap::firstKey() const + * + * Returns a reference to the first key in the lopmap. + * + * This function assumes that the map is not empty. + * + * @sa lastKey(), first(), and isEmpty(). + */ + +/*! + * @fn const T& Lopmap::last() const + * + * Returns a reference to the last value in the lopmap. + * + * This function assumes that the map is not empty. + * + * @sa last(), firstKey(), and isEmpty(). + */ + +/*! + * @fn const Key& Lopmap::lastKey() const + * + * Returns a reference to the last key in the lopmap. + * + * This function assumes that the map is not empty. + * + * @sa lastKey(), first(), and isEmpty(). + */ + +/*! + * @fn Key Lopmap::key(const T& value, const Key& defaultKey) const + * + * Returns the key with value @a value, or @a defaultKey if the lopmap contains no item with @a value. If no + * @a defaultKey is provided the functionr returns a default-constructed key. + * + * This function can be slow, because Lopmap's internal data structure is optimized for fast lookup by key, + * not by value. + * + * @sa value(), and keys(). + */ + +/*! + * @fn iterator Lopmap::erase(const_iterator pos) + * + * Removes the (key, value) pair pointed to by the iterator @a pos from the lopmap, and returns an + * iterator to the next item in the map. + * + * @note The iterator @a pos @e must be valid and dereferenceable. + * + * @sa remove(), and take(). + */ + +/*! + * @fn iterator Lopmap::erase(const_iterator first, const_iterator last) + * + * Removes the (key, value) pairs pointed to by the iterator range [first, last) from the lopmap, + * and returns an iterator to the item in the map following the last removed element. + * + * @note The range @c [first, @c last) @e must be a valid range in @c *this. + * + * @sa remove(), and take(). + */ + +/*! + * @fn void Lopmap::insert(Lopmap&& other) + * + * Moves all the items from @a other into this lopmap. + * + * If a key is common to both maps, its value with be replaced with the value stored in @a other. + */ + +/*! + * @fn void Lopmap::insert(const Lopmap& other) + * + * Inserts all the items in the @a other lopmap into this lopmap. + * + * If a key is common to both maps, its value with be replaced with the value stored in @a other. + */ + +/*! + * @fn iterator Lopmap::insert(const Key& key, const T& value) + * + * Inserts a new item with the key @a key and value of @a value. + * + * If there is already an item with the key @a key, that item's value is replaced with @a value. + * + * Returns an iterator pointing to the new/updated element. + */ + +/*! + * @fn iterator Lopmap::insert(const_iterator pos, const Key& key, const T& value) + * + * Inserts a new item with the key @a key and value @a value and with hint @a pos suggesting where to do + * the insert. + * + * If constBegin() is used as hint it indicates that the @a value should come before any value in the map + * while constEnd() suggests that the @a value should (strictly) come after any key in the map. Otherwise, + * the hint should meet the condition (pos - 1).value() < value <= pos.value() (assuming a Comapre of + * std::less). If the hint @a pos is wrong it is ignored and a regular insert is done. + * + * If there is already an item with the key @a key, that item's value is replaced with @a value. + * + * If the hint is correct, the insert executes in amortized constant time. + * + * When creating a map from sorted data inserting the last value first with constBegin() is faster than + * inserting in sorted order with constEnd(), since constEnd() - 1 (which is needed to check if the hint + * is valid) needs logarithmic time. + * + * Returns an iterator pointing to the new/updated element. + */ + +/*! + * @fn bool Lopmap::contains(const Key& key) const + * + * Returns @c true if the lopmap contains an item with the key @a key; otherwise, + * returns @c false. + * + * @sa count(). + */ + +/*! + * @fn size_type Lopmap::remove(const Key& key) + * + * Remove the item with key @a key from the map if it exists and returns @c 1; otherwise, returns @c 0. + * + * @sa clear() and take(). + */ + +/*! + * @fn qsizetype Lopmap::removeIf(Predicate pred) + * + * Removes all elements for which the predicate @a pred returns true from the lopmap. + * + * The function supports predicates which take either an argument of type Lopmap::const_iterator, + * or an argument of type std::pair. + * + * Returns the number of elements removed, if any. + * + * @sa clear() and take(). + */ + +/*! + * @fn T Lopmap::take(const Key& key) + * + * Removes the item with the key @a key from the lopmap and returns the value + * associated with it. + * + * If this item does not exist in the lopmap, the function simply returns a default-constructed value + * + * If you don't use the return value, remove() is more efficient. + * + * @sa remove(). + */ + +/*! + * @fn void Lopmap::swap(Lopmap& other) + * + * Swaps lopmap @a other with this lopmap. This operation is very fast and never fails. + */ + +/*! + * @fn qsizetype Lopmap::size() const + * + * Returns the number of items in the lopmap. + * + * @sa isEmpty() and count(). + */ + +/*! + * @fn qsizetype Lopmap::count() const + * + * Same as size(). + */ + +/*! + * @fn bool Lopmap::isEmpty() const + * + * Returns @c true if the lopmap contains no items; otherwise, returns @c false. + * + * @sa size(). + */ + +/*! + * @fn bool Lopmap::empty() const + * + * Same as isEmpty(). + */ + +/*! + * @fn void Lopmap::clear() + * + * Removes all items from the lopmap. + * + * @sa remove(). + */ + +/*! + * @fn T Lopmap::value(const Key& key, const T& defaultValue) const + * + * Returns the value associated with the key @a key. + * + * If the lopmap contains no item with key @a key, the function returns @a defaultValue. + * If no @a defaultValue is specified, the function returns a default-constructed value. + * + * @sa key(), values(), contains(), and operator[](). + */ + +/*! + * @fn QList Lopmap::keys() const + * + * Returns a list containing all of the keys in the lopmap in order of their associated values. + * + * The order is guaranteed to be the same as that used by values(). + * + * This function creates a new list, in linear time. + * + * @sa values() and key(). + */ + +/*! + * @fn QList Lopmap::keys(const T& values) const + * + * Returns a list containing all of the keys associated with the value @a value in order of their + * associated values. + * + * This function creates a new list, in linear time. + */ + +/*! + * @fn QList Lopmap::values() const + * + * Returns a list containing all of the values in the lopmap in order. + * + * This function creates a new list, in linear time. The time and memory use that entails can be + * avoided by iterating from begin() to end(). + * + * @sa keys() and value(). + */ + +//-Operators--------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn T Lopmap::operator[](const Key& key) const + * + * Same as value(). + */ + +/*! + * @fn bool Lopmap::operator==(const Lopmap& other) const + * + * Returns @c true if @a other is equal to this lopmap; otherwise, returns @c false. + * + * Two lopmap's are considered equal if they contain the same (key, value) pairs. + * + * This function requires the key and the value types to implement @c operator==(). + * + * @sa operator!=(). + */ + +/*! + * @fn bool Lopmap::operator!=(const Lopmap& other) const + * + * Returns @c true if @a other is not equal to this lopmap; otherwise, returns @c false. + * + * Two lopmap's are considered equal if they contain the same (key, value) pairs. + * + * This function requires the key and the value types to implement @c operator==(). + * + * @sa operator==(). + */ + +//=============================================================================================================== +// Lopmap::const_iterator +//=============================================================================================================== + +/*! + * @class Lopmap::const_iterator qx/core/qx-lopmap.h + * @ingroup qx-core + * + * @brief The Lopmap::const_iterator class provides an STL-style const iterator for Lopmap. + * + * Lopmap::const_iterator allows you to iterate over a Lopmap. + * + * The default Lopmap::const_iterator constructor creates an uninitialized iterator. You must initialize it using + * a Lopmap function like Lopmap::cbegin(), Lopmap::cend(), or Lopmap::constFind() before you can start iterating. + * + * Lopmap stores its items ordered according to Compare. + * + * Multiple iterators can be used on the same map. If you add items to the map, existing iterators will remain + * valid. If you remove items from the map, iterators that point to the removed items will become dangling iterators. + * + * Inserting relationships into the lopmap or calling methods such as Lopmap::reserve() or Lopmap::squeeze() can + * invalidate all iterators pointing into the lopmap. Iterators are guaranteed to stay valid only as long as the + * Lopmap doesn't have to grow/shrink its internal table. Using any iterator after a rehashing operation has + * occurred will lead to undefined behavior + */ + +//-Aliases---------------------------------------------------------------------------------------------- +//Public: +/*! + * @typedef Lopmap::const_iterator::iterator_category + * A synonym for std::bidirectional_iterator_tag indicating this iterator is a bidirectional iterator. + */ +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn Lopmap::const_iterator::const_iterator() + * + * Constructs an uninitialized iterator. + * + * Functions like key(), value(), and operator++() must not be called on an uninitialized iterator. + * Use operator=() to assign a value to it before using it. + * + * @sa Lopmap::constBegin() and Lopmap::constEnd(). + */ + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn const Key& Lopmap::const_iterator::key() const + * + * Returns the current item's key. + * + * @sa value() and operator*(). + */ + +/*! + * @fn const T& Lopmap::const_iterator::value() const + * + * Returns the current item's value. + * + * @sa key(). + */ + +//-Operators--------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn bool Lopmap::const_iterator::operator==(const const_iterator& other) const + * + * Returns @c true if @a other points to the same item as this iterator; otherwise, returns @c false. + */ + +/*! + * @fn const T& Lopmap::const_iterator::operator*() const + * + * Returns the current item's value. + * + * @sa key(). + */ + +/*! + * @fn const_iterator Lopmap::const_iterator::operator++() + * + * The prefix @c ++ operator @c (++i) advances the iterator to the next item in the lopmap and + * returns an iterator to the new item relationship. + * + * Calling this function on Lopmap::constEnd() leads to undefined results. + * + * @sa operator--(). + */ + +/*! + * @fn const_iterator Lopmap::const_iterator::operator++(int) + * + * The postfix @c ++ operator @c (i++) advances the iterator to the next item in the lopmap and + * returns an iterator to the previously current item. + * + * Calling this function on Lopmap::constEnd() leads to undefined results. + */ + +/*! + * @fn const_iterator Lopmap::const_iterator::operator--() + * + * The prefix @c -- operator @c (--i) makes the preceding item current and + * returns an iterator to the new current item. + * + * Calling this function on Lopmap::constBegin() leads to undefined results. + * + * @sa operator++(). + */ + +/*! + * @fn const_iterator Lopmap::const_iterator::operator--(int) + * + * The postfix @c -- operator @c (--i) makes the preceding item current and + * returns an iterator to the previously current item. + * + * Calling this function on Lopmap::constEnd() leads to undefined results. + */ + +/*! + * @fn const T* Lopmap::const_iterator::operator->() const + * + * Returns a pointer to the current item's value. + * + * @sa value(). + */ + +//=============================================================================================================== +// Lopmap::const_reverse_iterator +//=============================================================================================================== + +/*! + * @class Lopmap::const_reverse_iterator qx/core/qx-lopmap.h + * @ingroup qx-core + * + * @brief The Lopmap::const_reverse_iterator class provides an STL-style const reverse iterator for Lopmap. + * + * Same as Lopmap::const_iterator, except that it works in the opposite direction. + */ + +} diff --git a/lib/core/src/qx-processbider.cpp b/lib/core/src/qx-processbider.cpp index 7cb9b776..af4c672e 100644 --- a/lib/core/src/qx-processbider.cpp +++ b/lib/core/src/qx-processbider.cpp @@ -7,6 +7,7 @@ // Inter-component Includes #include "qx/core/qx-system.h" +#include "__private/qx-generalworkerthread.h" // TODO: Add the ability to add a starting PID so that if more than one process with the same // name are running, the user can specify which to latch to (won't matter for grace restart though) @@ -136,7 +137,7 @@ void ProcessBiderWorker::handleClosure(std::chrono::milliseconds timeout, bool f /* In case a grace expiration is queued and the process might still be running, queue a closure * for if the process is hooked again */ - Qt::ConnectionType ct = static_cast(Qt::AutoConnection | Qt::SingleShotConnection | Qt::UniqueConnection); + Qt::ConnectionType ct = static_cast(Qt::AutoConnection | Qt::SingleShotConnection | Qt::UniqueConnection); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) connect(this, &ProcessBiderWorker::processHooked, this, [this, timeout, force]{ mWaiter.close(timeout, force); }, ct); @@ -155,100 +156,24 @@ void ProcessBiderWorker::bide() // ProcessBiderManager //=============================================================================================================== -/* NOTE: We explicitly avoid haivng the manager be a QObject here since it may be spawned in any thread - * due to RAII and therefore we can't be certain about thread afinity. Additionally, for this reason, we +/* NOTE: We explicitly avoid having the manager be a QObject here since it may be spawned in any thread + * due to RAII and therefore we can't be certain about thread affinity. Additionally, for this reason, we * must be sure to avoid using 'this' as a context parameter for any connections that the manager makes * so that nothing is set to run in the managers thread. The manager could be a QObject and could be moved * to the main thread upon creation to ensure it's thread affinity is always valid, but we don't want the * managers operation to potentially get held up just because the main thread may block. */ -//-Constructor---------------------------------------------------------------------------------------------- -//private: -ProcessBiderManager::ProcessBiderManager() : - mThread(nullptr), - mWorkerCount(0) -{} - -//-Destructor---------------------------------------------------------------------------------------------- -//Public: -ProcessBiderManager::~ProcessBiderManager() -{ - // In theory this could be deleted while something else is accessing it, however unlikely that is given it's static - QMutexLocker lock(&smMutex); - stopThreadIfStarted(true); -} - -//-Class Functions---------------------------------------------------------------------------------------------- -//Private: -Qx::ExclusiveAccess ProcessBiderManager::instance() -{ - /* We don't control the ProcessBider instances, so they could be in any thread and we need - * to synchronize access to the manager (also becuase workers access this). - * An alternative is making the manager a QObject and using signals/slots, but that gets tricky since it's created - * via RAII and therefore may be created in a thread that gets destroyed (and would then have no affinity). - * This is simpler. - */ - static ProcessBiderManager m; - return Qx::ExclusiveAccess(&m, &smMutex); // Provides locked access to manager, that unlocks when destroyed -} - -//-Instance Functions--------------------------------------------------------------------------------------------- -//Private: -void ProcessBiderManager::startThreadIfStopped() -{ - if(mThread) - return; - - QThread* mainThread = QCoreApplication::instance()->thread(); - if(!mainThread) [[unlikely]] - { - // It's documented that you're not supposed to use QObjects before QCoreAppliation is created, - // but check explicitly anyway - qCritical("Cannot use ProcessBiders before QCoreApplication is created"); - } - - mThread = new QThread(); - - /* mThread is the only QObject member of this class. We need to move it to the main thread because - * the manager can be created in any thread since it's done by RAII and UB occurs if a Object (in this case - * the QThread) continues to be used if the thread it belongs to is shutdown. moveToThread() already checks - * if this is the main thread and results in a no-op if so - */ - mThread->moveToThread(mainThread); - mThread->start(); -} - -void ProcessBiderManager::stopThreadIfStarted(bool wait) -{ - if(!mThread || !mThread->isRunning()) - return; - - // Quit thread, queue it for deletion, and abandon it - mThread->quit(); - QObject::connect(mThread, &QThread::finished, mThread, &QObject::deleteLater); - if(wait) - mThread->wait(); - mThread = nullptr; -} - //Public: void ProcessBiderManager::registerBider(ProcessBider* bider) { - startThreadIfStopped(); - ProcessBiderWorker* worker = new ProcessBiderWorker(); worker->setGrace(bider->respawnGrace()); worker->setProcessName(bider->processName()); worker->setStartWithGrace(bider->initialGrace()); - worker->moveToThread(mThread); - mWorkerCount++; // Management QObject::connect(bider, &QObject::destroyed, worker, &QObject::deleteLater); // If bider is unexpectedly deleted, remove bide worker - QObject::connect(mThread, &QThread::finished, worker, &QObject::deleteLater); // Kill worker if it still exists and the thread is being shutdown - QObject::connect(worker, &QObject::destroyed, worker, []{ ProcessBiderManager::instance()->notifyWorkerFinished(); }); - // ^ Have worker notify the manager when it dies to track count. Use static accessor for synchronization instead of capturing 'this' QObject::connect(worker, &ProcessBiderWorker::complete, worker, &QObject::deleteLater); // Self-trigger cleanup of worker QObject::connect(bider, &ProcessBider::stopped, worker, &ProcessBiderWorker::handleAbort); // Handle aborts QObject::connect(bider, &ProcessBider::__startClose, worker, &ProcessBiderWorker::handleClosure); // Handle closes @@ -281,16 +206,14 @@ void ProcessBiderManager::registerBider(ProcessBider* bider) }); }); + // Move to worker thread + auto gwt = GeneralWorkerThread::instance(); + gwt->moveTo(worker); + // Start work asynchronously QMetaObject::invokeMethod(worker, &ProcessBiderWorker::bide); } -void ProcessBiderManager::notifyWorkerFinished() -{ - if(!--mWorkerCount) - stopThreadIfStarted(); -} - /*! @endcond */ //=============================================================================================================== @@ -523,7 +446,7 @@ void ProcessBider::start() return; mBiding = true; - ProcessBiderManager::instance()->registerBider(this); + ProcessBiderManager::registerBider(this); emit started(); } diff --git a/lib/core/src/qx-processbider_p.h b/lib/core/src/qx-processbider_p.h index c963a3f1..2516143d 100644 --- a/lib/core/src/qx-processbider_p.h +++ b/lib/core/src/qx-processbider_p.h @@ -91,39 +91,13 @@ class ProcessBider; class ProcessBiderManager { -//-Class Members--------------------------------------------------------------------------------------------- -private: - // Needs to be static so it can be locked before the the singleton is created, or else a race in instance() could occur. - static inline constinit QMutex smMutex; - -//-Instance Members------------------------------------------------------------------------------------------ -private: - QThread* mThread; - int mWorkerCount; - -//-Constructor---------------------------------------------------------------------------------------------- -private: - explicit ProcessBiderManager(); - -//-Destructor---------------------------------------------------------------------------------------------- -public: - ~ProcessBiderManager(); - -//-Class Functions---------------------------------------------------------------------------------------------- + // Doesn't need synchronization (i.e. ThreadSafeSingleton) as there are no data members + // TODO: This is so simple, maybe just make it a static member of ProcessBider +//-Class Functions--------------------------------------------------------------------------------------------- public: - static Qx::ExclusiveAccess instance(); - -//-Instance Functions---------------------------------------------------------------------------------------------- -private: - void startThreadIfStopped(); - void stopThreadIfStarted(bool wait = false); - -public: - void registerBider(ProcessBider* bider); - void notifyWorkerFinished(); - -/*! @endcond */ + static void registerBider(ProcessBider* bider); }; +/*! @endcond */ } diff --git a/lib/core/src/qx-property.cpp b/lib/core/src/qx-property.cpp new file mode 100644 index 00000000..4f07b77b --- /dev/null +++ b/lib/core/src/qx-property.cpp @@ -0,0 +1,1573 @@ +// Unit Include +#include "qx/core/qx-property.h" +#include "qx/core/__private/qx-property_detail.h" +#include "qx-property_p.h" + +/* I got through most of the core implementation of this, only to then find out that It seems + * like what I'm doing here is essentially creating/manipulating with DAGs (Directed Acyclic Graph), + * funny accidental "invention" of an existing concept. + * + * One take away from that is that it might be more optimal to try and implement some kind of + * running topographical sort that can be traversed during an update wave instead of the current + * system, although the current system is nearly the same thing already. + */ + +// TODO: Try to reduce the cross-over between Qx and _QxPrivate here + +/*! + * @file qx-property.h + * @ingroup qx-core + * + * @brief The qx-property.h header file provides access to the @ref properties "Qx Bindable Properties System" + */ + +/*! @cond */ +namespace Qx +{ + +//=============================================================================================================== +// DepthSortedLinks +//=============================================================================================================== + +//-Instance Functions------------------------------------------------------------- +//Public: +bool DepthSortedLinks::remove(const PropertyNode* node) { return FlatMultiSet::removeIf([node](const DepthLink& l){ return l == node; }) > 0; } + +DepthSortedLinks::const_iterator DepthSortedLinks::insert(PropertyNode* node) +{ + /* Enforces uniqueness for nodes + * + * Updates with the same depth are ignored to avoid necessary removal/insert; otherwise, + * the link is removed and re-inserted to enforce proper sorting. + */ + auto currentDepth = node->depth(); + auto itr = std::find(cbegin(), cend(), node); + bool existing = itr != cend(); + bool sameDepth = existing && itr->stableDepth == currentDepth; + if(!sameDepth) + { + if(existing) + itr = FlatMultiSet::erase(itr); + + itr = FlatMultiSet::insert(itr, {node, currentDepth}); // Use old post pos as a hint, as the new value is likely only 1 greater and therefore nearby + } + + return itr; +} + +//=============================================================================================================== +// PropertyNode +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Public: +PropertyNode::PropertyNode(IFace* property) : + mProperty(property) +{} + +//-Destructor------------------------------------------------------------- +//Public: +PropertyNode::~PropertyNode() +{ + Q_ASSERT(!Qx::PropertyCoordinator::instance()->isBindingBeingEvaluated()); // Do not support deleting a binding within a binding eval + disconnectDependents(); + disconnectDependencies(); +} + +//-Instance Functions------------------------------------------------------------- +//Private: +template +void PropertyNode::depthAlteringOperation(Operation o) +{ + /* Minor stack overflow concerns here due to recursion that could be quelled + * by making this iterative instead + */ + Depth originalDepth = depth(); + o(); + + // Propagate + if(depth() != originalDepth) + for(const auto& d : std::as_const(mDependencies)) + d->addOrUpdateDependent(this); +} + +bool PropertyNode::recursiveNodeSearch(const PropertyNode* searchNode, const PropertyNode* target) +{ + // DFS + for(const PropertyNode* dependency : searchNode->mDependencies) + { + if(dependency == target || recursiveNodeSearch(dependency, target)) + return true; + } + + return false; +} + +void PropertyNode::checkForCycle(const PropertyNode* newDependency) +{ + /* Uses a DFS to check for a cycle at connection time. A different approach, like those mentioned higher + * in this file, could be more efficient. + * + * TODO: This could cause a stack overflow, but that would likely only happen with a crazy number of node + * connections; still, it's something to work around by using an iterative approach instead. + * + * TODO: Consider making this debug configuration only + */ + bool cycle = recursiveNodeSearch(newDependency, this); + if(cycle) + qFatal("Property dependency cycle occurred while connecting %p to %p", this, newDependency); +} + +void PropertyNode::addOrUpdateDependent(PropertyNode* dependent) +{ + depthAlteringOperation([this, dependent]{ mDependents.insert(dependent); }); +} + +void PropertyNode::removeDependency(const PropertyNode* dependency) { mDependencies.removeAll(dependency); } + +void PropertyNode::removeDependent(const PropertyNode* dependent) +{ + depthAlteringOperation([this, dependent]{ mDependents.remove(const_cast(dependent)); }); +} + +//Public: +PropertyNode::IFace* PropertyNode::property() const { return mProperty; } +PropertyNode::Depth PropertyNode::depth() const { return mDependents.isEmpty() ? 0 : mDependents.first().stableDepth + 1; } +PropertyNode::Itr PropertyNode::cbeginDependents() const { return mDependents.cbegin(); } +PropertyNode::Itr PropertyNode::cendDependents() const { return mDependents.cend(); } +PropertyNode::Links PropertyNode::dependencies() const { return mDependencies; } + +void PropertyNode::relinkProperty(PropertyNode::IFace* property) { mProperty = property; } + +bool PropertyNode::addDependency(PropertyNode* dependency) +{ + /* Originally this was addOrUpdateDependency() and always refreshed connection from + * dependency to this (i.e dependency->addOrUpdateDependent(this); ), but I'm almost + * certain this is unnecessary, as when a node is being freshly connected it will be + * updated correctly by that line in the if statement below, and if an already connected + * node needs to be updated because of a higher connection, it will be refreshed + * immediately since the update is recursive. + * + * Function returns true if the node was actually added (new), or false if not (existing). + */ + Q_ASSERT(dependency != this); + /* A set would be nice to avoid this check, but we favor iteration speed, at it also + * helps prevent touching the list-backed Dependents container if the node is already + * present. + */ + if(!mDependencies.contains(dependency)) + { + checkForCycle(dependency); + mDependencies.append(dependency); + dependency->addOrUpdateDependent(this); + return true; + } + + return false; +} + +void PropertyNode::disconnectDependents() +{ + /* Might want some kind of assert here as we don't really support disconnecting nodes in + * the middle of an update, though technically its OK if it doesn't cause a dangling pointer, + * but that's on the user to ensure; however, it would be nice to watch for troublesome + * disconnections specifically somehow and explicitly abort with a message of detected. + * + * It's worth noting that since observer updates are queued to happen at the end of an update + * wave, deletions of properties, or the removal of their bindings, that are called for during + * an update wont take effect until its end. + */ + + // Disconnect from dependents + for(auto itr = mDependents.cbegin(); itr != mDependents.cend();) + { + itr->node->removeDependency(this); // Disconnects other from this + itr = mDependents.erase(itr); // Disconnects this from other + } +} + +void PropertyNode::disconnectDependencies() +{ + /* Might want some kind of assert here as we don't really support disconnecting nodes in + * the middle of an update, though technically its OK if it doesn't cause a dangling pointer, + * but that's on the user to ensure; however, it would be nice to watch for troublesome + * disconnections specifically somehow and explicitly abort with a message of detected. + * + * It's worth noting that since observer updates are queued to happen at the end of an update + * wave, deletions of properties, or the removal of their bindings, that are called for during + * an update wont take effect until its end. + */ + + // Disconnect from dependencies + for(auto itr = mDependencies.cbegin(); itr != mDependencies.cend();) + { + (*itr)->removeDependent(this); // Disconnects other from this + itr = mDependencies.erase(itr); // Disconnects this from other clazy:exclude=strict-iterators + } +} + +//=============================================================================================================== +// PropertyDependentWalker +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Public: +PropertyDependentWalker::PropertyDependentWalker(PropertyNode* node) : + mNode(node), + mDepItr(node->cbeginDependents()), + mDepEnd(node->cendDependents()) +{} + +//-Instance Functions------------------------------------------------------------- +//Public: +const PropertyNode* PropertyDependentWalker::node() const { return mNode; } +PropertyNode::Depth PropertyDependentWalker::depth() const { return mNode->depth(); } +bool PropertyDependentWalker::isExhausted() const { return mDepItr == mDepEnd; } + +void PropertyDependentWalker::refresh(PropertyNode::Depth depth) +{ + /* This deals with when the dependent iterators are invalidated by reacquiring them + * and resetting the active iterator to the start of entries at 'depth' + */ + mDepEnd = mNode->cendDependents(); + mDepItr = std::lower_bound(mNode->cbeginDependents(), mDepEnd, depth); +} + +std::optional PropertyDependentWalker::fork(PropertyNode::Depth targetDepth, QSet& ignore) +{ + while(!isExhausted()) + { + PropertyNode* dependent = mDepItr->node; + Q_ASSERT(targetDepth >= dependent->depth()); // Depths higher than 'targetDepth' should already have been processed + if(dependent->depth() < targetDepth) + return std::nullopt; // Bail with iterator still on first at higher depth + + // Push iterator to next depth regardless of if we fork or not from this point + ++mDepItr; + + // Fork if not ignored + if(!ignore.contains(dependent)) + { + ignore.insert(dependent); // Prevent other walkers from forking here + return PropertyDependentWalker(dependent); + } + } + + return std::nullopt; +} + +bool PropertyDependentWalker::evaluate() { return PropertyCoordinator::instance()->evaluate(mNode->property()); } + +//=============================================================================================================== +// PropertyWalkerManager +//=============================================================================================================== + +/* This, along with its "static iterator", allow storing walkers in a fashion where the current list can be iterated + * from start to finish while adding new items that are not covered by the iteration until the next cycle, while + * only using one container. This also automatically handles erasing exhausted walkers. + */ + +//-Constructor------------------------------------------------------------- +//Public: +PropertyWalkerManager::PropertyWalkerManager(const QList& origins) : + mWalkers(origins.cbegin(), origins.cend()) +{} + +//-Instance Functions------------------------------------------------------------- +//Public: +bool PropertyWalkerManager::isEmpty() const { return mWalkers.isEmpty(); } +PropertyWalkerManager::Iterator PropertyWalkerManager::staticIterator() { return Iterator(mWalkers); } +void PropertyWalkerManager::addWalker(PropertyDependentWalker&& walker) { mWalkers.append(std::move(walker)); } + +void PropertyWalkerManager::refreshWalkers(PropertyNode::Depth targetDepth) +{ + for(auto& w : mWalkers) + w.refresh(targetDepth); +} + +//=============================================================================================================== +// PropertyWalkerManager::Iterator +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Private: +PropertyWalkerManager::Iterator::Iterator(PropertyWalkerManager::Container& c) : + mContainer(c), + mIdx(0), + mEndIdx(c.count() - 1) +{} + +//-Operators---------------------------------------------------------------- +//Public: +PropertyWalkerManager::Iterator::operator bool() const { return mIdx <= mEndIdx; } +PropertyDependentWalker& PropertyWalkerManager::Iterator::operator*() { Q_ASSERT(static_cast(*this)); return mContainer[mIdx]; } +PropertyDependentWalker* PropertyWalkerManager::Iterator::operator->() { Q_ASSERT(static_cast(*this)); return &mContainer[mIdx]; } + +PropertyWalkerManager::Iterator& PropertyWalkerManager::Iterator::operator++() +{ + Q_ASSERT(static_cast(*this)); + auto& self = *this; + if(self->isExhausted()) + { + mContainer.remove(mIdx); + --mEndIdx; + } + else + ++mIdx; + + return *this; +} + +//=============================================================================================================== +// PropertyUpdateWave +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Public: +PropertyUpdateWave::PropertyUpdateWave() = default; + +PropertyUpdateWave::PropertyUpdateWave(PropertyNode* initiator) : + mOrigins{initiator}, + mChangedNodes{initiator}, + mDeadends{initiator} +{ + /* We need to detect when the dependency graph changes in a potentially consequential way (that is, when a node + * being evaluated suddenly depends on nodes that are part of the update wave, but which haven't been + * evaluated yet themselves) happens due new dependencies being picked up that were not previously known + * (e.g. due to boolean short-circuiting during previous passes). + * + * So, we save which nodes have already been handled or are known to be deadends in order to save time should + * a reflow occur. + * + * The origin node is of course already handled so it's added above immediately. Technically, when using + * multiple origins, it should be impossible for a deeper origin to ever fork to a higher one as origin's + * should never have dependencies, but I'm throwing them to mDeadends to prevent walker overlap just in case. + */ +} + +//-Instance Functions------------------------------------------------------------- +//Private: +bool PropertyUpdateWave::reflowFinished(const PropertyNode* newFork) +{ + if(!mReflowStack.empty() && mReflowStack.top() == newFork) + { + // Finish up + mReflowStack.pop(); + mWalkersStale = true; // So that the next flow down knows to refresh its walkers + return true; + } + + return false; +} + +void PropertyUpdateWave::postReflowRefresh(PropertyWalkerManager& walkerManager, PropertyNode::Depth d) +{ + /* Any graph modifications at all will trash the node iterators within our walkers (and there + * might also be depth differences based on the changes), so we always re-init them after any + * invalidation (a reflow) occurs. + * + * mOriginalWalkersStale is how we know if a reflow occurred after performing an evaluation, + * in which case any walker's internal iterator could have been invalidated so we refresh + * them all here. + * + * The active fork will always remain valid so that doesn't need to be touched. + * + * The current walker iterator in PropertyUpdateWave::flow() might be in the middle of its current + * depth (e.g. pointing to the 2nd dependent at depth 3 out of 4 dependents with that depth), and will + * be reset to the 1st at the target depth; however, this isn't an issue as the 'ignore' parameter of + * PropertyDependentWalker::fork() will prevent that walker from double-forking to nodes id already covered, + * and instead it will safely just iterate over those. + */ + if(!mWalkersStale) + return; + + walkerManager.refreshWalkers(d); + mWalkersStale = false; +} + +void PropertyUpdateWave::notifyObservers() +{ + // Notify observers of all nodes that changed + for(const auto node : std::as_const(mChangedNodes)) + node->property()->notifyObservers(); +} + +//Public: +const QList& PropertyUpdateWave::origins() const { return mOrigins; } +bool PropertyUpdateWave::isValid() const { return !mOrigins.isEmpty(); } + +void PropertyUpdateWave::addInitiator(PropertyNode* initiator) +{ + if(mOrigins.contains(initiator)) + return; + + // See ctor for notes on this + mOrigins.append(initiator); + mChangedNodes.append(initiator); + mDeadends.insert(initiator); +} + +void PropertyUpdateWave::flow() +{ + Q_ASSERT(isValid()); + + // Determine starting (greatest) depth + auto depthCompare = [](const PropertyNode* a, const PropertyNode* b) { return a->depth() < b->depth(); }; + PropertyNode::Depth startDepth = (*std::max_element(mOrigins.cbegin(), mOrigins.cend(), depthCompare))->depth(); + + // If all top level nodes, all we need to do is notify + if(startDepth == 0) + { + notifyObservers(); + return; + } + + // Tracking, start with one walker at each origin + PropertyWalkerManager walkers(mOrigins); + + // Walk tree until all depths under start depth are covered + for(PropertyNode::Depth d = startDepth - 1; d >= 0; --d) + { + /* Update global depth. + * + * This is for potential nested reflows. This allows each invocation to keep its own + * depth, while also tracking the depth of the most recent invocation. + */ + mGlobalDepth = d; + + /* We want to prevent two walkers from forking onto the same node (in the case where + * node dependents converge). Because forking is delayed until a given depth is reached + * two convergent nodes should always end up trying to fork onto the common node at + * the same depth, so we just need to temporarily track which nodes have been covered + * during a depth iteration and ignore them for subsequent walkers until we get to the + * next depth. We start with known dead-ends which are preserved for the entire process + * and fork() will add any that have been gone to for the current depth. + */ + QSet noGoNodes = mDeadends; + + // Progress current walkers + for(auto wlkItr = walkers.staticIterator(); wlkItr; ++wlkItr) + { + // Keep forking until current depth is exhausted + while(auto optFork = wlkItr->fork(d, noGoNodes)) + { + auto fork = *optFork; + auto fNode = fork.node(); + // If this is a reflow and we reached the trigger point, bail so the original can finish + if(reflowFinished(fNode)) + return; + + // Evaluate the node if not already done (i.e. reflow) + bool proceed = mChangedNodes.contains(fNode); + if(!proceed) + { + bool valueChanged = fork.evaluate(); // Can trigger reflow + postReflowRefresh(walkers, d); // Handles reflow cleanup if one occurred + if(valueChanged) + { + mChangedNodes.append(fNode); + proceed = true; + } + else + mDeadends.insert(fNode); + } + + if(proceed && !fork.isExhausted()) + walkers.addWalker(std::move(fork)); + } + } + } + Q_ASSERT(walkers.isEmpty()); // No walkers should remain + Q_ASSERT(mReflowStack.empty()); // Only an original flow should reach the end here + + // Notify observers of all nodes that changed + notifyObservers(); +} + +void PropertyUpdateWave::reflowIfNeeded(const PropertyNode* evaluating, const PropertyNode* dep, PropertyNode::Depth depOrigDepth) +{ + // Also need to make sure the focal node wasn't already processed in the case where the depths are equal + if(depOrigDepth < mGlobalDepth || (depOrigDepth == mGlobalDepth && !mChangedNodes.contains(dep))) + { + /* We use the node that was under evaluation when this was detected to know when the reflow is finished + * (when it's reached again). We can't use the new dependency node itself as it may never be reached + * due to dead-ends, but we know that the node currently being evaluated must be reached again. + */ + mReflowStack.push(evaluating); + flow(); + } +} + +//=============================================================================================================== +// PropertyCoordinator +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Public: +PropertyCoordinator::PropertyCoordinator() : + mUpdateDelay(0) +{} + +//-Class Functions---------------------------------------------------------------- +//Public: +PropertyCoordinator* PropertyCoordinator::instance() { thread_local static PropertyCoordinator pc; return &pc; } + +//-Instance Functions------------------------------------------------------------- +//Private: +void PropertyCoordinator::checkForCycle(const PropertyNode* notifyingProperty) const +{ + /* Checks for a cycle that could occur during an update wave (i.e. after connection time) + * due to indirect update triggers, like user configured observer functions. + * + * To do this, we see if any new update wave that gets queued originates from + * the same node of any update wave that is active. Active update waves (tracked via + * just their nodes) are any that were started and then handled all in one go, originating + * from the same call-site. Normally this is just one wave, but if any others are triggered (e.g. + * due to a user observer function) during this update wave, they are queued and then handled + * in sequence once the original finishes. This active stack is only cleared once all queued + * waves are finished, which is what allows for checking for cycles. + * + * If for some reason any false-positives occur from this, we can have the active list be of a + * struct that not only includes the update origin node, but also a pointer to the node that + * was being updated when that origin node was queued, so that a cycle is only considered to have + * occurred if the new queued update that has the same origin node as an active item was spawned + * by the same node as well, though I don't think this is required. + * + * Technically, a cycle could still occur due to some kind of loop in user-code, but then that's + * their problem. + */ + if(mActiveUpdate.origins().contains(notifyingProperty) || mChainedUpdates.contains(notifyingProperty)) + qFatal("Property dependency cycle occurred during update (caught on %p)", notifyingProperty); +} + +void PropertyCoordinator::queueUpdateWave(PropertyNode* origin) +{ + // If delaying, note origin and wait + if(mUpdateDelay) + { + mDelayedUpdate.addInitiator(origin); + return; + } + + // Otherwise, add to queue, start processing if not already + mUpdateQueue.push(PropertyUpdateWave(origin)); + if(!mActiveUpdate.isValid()) + processUpdateQueue(); +} + +void PropertyCoordinator::processUpdateQueue() +{ + Q_ASSERT(!mActiveUpdate.isValid() && !mDelayedUpdate.isValid() && mChainedUpdates.isEmpty() && mUpdateQueue.size() == 1); + + // This will handle nested update waves (queues during eval) until no more occur + while(!mUpdateQueue.empty()) + { + // Make top queue item active and start processing + mActiveUpdate = std::move(mUpdateQueue.front()); + mUpdateQueue.pop(); + mActiveUpdate.flow(); + + // Added completed wave to cycle detection list (mActiveUpdate is replaced on next iteration) + for(const auto o : mActiveUpdate.origins()) + mChainedUpdates.append(o); + } + + /* Initial process invocation is complete, clear active and chain list since it's only for detecting + * cycles caused by nested update waves, so once all have been exhausted any new waves + * that are started should be independent. + */ + mActiveUpdate = {}; + mChainedUpdates.clear(); +} + +//Public: +bool PropertyCoordinator::isBindingBeingEvaluated() const { return !mEvaluationStack.empty(); } + +bool PropertyCoordinator::evaluate(PropertyCoordinator::IFace* property) +{ + mEvaluationStack.push(property->node()); + bool changed = property->callBinding(); + mEvaluationStack.pop(); + return changed; +} + +void PropertyCoordinator::evaluateAndNotify(PropertyCoordinator::IFace* property) +{ + if(evaluate(property)) + notify(property); +} + +void PropertyCoordinator::notify(PropertyCoordinator::IFace* property) +{ + auto node = property->node(); + checkForCycle(node); + queueUpdateWave(node); +} + +void PropertyCoordinator::addOrUpdateCurrentEvalDependency(const PropertyCoordinator::IFace* property) +{ + if(mEvaluationStack.empty()) + return; + + auto node = property->node(); + auto originalDepth = node->depth(); + bool newDependency = mEvaluationStack.top()->addDependency(node); + + /* If the dependency was not pre-existing and an update is active, that update's graph is now + * potentially invalid if the depth of the added node was one that the wave hasn't reached yet + * (i.e. the node's depth changed meaning it needs to be processed before the current step). + * There are two cases in that context: + * + * 1) The new dependency is not part of the update wave graph + * 2) The new dependency is a different node within the update graph + * + * Technically, the graph will only be invalidated in case 2, but the effort to check which + * is true (recursive search for the origin node) is such that it's more effective to simply + * trigger a reflow regardless if we are in this situation, as doing a "pointless" reflow + * will at worst take as much time as checking for if one is needed, meaning that more + * time would be consumed if it ends up being needed. + */ + if(mActiveUpdate.isValid() && newDependency) + mActiveUpdate.reflowIfNeeded(mEvaluationStack.top(), node, originalDepth); +} + +void PropertyCoordinator::incrementUpdateDelay() +{ + Q_ASSERT(mEvaluationStack.empty()); + ++mUpdateDelay; +} + +void PropertyCoordinator::decrementUpdateDelay() +{ + Q_ASSERT(mEvaluationStack.empty()); + Q_ASSERT(mUpdateDelay > 0); + + // Check for when all update groups have been closed, and if an update was prepared in the meanwhile + if(!--mUpdateDelay && mDelayedUpdate.isValid()) + { + // Move update into queue and start processing if not already + mUpdateQueue.push(std::move(mDelayedUpdate)); + if(!mActiveUpdate.isValid()) + processUpdateQueue(); + } +} + +} // namespace Qx + +namespace _QxPrivate +{ + +//=============================================================================================================== +// BindableInterface +//=============================================================================================================== + +/* NOTE: The assertions here that make sure no evaluations are running should stay here, + * or be carefully inspected before moving them, as some nested evaluations can occur + * during a reflow, which would cause a false-positive assert if they were placed within + * PropertyNode's equivalent functions. + */ + +//-Constructor------------------------------------------------------------- +//Protected: +BindableInterface::BindableInterface() : + mNode(std::make_unique(this)) +{} + +BindableInterface::BindableInterface(BindableInterface&& other) { *this = std::move(other); } + +//-Destructor-------------------------------------------------------------------- +//Public: +// Needed for std::unique_ptr to see the implementation of PropertyNode::~PropertyNode() +BindableInterface::~BindableInterface() = default; + +//-Instance Functions------------------------------------------------------------- +//Protected: +void BindableInterface::notifyBindingAdded() +{ + Q_ASSERT_X(!Qx::PropertyCoordinator::instance()->isBindingBeingEvaluated(), + "notifyBindingAdded", + "Do not modify a property's binding within a binding!" + ); + Qx::PropertyCoordinator::instance()->evaluateAndNotify(this); +} + +void BindableInterface::notifyBindingRemoved() +{ + Q_ASSERT_X(!Qx::PropertyCoordinator::instance()->isBindingBeingEvaluated(), + "notifyBindingRemoved", + "Do not remove a property's binding within a binding!" + ); + mNode->disconnectDependencies(); +} + +void BindableInterface::notifyValueChanged() +{ + Q_ASSERT_X(!Qx::PropertyCoordinator::instance()->isBindingBeingEvaluated(), + "notifyValueChanged", + "Do not update a property within a binding!" + ); + Qx::PropertyCoordinator::instance()->notify(this); +} +void BindableInterface::attachToCurrentEval() const { Qx::PropertyCoordinator::instance()->addOrUpdateCurrentEvalDependency(this); } + +//Public: +Qx::PropertyNode* BindableInterface::node() const { return mNode.get(); } + +//-Operators------------------------------------------------------------- +//Protected: +BindableInterface& BindableInterface::operator=(BindableInterface&& other) +{ + mNode = std::exchange(other.mNode, nullptr); + mNode->relinkProperty(this); // Re-link node to new address + return *this; +} + +//=============================================================================================================== +// PropertyObserverManager +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Private: +PropertyObserverManager::PropertyObserverManager() {} + +//-Instance Functions------------------------------------------------------------- +//Public: +void PropertyObserverManager::remove(ObserverId id) { std::erase_if(mObservers, [id](const Observer& o){ return o.id() == id; }); } + +void PropertyObserverManager::invokeAll() const +{ + for(const auto& o : mObservers) + o.invoke(); +} + +//=============================================================================================================== +// ObjectPropertyAdapterManager +//=============================================================================================================== + +//-Instance Functions------------------------------------------------------------- +//Public: +void* ObjectPropertyAdapterRegistry::retrieve(const QObject* obj, const QMetaProperty& property) +{ + Q_ASSERT(obj && property.isValid()); + + auto adaptedObj = mStorage.constFind(obj); + if(adaptedObj == mStorage.cend()) + return nullptr; + + auto pi = property.propertyIndex(); + return adaptedObj->at(pi); +} + +void ObjectPropertyAdapterRegistry::store(const QObject* obj, const QMetaProperty& property, void* adapter) +{ + Q_ASSERT(obj && property.isValid()); + auto pc = obj->metaObject()->propertyCount(); + auto pi = property.propertyIndex(); + Q_ASSERT(pi < pc); + + auto objStore = mStorage.find(obj); + if(objStore == mStorage.end()) + objStore = mStorage.emplace(obj, pc); // Setup storage for this specific object (size list to property count) + + auto& adptrSlot = (*objStore)[pi]; + Q_ASSERT(!adptrSlot); // Should be no adapter yet + adptrSlot = adapter; +} + +void ObjectPropertyAdapterRegistry::remove(const QObject* obj, const QMetaProperty& property) +{ + Q_ASSERT(obj && property.isValid()); + auto pi = property.propertyIndex(); + + /* Do not check the property count of 'obj's meta-object against the index of 'property' + * here because this might be called due to 'obj's 'destroyed' signal which means that + * its v-table has collapsed down to just QObject itself, in which case the property count + * reported will only reflect the properties it has (1) and therefore never match whatever + * it was originally when the adapter was stored. + */ + + auto objStore = mStorage.find(obj); + Q_ASSERT(objStore != mStorage.end()); // Obj should b here + auto& adptrSlot = (*objStore)[pi]; // operator[] will assert that 'pi' is within range of the original property count + Q_ASSERT(adptrSlot); // Should be an adapter here + adptrSlot = nullptr; +} + + +//=============================================================================================================== +// ObjectPropertyAdapterLiaison +//=============================================================================================================== + +//-Instance Functions------------------------------------------------------------- +//Public: +bool ObjectPropertyAdapterLiaison::configure(const QObject* o, QMetaProperty p) +{ + static QMetaMethod thisNotifySlot = [this]{ + auto sig = QMetaObject::normalizedSignature("handleNotify()"); + auto idx = this->metaObject()->indexOfMethod(sig); + auto meth = this->metaObject()->method(idx); + Q_ASSERT(meth.isValid()); + return meth; + }(); + + if(!connect(o, &QObject::destroyed, this, &ObjectPropertyAdapterLiaison::objectDeleted)) + { + qWarning("Qx::ObjectPropertyAdapter: Failed to connect to destroyed signal for QObject bindable."); + return false; + } + + if(!connect(o, p.notifySignal(), this, thisNotifySlot, Qt::DirectConnection)) + { + qWarning("Qx::ObjectPropertyAdapter: Failed to connect to notify signal for QObject bindable."); + return false; + } + + return true; +} + +void ObjectPropertyAdapterLiaison::setIgnoreUpdates(bool ignore) { mIgnoreUpdates = ignore; } + +//-Signals & Slots------------------------------------------------------------- +//Private Slots: +void ObjectPropertyAdapterLiaison::handleNotify() { if(!mIgnoreUpdates) emit propertyNotified(); } + +} // namespace _QxPrivate +/*! @endcond */ + +namespace Qx +{ + +//=============================================================================================================== +// PropertyNotifier +//=============================================================================================================== + +/*! + * @class PropertyNotifier qx/core/qx-property.h + * @ingroup qx-core + * + * @brief The PropertyNotifier class controls the lifecycle of a change callback installed on a Property. + * + * An instance of this class is created when registering a callback on a Property to be notified when + * the property's value changes, as long as a "lifetime" registration method wasn't used. When that instance + * instance is destroyed, the callback is unregistered from the property. + * + * Instances of PropertyNotifier can be transferred between C++ scopes using move semantics. + */ + +//-Constructor------------------------------------------------------------- +//Private: +PropertyNotifier::PropertyNotifier(const ManagerPtr& manager, ObserverId id) : + mManager(manager), + mId(id) +{} + +//Public: +/*! + * Move constructs a PropertyNotifier from @a other. + */ +PropertyNotifier::PropertyNotifier(PropertyNotifier&& other) noexcept { *this = std::move(other); } + +//-Destructor------------------------------------------------------------- +//Public: +/*! + * Destroys the notifier, unregistering the callback associated with it from the property + * the callback was installed on. + */ +PropertyNotifier::~PropertyNotifier() +{ + /* The only thing that kills the observer is either this dying or the property dying, so + * if the manager (and therefore the property) is still alive, the observer is still there + */ + if(ManagerPtr man = mManager.lock()) + man->remove(mId); +} + +//-Operators------------------------------------------------------------- +//Public: +/*! + * Move assigns the PropertyNotifier from @a other. + */ +PropertyNotifier& PropertyNotifier::operator=(PropertyNotifier&& other) noexcept +{ + if(&other != this) + { + mManager = std::exchange(other.mManager, {}); + mId = std::exchange(other.mId, 0); + } + return *this; +} + +//=============================================================================================================== +// PropertyBinding +//=============================================================================================================== + +/*! + * @class PropertyBinding qx/core/qx-property.h + * @ingroup qx-core + * + * @brief The PropertyBinding class acts as a functor for properties with automatic property bindings. + * + * PropertyBinding encapsulates a binding function to be used with properties in order to enable automatic + * updates of that property when its dependencies change. This class is often not used directly, and instead + * a binding is generally created directly within a property via Property::setBinding(), though it is possible + * to pre-create bindings, as well as move them between properties. + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn PropertyBinding::PropertyBinding() + * + * Constructs a null property binding. + */ + +/*! + * @fn PropertyBinding::PropertyBinding(Functor&& f) + * + * Constructs a property binding from functor @a f. + */ + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn bool PropertyBinding::isNull() const + * + * Returns @c true if the binding is null; otherwise, returns @c false. + * + * A property binding is null if it contains no callable function target (i.e. is default constructed). + * + * @sa operator bool(). + */ + + +//-Operators----------------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn explicit PropertyBinding::operator bool() const + * + * Same as isNull(). + */ + +/*! + * @fn T PropertyBinding::operator()() const + * + * Invokes the contained callable function target, if present. + */ + +//=============================================================================================================== +// AbstractBindableProperty +//=============================================================================================================== + +/*! + * @class AbstractBindableProperty qx/core/qx-property.h + * @ingroup qx-core + * + * @brief The AbstractBindableProperty class provides the baseline feature for bindable properties + * of the @ref properties "Qx Bindable Properties System". + * + * AbstractBindableProperty is the standard interface shared by all bindable properties and contains + * most of the functionality that any given implementation will provide to user code; that is, most + * individual implementations are minimal annex that simply dictate how the underlying data of the + * property is read/written, and the bulk of the Bindable Properties System is contained within this + * base class. + * + * You can assign a value to properties and you can read them via value(), operator*(), or operator const T&(). + * You can also tie a property to an expression that computes the value dynamically, called a + * "binding expression". The binding expression can be any C++ functor, though most often a lambda, and + * can be used to express relationships between different properties in your application. + * + * @sa Property. + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn AbstractBindableProperty::AbstractBindableProperty() + * + * Constructs an AbstractBindableProperty. + */ + +/*! + * @fn AbstractBindableProperty::AbstractBindableProperty(AbstractBindableProperty&& other) noexecpt + * + * Move-constructs an AbstractBindableProperty using @a other. + */ + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn void AbstractBindableProperty::setValueBypassingBindings(const T& v) = 0 + * + * Directly sets the value of the property to @a v. + * + * This is generally only used by a derived class to control how the underlying data is written + * when a copy of T is provided, but there are some cases (like maintaining class invariants) + * where it is useful externally. + * + * @note Using this method will bypass any potential binding registered for this property. + */ + +/*! + * @fn void AbstractBindableProperty::setValueBypassingBindings(T&& v) = 0 + * + * @overload + * + * Directly sets the value of this property to @a v. + * + * This is generally only used by a derived class to control how the underlying data is written + * when an rvalue of T is provided, but there are some cases (like maintaining class invariants) + * where it is useful externally. + * + * @note Using this method will bypass any potential binding registered for this property. + */ + +/*! + * @fn const T& AbstractBindableProperty::valueBypassingBindings() const = 0 + * + * Returns the direct value of the property. + * + * This is generally only used by a derived class to control how the underlying data is read + * when requested, but there are some cases where it is useful externally. + * + * @note Using this method will not register the property access with any currently executing binding. + */ + +/*! + * @fn PropertyBinding AbstractBindableProperty::binding() const + * + * Returns the binding expression that is associated with this property. A default constructed PropertyBinding + * will be returned if no such association exists. + */ + +/*! + * @fn PropertyBinding AbstractBindableProperty::takeBinding() + * + * Disassociates the binding expression from this property and returns it. After calling this function, the value + * of the property will only change if you assign a new value to it, or when a new binding is set. + * + * @sa removeBinding() and setBinding(). + */ + +/*! + * @fn void AbstractBindableProperty::removeBinding() + * + * Disassociates the binding expression from this property. After calling this function, the value + * of the property will only change if you assign a new value to it, or when a new binding is set. + * + * @sa takeBinding() and setBinding(). + */ + +/*! + * @fn PropertyBinding AbstractBindableProperty::setBinding(Functor&& f) + * + * Associates the value of this property with the provided functor @a f and returns the previously associated + * binding. The property's value is set to the result of evaluating the new binding. Whenever a dependency of + * the binding changes, the binding will be re-evaluated, and the property's value gets updated accordingly. + */ + +/*! + * @fn PropertyBinding AbstractBindableProperty::setBinding(const PropertyBinding& binding) + * + * @overload + * + * Associates the value of this property with the provided @a binding expression and returns the previously + * associated binding. The property's value is set to the result of evaluating the new binding. Whenever a + * dependency of the binding changes, the binding will be re-evaluated, and the property's value gets updated + * accordingly. + */ + +/*! + * @fn bool AbstractBindableProperty::hasBinding() const + * + * Returns @c true if the property has a binding associated with it; otherwise, returns @a false. + */ + +/*! + * @fn const T& AbstractBindableProperty::value() const + * + * Returns the value of the property. This may evaluate a binding expression that is tied to this property, + * before returning the value. + * + * @sa value(). + */ + +/*! + * @fn void AbstractBindableProperty::setValue(const T& newValue) + * + * Assigns @a newValue to this property and removes the property's associated binding, if present. + * + * @sa binding() and beginPropertyUpdateGroup(). + */ + +/*! + * @fn void AbstractBindableProperty::setValue(T&& newValue) + * + * @overload + */ + +/*! + * @fn PropertyNotifier AbstractBindableProperty::addNotifier(Functor&& f) + * + * Subscribes the given functor @a f as a callback that is called whenever the value of the property changes. + * + * The callback @a f is expected to be a type that has a plain call @c operator() without any parameters. + * This means that you can provide a C++ lambda expression, a std::function or even a custom struct with a call + * operator. + * + * The returned property change handler object keeps track of the subscription. When it goes out of scope, + * the callback is unsubscribed. + * + * @sa addLifetimeNotifier() and subscribe(). + */ + +/*! + * @fn void AbstractBindableProperty::addLifetimeNotifier(Functor&& f) + * + * Same as addNotifier(), but the lifetime of the subscription is tied to the lifetime of the property so + * no change handler object is returned. + * + * @warning Be sure that any data referenced in @a f lives as long as the property itself. + * + * @sa addNotifier() and subscribeLifetime(). + */ + +/*! + * @fn PropertyNotifier AbstractBindableProperty::subscribe(Functor&& f) + * + * Same as invoking f (e.g. `f()`), followed by `addNotifier(f)`. + * + * That is, the callback functor is called immediately before it's registered. + * + * @sa subscribeLifetime() and addNotifier(). + */ + +/*! + * @fn void AbstractBindableProperty::subscribeLifetime(Functor&& f) + * + * Same as invoking f (e.g. `f()`), followed by `addLifetimeNotifier(f)`. + * + * That is, the callback functor is called immediately before it's registered. + * + * @sa subscribe() and addLifetimeNotifier(). + */ + +//-Operators------------------------------------------------------------- +//Protected: +/*! + * @fn AbstractBindableProperty& AbstractBindableProperty::operator=(AbstractBindableProperty&& other) noexecpt + * + * Move-assigns an AbstractBindableProperty using @a other. + */ + +//Public: +/*! + * @fn const T* AbstractBindableProperty::operator->() const + * + * Returns a pointer to the underlying property data (bindings are still respected). + * + * @sa value(). + */ + +/*! + * @fn const T& AbstractBindableProperty::operator*() const + * + * Same as value(). + */ + +/*! + * @fn AbstractBindableProperty::operator const T&() const + * + * Type-conversion operator for a const reference to the underlying type. + */ + +//=============================================================================================================== +// Bindable +//=============================================================================================================== + +/*! + * @class Bindable qx/core/qx-property.h + * @ingroup qx-core + * + * @brief Bindable is a wrapper class around binding-enabled properties that provides uniform access, + * regardless of the specific type. + * + * Bindable acts as a convenience type for sharing access to any concrete bindable property without the need + * to use interface pointers directly, via a thin, cheap to copy wrapper. + * + * The methods of this class essentially mirror those of AbstractBindableProperty and an instance of this class + * can be used in an identical fashion as the property it shadows. + * + * Additionally, the constructors that take a QObject pointer can be used to wrap a property of a classes based + * on it (i.e. those created using Q_PROPERTY()), which are not already bindable. + * + * Due to the abstraction that this class provides, instances may be "read-only", meaning that any attempt to + * use a non-const method will fail and a warning will be emitted. The contexts in which this is the case + * are noted in the documentation for the constructors of this class. Use isReadOnly() to check if a particular + * instance cannot mutate the property it wraps. + * + * @note + * @parblock + * Since the Qx Bindable Properties System does not directly interact with the native Qt equivalent, the + * aforementioned constructor of QBindable will need to be used to bind to the property of any any QObject + * derived typed that does not have accessor methods to underlying Qx bindable properties (e.g. Qx::Property), + * even if the property in question is already bindable using Qt's system. + * + * The best way to create a QObject-based class that natively uses Qx properties is to add instances of + * Property as class members and provide an accessor method to them that returns a Qx::Bindable, like so: + * @code{.cpp} + * class MyObject : public QObject + * { + * Q_OBJECT + * Qx::Property data; + * public: + * Qx::Bindable bindableData() { return data; } + * }; + * @endcode + * @endparblock + * + * @sa Property and @ref properties "Qx Bindable Properties System" + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn Bindable::Bindable() + * + * Constructs an invalid Bindable. + */ + +/*! + * @fn Bindable::Bindable(AbstractBindableProperty& bp) + * + * Constructs a Bindable wrapper for the bindable property @a bp. + */ + +/*! + * @fn Bindable::Bindable(const AbstractBindableProperty& bp) + * + * Constructs a Bindable wrapper for the bindable property @a bp. + * + * @note The Bindable will be read-only since @a bp is const. + */ + +/*! + * @fn Bindable::Bindable(QObject* obj, const QMetaProperty& property) + * + * Constructs a Bindable wrapper for property @a property of @a obj. + * + * The property must have a notify signal, and you must access the property through + * the created Bindable (e.g. via value(), etc.) instead of the normal property READ + * function (or MEMBER) to enable dependency tracking. + * + * When binding using a lambda, you may prefer to capture the QBindable by value to + * avoid the cost of calling this constructor in the binding expression. + * + * @note The Bindable will be read-only if @a property is itself read-only. + */ + +/*! + * @fn Bindable::Bindable(QObject* obj, const char* property) + * + * Constructs a Bindable wrapper for the property named @a property of @a obj. + * + * See Bindable(QObject*, const QMetaProperty&). + * + * @note The Bindable will be read-only if @a property is read-only itself. + */ + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn void Bindable::setValueBypassingBindings(const T& v) + * + * @copydoc AbstractBindableProperty::setValueBypassingBindings(const T& v) + */ + +/*! + * @fn void Bindable::setValueBypassingBindings(T&& v) + * + * @copydoc AbstractBindableProperty::setValueBypassingBindings(T&& v) + */ + +/*! + * @fn const T& Bindable::valueBypassingBindings() const + * + * @copydoc AbstractBindableProperty::valueBypassingBindings() const + */ + +/*! + * @fn PropertyBinding Bindable::binding() const + * + * @copydoc AbstractBindableProperty::binding() const + */ + +/*! + * @fn PropertyBinding Bindable::takeBinding() + * + * @copydoc AbstractBindableProperty::takeBinding() + */ + +/*! + * @fn void Bindable::removeBinding() + * + * @copydoc AbstractBindableProperty::removeBinding() + */ + +/*! + * @fn PropertyBinding Bindable::setBinding(Functor&& f) + * + * @copydoc AbstractBindableProperty::setBinding(Functor&& f) + */ + +/*! + * @fn PropertyBinding Bindable::setBinding(const PropertyBinding& binding) + * + * @copydoc AbstractBindableProperty::setBinding(const PropertyBinding& binding) + */ + +/*! + * @fn bool Bindable::hasBinding() const + * + * @copydoc AbstractBindableProperty::hasBinding() const + */ + +/*! + * @fn const T& Bindable::value() const + * + * @copydoc AbstractBindableProperty::value() const + */ + +/*! + * @fn void Bindable::setValue(const T& newValue) + * + * @copydoc AbstractBindableProperty::setValue(const T& newValue) + */ + +/*! + * @fn void Bindable::setValue(T&& newValue) + * + * @copydoc AbstractBindableProperty::setValue(T&& newValue) + */ + +/*! + * @fn PropertyNotifier Bindable::addNotifier(Functor&& f) const + * + * @copydoc AbstractBindableProperty::addNotifier(Functor&& f) const + */ + +/*! + * @fn void Bindable::addLifetimeNotifier(Functor&& f) const + * + * @copydoc AbstractBindableProperty::addLifetimeNotifier(Functor&& f) const + */ + +/*! + * @fn PropertyNotifier Bindable::subscribe(Functor&& f) const + * + * @copydoc AbstractBindableProperty::subscribe(Functor&& f) const + */ + +/*! + * @fn void Bindable::subscribeLifetime(Functor&& f) const + * + * @copydoc AbstractBindableProperty::subscribeLifetime(Functor&& f) const + */ + +/*! + * @fn bool Bindable::isValid() const + * + * Returns @c true if the Bindable is valid; otherwise, returns @c false. + * + * A binding is invalid if there was an issue the arguments passed to its constructor, + * like a null pointer or a property being specified that does not actually belong + * to the preceding object, or if the default constructor was used. + */ + +/*! + * @fn bool Bindable::isReadOnly() const + * + * Returns @c true if the Bindable wraps a read-only property; otherwise, returns @c false. + * + * A Bindable may be read-only depending on how it was constructed. + * + * @sa Bindable(QObject* obj, const QMetaProperty&) and Bindable(const AbstractBindableProperty&) + */ + +//-Operators----------------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn const T* Bindable::operator->() const + * + * @copydoc AbstractBindableProperty::operator->() const + */ + +/*! + * @fn const T& Bindable::operator*() const + * + * @copydoc AbstractBindableProperty::operator*() const + */ + +// copydoc not working here for some reason +/*! + * @fn Bindable::operator const T&() const + * + * Type-conversion operator for a const reference to the underlying type. + */ + +/*! + * @fn Bindable& Bindable::operator=(T&& newValue) noexcept + * + * Assigns @a newValue to the property and returns a reference to this bindable. + */ + +/*! + * @fn Bindable& Bindable::operator=(const T& newValue) noexcept + * + * @overload + */ + +//=============================================================================================================== +// Property +//=============================================================================================================== + +/*! + * @class Property qx/core/qx-property.h + * @ingroup qx-core + * + * @brief The Property class is a template class that enables automatic property bindings + * + * Property is the principal implementation of the @ref properties "Qx Bindable Properties System". It is a container + * that holds an instance of T. + * + * It can be used in all of the ways described in AbstractBindableProperty to build a web of dynamic properties. + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn Property::Property() + * + * Constructs a property with a default constructed instance of T. + */ + +/*! + * @fn Property::Property(Property&& other) + * + * Move-constructs a property from @a other. + */ + +/*! + * @fn Property::Property(Functor&& f) + * + * Constructs a property that is tied to the provided binding expression @a f. The binding is immediately evaluated + * to establish the initial value of the property. Whenever a dependency of the binding changes, the binding will + * be re-evaluated, and the property's value will be updated accordingly. + */ + +/*! + * @fn Property::Property(const PropertyBinding& binding) + * + * Constructs a property that is tied to the provided @a binding expression. The binding is immediately evaluated + * to establish the initial value of the property. Whenever a dependency of the binding changes, the binding will + * be re-evaluated, and the property's value will be updated accordingly. + */ + +/*! + * @fn Property::Property(T&& initialValue) + * + * Move-constructs a property with the provided @a initialValue. + */ + +/*! + * @fn Property::Property(const T& initialValue) + * + * Constructs a property with the provided @a initialValue. + */ + +//-Operators----------------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn Property& Property::operator=(Property&& other) noexcept + * + * Move assigns @a other to this. + */ + +/*! + * @fn Property& Property::operator=(T&& newValue) noexcept + * + * Assigns @a newValue to this property and returns a reference to this property. + */ + +/*! + * @fn Property& Property::operator=(const T& newValue) noexcept + * + * @overload + */ + +//=============================================================================================================== +// namespace functions +//=============================================================================================================== + +/*! + * Marks the beginning of a property update group. Inside this group, changing a property does neither + * immediately update any dependent properties nor does it trigger change notifications. Those are instead + * deferred until the group is ended by a call to endPropertyUpdateGroup. + * + * Groups can be nested. In that case, the deferral ends only after the outermost group has been ended. + * + * @note Change notifications are only send after all property values affected by the group have been updated to + * their new values. This allows re-establishing a class invariant if multiple properties need to be updated, + * preventing any external observer from noticing an inconsistent state. + * + * @sa Qt::endPropertyUpdateGroup and ScopedPropertyUpdateGroup. + */ +void beginPropertyUpdateGroup() { PropertyCoordinator::instance()->incrementUpdateDelay(); } + +/*! + * Ends a property update group. If the outermost group has been ended, any deferred binding evaluations and + * subsequent notifications are triggered. + * + * @warning Calling endPropertyUpdateGroup without a preceding call to beginPropertyUpdateGroup will result in + * the application aborting. + */ +void endPropertyUpdateGroup() { PropertyCoordinator::instance()->decrementUpdateDelay(); } + +//=============================================================================================================== +// ScopedPropertyUpdateGroup +//=============================================================================================================== + +/*! + * @class ScopedPropertyUpdateGroup qx/core/qx-property.h + * @ingroup qx-core + * + * @brief The ScopedPropertyUpdateGroup class starts an update group when constructed and ends it when destroyed. + * + * This class calls Qt::beginPropertyUpdateGroup() in its constructor and Qt::endPropertyUpdateGroup() in its + * destructor, making sure the latter function is reliably called even in the presence of early returns or + * thrown exceptions. + * + * Note: Qx::endPropertyUpdateGroup() may re-throw exceptions thrown by binding evaluations. This means your + * application may crash (std::terminate() called) if another exception is causing ScopedPropertyUpdateGroup's + * destructor to be called during stack unwinding. If you expect exceptions from binding evaluations, use + * manual Qx::endPropertyUpdateGroup() calls and try/catch blocks. + * + * @sa Property. + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn ScopedPropertyUpdateGroup::ScopedPropertyUpdateGroup() + * + * Calls Qx::beginPropertyUpdateGroup(). + */ + +//-Destructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn ScopedPropertyUpdateGroup::~ScopedPropertyUpdateGroup() + * + * Calls Qx::endPropertyUpdateGroup(). + */ + +} diff --git a/lib/core/src/qx-property_p.h b/lib/core/src/qx-property_p.h new file mode 100644 index 00000000..448ccd0a --- /dev/null +++ b/lib/core/src/qx-property_p.h @@ -0,0 +1,286 @@ +#ifndef QX_PROPERTY_P_H +#define QX_PROPERTY_P_H + +// Standard Library Includes +#include +#include +#include + +// Qt Includes +#include +#include + +// Intra-component Includes +#include "qx/core/qx-flatmultiset.h" + +/* NOTE: DO NOT STORE POINTERS TO BINDABLEINTERFACE INSTANCES AS THEY CAN BE INVALIDATED. + * INSTEAD, IF NEED, STORE A POINTER TO ITS NODE AND THEN GET THE PROPERTY THROUGH + * THAT THE MOMENT ITS REQUIRED. + * + * The only place that can store such a pointer is PropertyNode itself. + */ + +/*! @cond */ +namespace _QxPrivate { class BindableInterface; } + +namespace Qx +{ + +class PropertyNode; + +struct DepthLink +{ + using Depth = int; + PropertyNode* node; + Depth stableDepth; + + inline bool operator<(const DepthLink& other) const { return this->stableDepth > other.stableDepth; } + inline bool operator<(Depth depth) const { return this->stableDepth > depth; } + inline bool operator==(const PropertyNode* node) const { return this->node == node; } +}; + +class DepthSortedLinks : private FlatMultiSet +{ + /* This container acts somewhat like Lopmap. It handles nodes in a unique fashion, + * just using the underlying container to allow for multiple nodes with the + * same depth. That is, the same node pointer is only allowed once. + * + * This used to store the node pointers directly, with the depth of a node essentially + * being stored implicitly (i.e. depth() would be 0 if empty, or chain to the first + * dependent which would do the same check until the top level node was reached). As + * cool as this was, it made it a hassle to keep proper sort order because the old + * depth of a node had to be communicated through the function calls to update the + * dependents themselves since depths were essentially updated instantly, and it also + * was likely a bit slower since every depth check was basically the same as traversing + * a linked-list (though this would have been a neet way to always have cycle detection built-in). + * + * So, we store the current depth of the node as part of the link, which allows + * checking the depth associated with a link instantly without a chain, and also + * allows the value to become stale so that when an update insertion happens + * we can easily tell that the value was changed and remove/re-insert to re-sort. + * + * This could mostly be replaced if we just made FlatLopmap, but for now + * this is fine and slightly more efficient due to only using one underlying + * container, while that presumably would use two. + */ + +//-Base Class Forwards----------------------------------------------------------- +public: + using FlatMultiSet::const_iterator; + using FlatMultiSet::isEmpty; + using FlatMultiSet::cbegin; + using FlatMultiSet::cend; + using FlatMultiSet::first; + using FlatMultiSet::erase; + +//-Instance Functions------------------------------------------------------------- +public: + bool remove(const PropertyNode* node); + const_iterator insert(PropertyNode* node); +}; + +class PropertyNode +{ + Q_DISABLE_COPY_MOVE(PropertyNode); +//-Aliases------------------------------------------------------------------------ +public: + using IFace = _QxPrivate::BindableInterface; + using Depth = DepthLink::Depth; + using Links = QList; // Using list for iteration speed TODO: Possible candidate for std::flat_set for when using C++23 + using Itr = DepthSortedLinks::const_iterator; + +//-Instance Variables------------------------------------------------------------- +private: + DepthSortedLinks mDependents; + Links mDependencies; + IFace* mProperty; + +//-Constructor-------------------------------------------------------------------- +public: + PropertyNode(IFace* property); + +//-Destructor-------------------------------------------------------------------- +public: + ~PropertyNode(); + +//-Instance Functions------------------------------------------------------------- +private: + template + void depthAlteringOperation(Operation o); + bool recursiveNodeSearch(const PropertyNode* searchNode, const PropertyNode* target); + void checkForCycle(const PropertyNode* newDependency); + void addOrUpdateDependent(PropertyNode* dependent); + void removeDependency(const PropertyNode* dependency); + void removeDependent(const PropertyNode* dependent); + +public: + IFace* property() const; + Depth depth() const; + Itr cbeginDependents() const; + Itr cendDependents() const; + Links dependencies() const; + + void relinkProperty(IFace* property); // For moves + bool addDependency(PropertyNode* dependency); + void disconnectDependents(); + void disconnectDependencies(); +}; + +class PropertyDependentWalker +{ +//-Instance Variables------------------------------------------------------------- +private: + PropertyNode* mNode; + PropertyNode::Itr mDepItr; + PropertyNode::Itr mDepEnd; + +//-Constructor-------------------------------------------------------------------- +public: + PropertyDependentWalker(PropertyNode* node); + +//-Instance Functions------------------------------------------------------------- +public: + const PropertyNode* node() const; + PropertyNode::Depth depth() const; + bool isExhausted() const; + void refresh(PropertyNode::Depth depth); + + std::optional fork(PropertyNode::Depth targetDepth, QSet& ignore); + bool evaluate(); +}; + +class PropertyWalkerManager +{ +//-Inner Classes------------------------------------------------------------------ +public: + class Iterator; + +//-Aliases------------------------------------------------------------------------ +public: + using Container = QVarLengthArray; + +//-Instance Variables------------------------------------------------------------- +private: + Container mWalkers; + +//-Constructor-------------------------------------------------------------------- +public: + PropertyWalkerManager(const QList& origins); + +//-Instance Functions------------------------------------------------------------- +public: + bool isEmpty() const; + Iterator staticIterator(); + void addWalker(PropertyDependentWalker&& walker); + void refreshWalkers(PropertyNode::Depth targetDepth); +}; + +class PropertyWalkerManager::Iterator +{ + friend class PropertyWalkerManager; +//-Aliases------------------------------------------------------------- +private: + using Container = PropertyWalkerManager::Container; + +//-Instance Variables------------------------------------------------------------- +private: + Container& mContainer; + Container::size_type mIdx; + Container::size_type mEndIdx; + +//-Constructor-------------------------------------------------------------------- +private: + Iterator(PropertyWalkerManager::Container& c); + +//-Operators---------------------------------------------------------------------- +public: + explicit operator bool() const; + PropertyDependentWalker& operator*(); + PropertyDependentWalker* operator->(); + Iterator& operator++(); +}; + +class PropertyUpdateWave +{ + Q_DISABLE_COPY(PropertyUpdateWave); +//-Instance Variables------------------------------------------------------------- +private: + QList mOrigins; + QList mChangedNodes; + QSet mDeadends; + std::stack mReflowStack; + PropertyNode::Depth mGlobalDepth; + bool mWalkersStale = false; + + //-Constructor-------------------------------------------------------------------- +public: + PropertyUpdateWave(); + PropertyUpdateWave(PropertyNode* initiator); + PropertyUpdateWave(PropertyUpdateWave&& other) = default; + +//-Instance Functions------------------------------------------------------------- +private: + bool reflowFinished(const PropertyNode* nodeJustEvaluated); + void postReflowRefresh(PropertyWalkerManager& walkerManager, PropertyNode::Depth d); + void notifyObservers(); + +public: + const QList& origins() const; + bool isValid() const; + + void addInitiator(PropertyNode* initiator); + void flow(); + void reflowIfNeeded(const PropertyNode* evaluating, const PropertyNode* dep, PropertyNode::Depth depOrigDepth); + +//-Operators--------------------------------------------------------------------- +public: + PropertyUpdateWave& operator=(PropertyUpdateWave&& other) = default; +}; + +class PropertyCoordinator +{ + Q_DISABLE_COPY_MOVE(PropertyCoordinator); +//-Aliases------------------------------------------------------------------------ +private: + using IFace = _QxPrivate::BindableInterface; + using EvaluationStack = std::stack; + using UpdateStack = QVarLengthArray; // Could be QSet, but given the likely small element counts this is likely faster + using UpdateQueue = std::queue; + +//-Instance Variables------------------------------------------------------------- +private: + EvaluationStack mEvaluationStack; // Almost always 0 or 1 items, but more in the case of a reflow + PropertyUpdateWave mActiveUpdate; + PropertyUpdateWave mDelayedUpdate; + UpdateStack mChainedUpdates; + UpdateQueue mUpdateQueue; + int mUpdateDelay; + +//-Constructor-------------------------------------------------------------------- +private: + PropertyCoordinator(); + +//-Class Functions---------------------------------------------------------------- +public: + static PropertyCoordinator* instance(); + +//-Instance Functions------------------------------------------------------------- +private: + void checkForCycle(const PropertyNode* notifyingProperty) const; + void queueUpdateWave(PropertyNode* origin); + void processUpdateQueue(); + +public: + bool isBindingBeingEvaluated() const; + bool evaluate(IFace* property); + void evaluateAndNotify(IFace* property); + void notify(IFace* property); + void addOrUpdateCurrentEvalDependency(const IFace* property); + void incrementUpdateDelay(); + void decrementUpdateDelay(); +}; +/*! @endcond */ + +} + +#endif // QX_PROPERTY_P_H diff --git a/lib/core/src/qx-setonce.dox b/lib/core/src/qx-setonce.dox index 45843e40..3b84afd7 100644 --- a/lib/core/src/qx-setonce.dox +++ b/lib/core/src/qx-setonce.dox @@ -5,48 +5,49 @@ namespace Qx //=============================================================================================================== /*! - * @class SetOnce qx/core/qx-setonce.h + * @class SetOnce qx/core/qx-setonce.h * @ingroup qx-core * * @brief The SetOnce template class acts as a container for a value that can only be set once. * - * The optional @a CompareEq template parameter can be used to provide a custom compare-equal function - * object type. + * The optional @a C template parameter (defaults to @c void) can be used to provide a comparator + * (such as std::equal_to), which is then used to reject assignment if the passed value is the same + * as the containers default. + * + * @sa operator=(). */ //-Constructor---------------------------------------------------------------------------------------------- //Public: /*! - * @fn SetOnce::SetOnce(T initial, const CompareEq& comp) + * @fn SetOnce::SetOnce(T initial, C&& comp = C()) * * Creates a SetOnce container that holds the initial value @a initial. * * The container is initially unset and only holds this value until it is set. * - * Optionally, a custom compare-equal function can be provided through @a comp, which - * is used to determine whether or not an assigned value is different from the - * container's initial value. + * When C is not @c void, @a comp is used to compare the input value to the container's + * default value when performing an assignment. * * @sa operator=(const T& value). */ - //-Instance Functions---------------------------------------------------------------------------------------------- //Public: /*! - * @fn SetOnce::isSet() const + * @fn SetOnce::isSet() const * * Returns @c true if the containers value has been set; otherwise returns @c false. */ /*! - * @fn const T& SetOnce::value() const + * @fn const T& SetOnce::value() const * * Returns the current value of the container. */ /*! - * @fn void SetOnce::reset() + * @fn void SetOnce::reset() * * Resets the container to its initial state. * @@ -56,12 +57,41 @@ namespace Qx */ /*! - * @fn SetOnce& SetOnce::operator=(const T& value) + * @fn SetOnce& SetOnce::operator=(const T& value) * - * Sets the value of the container to @a value, if it is different from its initial value. + * Sets the value of the container to @a value. If C is not @c void and @a value is + * the same as the container's default value, the container is not considered to + * be set. + * + * To clarify: + * - C = void: Any assignment causes the container to be considered set. + * - C satisfies @ref Qx::comparator : Only assignment of a value other than the default + * causes the container to be considered set. * * Once the containers value has been set it cannot be changed again until it is reset. * * @sa reset(). */ + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn const T& SetOnce::operator*() const + * + * Same as value(). + */ + +/*! + * @fn const T* SetOnce::operator->() const + * + * Allows access to members of T for the value of the container. + */ + +/*! + * @fn SetOnce::operator bool() const + * + * Produces the boolean value @c true if the container is set; otherwise, produces @c false. + */ + + } diff --git a/lib/core/src/qx-string.cpp b/lib/core/src/qx-string.cpp index 413a5fa6..da2227a4 100644 --- a/lib/core/src/qx-string.cpp +++ b/lib/core/src/qx-string.cpp @@ -3,10 +3,166 @@ // Qt Includes #include +#include // Intra-component Includes #include "qx/core/qx-regularexpression.h" -#include "qx/core/qx-algorithm.h" +#include "qx/utility/qx-helpers.h" + +namespace +{ +// IMPLEMENTATION DETAILS FOR MAPARG +inline bool charsAreSame(QChar a, QChar b, Qt::CaseSensitivity cs) +{ + // Equalize case if case-insensitive + if(cs == Qt::CaseInsensitive) + { + a = a.toCaseFolded(); + b = b.toCaseFolded(); + } + + return a == b; +} + +/* 32 is the same number of expected sections that QString::arg() uses. + * + * We have to use QAnyStringView here because the pieces can either be a view + * of a QString in the replacement map, or a part of the original string which + * can be of any string type. + */ +using ViewList = QVarLengthArray; +class StringBlueprint +{ + ViewList mViews; + qsizetype mLength = 0; + +public: + StringBlueprint() = default; + void push_back(QAnyStringView&& v) { mViews.push_back(v); mLength += v.length(); } + + qsizetype length() const { return mLength; } + const ViewList& views() const { return mViews; } + +}; + +using RepItr = QMap::const_iterator; +struct Match +{ + qsizetype originalLength = 0; + QStringView replaceView; + + explicit operator bool() const { return originalLength; } +}; + +// Could use reverse iterator for this, except it would be tricky since we update the regular iterator +std::optional squeeze(QChar ch, qsizetype rIdx, RepItr& b, RepItr& e, Qt::CaseSensitivity cs, bool reverse) +{ + /* We always manually break out here when the iterators are equal, so we don't want + * to stop the loop when the "end" is reached. + */ + forever + { + // Shortcut when front squeeze already handled this itr + if(reverse && b == e) + break; + + if(!b.key().isEmpty() && charsAreSame(b.key()[rIdx], ch, cs)) + { + qsizetype repLen = rIdx + 1; + if(b.key().length() == repLen) + return Match{repLen, b.value()}; // Match + else + break; + } + else if(b == e) + return Match{}; // Out of potentials, end search for this start char + else + reverse ? --b : ++b; // Squeeze + } + + // Go to next char + return std::nullopt; +} + +Match checkForMatch(auto chars, auto limit, qsizetype startIdx, RepItr chkBegin, RepItr chkEnd, Qt::CaseSensitivity cs) +{ + /* This looks for a contiguous range of map keys that might match a word starting at the current character. + * Since a string map is ordered lexicographically, potential matches will always be contiguous. We take + * advantage of this to search for a match in essentially the same matter as a Trie. + */ + + // Iterate forward from passed in str index to look for match + for(qsizetype i = startIdx, repIdx = 0; i < limit; ++i, ++repIdx) + { + // Squeeze from front + if(auto m = squeeze(chars[i], repIdx, chkBegin, chkEnd, cs, false)) + return *m; + + // Squeeze from back + if(auto m = squeeze(chars[i], repIdx, chkEnd, chkBegin, cs, true)) + return *m; + } + + // End of source string reached + return Match{}; +} + +template +StringBlueprint buildViewList(StringView s, const QMap& rm, Qt::CaseSensitivity cs) +{ + /* A version of this could made that searches for a match without the final sub-loop + * that temporarily advances forward in the string by starting searches as a struct + * and putting them into a list. Then, on each index we handle the existing searches + * before starting new ones, and if a search ends up matching, we delay until any + * searches with a lower index are finished (since they came first) and then + * note the match; however, this would require another list which means there might + * be heap allocations, plus there would have to be extra processing to invalidate + * in-progress searches that overlap with a match when one is found, so it might not + * actually be faster. + * + * Also when checking each map key, it could make sense to use a binary search since + * the items are presorted, but that does have some overhead such that it doesn't really + * matter unless you have a sufficient number of elements in it. I've seen 30-40 quoted, + * though of course this varies depending on hardware, but going off that rough rule of thumb + * I doubt this function would be used very often with that many replacements, so we + * stick with a linear search. We could considering searching linearly from the top then bottom + * to get a lower and upper bound instead. + */ + StringBlueprint resultBp; + + const auto chars = s.data(); + const auto length = s.size(); + qsizetype segStart = 0; + + // Iterate over entire source string + qsizetype i = 0; + while(i < length) + { + /* Check for match starting at current index. We pass an iterator to the actual last + * idx (not end + 1) because we work on both iterators at once and need them to be + * at each "end" (first/last) of the map) + */ + if(Match match = checkForMatch(chars, length, i, rm.cbegin(), std::prev(rm.cend()), cs)) + { + if(i != segStart) + resultBp.push_back(s.sliced(segStart, i - segStart)); // Preceding raw characters + resultBp.push_back(match.replaceView); // Subbed Characters + i += match.originalLength; // Skip replaced str + segStart = i; // Note next segment start + } + else + ++i; + } + + // Check for trailing text + if(segStart < length) + resultBp.push_back(s.sliced(segStart, length - segStart)); + + return resultBp; +} + +// END +} namespace Qx { @@ -144,4 +300,83 @@ QString String::trimTrailing(const QStringView string) return QStringView(begin, newEnd).toString(); } +/*! + * Returns a copy of @a s with all occurances of each key in @a args replaced with their corresponding + * value, using the case sensitivity setting @a cs. + * + * Similar to the templated version of QString::arg(), this function checks for all replacements in one + * pass and only performs one heap allocation for the final string, so it should perform much better + * than chaining QString::replace() multiple times, especially when there is a significant number + * of replacements. + * + * @code{.cpp} + * // Instead of: + * formatStr.replace(u"phA"_s, u"argA"_s) + * .replace(u"phB"_s, u"argB"_s) + * .replace(u"phC"_s, u"argC"_s) + * ... + * + * // Try + * QString formated = Qx::mapArg(formatStr, { + * {u"phA"_s, u"argA"_s}, + {u"phB"_s, u"argB"_s}, + {u"phC"_s, u"argC"_s}, + ... + * }); + * @endcode + */ +QString String::mapArg(QAnyStringView s, const QMap& args, Qt::CaseSensitivity cs) +{ + if(s.isEmpty() || args.isEmpty()) + return QString(); + + // Create blueprint for the final string + StringBlueprint resultBp = s.visit([&](auto s) { return buildViewList(s, args, cs); }); + + // Form final string from blueprint + QString result(resultBp.length(), Qt::Uninitialized); + + /* Some of this is based on Qt's implementation of QString::arg(). Idk why + * they do this instead of just using data(), but who am I to argue with them. + * I can only assume it has something to due with the fact that data is always + * null terminated but constData may not be. + */ + auto resultRaw = const_cast(result.constData()); + + for(const QAnyStringView& view : resultBp.views()) + { + // QString::arg() uses size() here instead of isEmpty(), so we keep consistent + resultRaw = view.visit(qxFuncAggregate{ + [resultRaw](QLatin1StringView v){ + if(v.size()) + { + auto fromLatin1 = QStringDecoder(QStringDecoder::Latin1, QStringDecoder::Flag::Stateless); + auto postAppend = fromLatin1.appendToBuffer(resultRaw, v); + Q_ASSERT(!fromLatin1.hasError()); + return postAppend; + } + return resultRaw + v.size(); + }, + [resultRaw](QUtf8StringView v){ + auto fromUtf8 = QStringDecoder(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless); + auto postAppend = fromUtf8.appendToBuffer(resultRaw, v); + Q_ASSERT(!fromUtf8.hasError()); + return postAppend; + }, + [resultRaw](QStringView v){ + if(v.size()) + memcpy(resultRaw, v.data(), v.size() * sizeof(QChar)); + return resultRaw + v.size(); + } + }); + } + + /* According to QString::arg(), in the case of UTF-8 decoding, the size of the converted + * data might actually be smaller than the string container, so correct for that + */ + result.truncate(resultRaw - result.cbegin()); + + return result; +} + } diff --git a/lib/core/src/qx-system_win.cpp b/lib/core/src/qx-system_win.cpp index b6fbe6fb..bb930a40 100644 --- a/lib/core/src/qx-system_win.cpp +++ b/lib/core/src/qx-system_win.cpp @@ -71,7 +71,7 @@ quint32 processId(QString processName) PROCESSENTRY32 entry; entry.dwSize = sizeof(PROCESSENTRY32); - HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if(Process32First(snapshot, &entry)) { @@ -100,7 +100,7 @@ QString processName(quint32 processId) PROCESSENTRY32 entry; entry.dwSize = sizeof(PROCESSENTRY32); - HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if(Process32First(snapshot, &entry)) @@ -138,7 +138,7 @@ QList processChildren(quint32 processId, bool recursive) QList cPids = {processId}; // Initialize snapshot - if(HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL)) + if(HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)) { // Initialize entry data holder PROCESSENTRY32 procEntry; diff --git a/lib/core/src/qx-systemsignalwatcher.cpp b/lib/core/src/qx-systemsignalwatcher.cpp new file mode 100644 index 00000000..ed6edea9 --- /dev/null +++ b/lib/core/src/qx-systemsignalwatcher.cpp @@ -0,0 +1,424 @@ +// Unit Include +#include "qx/core/qx-systemsignalwatcher.h" +#include "qx-systemsignalwatcher_p.h" + +// Inter-component Includes +#ifdef _WIN32 +#include "__private/qx-signaldaemon_win.h" +#endif +#ifdef __linux__ +#include "__private/qx-signaldaemon_linux.h" +#endif + +namespace Qx +{ +/*! @cond */ + +//=============================================================================================================== +// SystemSignalWatcherPrivate +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------- +//Public: +SystemSignalWatcherPrivate::SystemSignalWatcherPrivate(SystemSignalWatcher* q) : + q_ptr(q), + mWatching(Signal::None) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: +bool SystemSignalWatcherPrivate::isRegistered() const { return mRepresentative.isValid(); } + +//Public: +void SystemSignalWatcherPrivate::watch(Signals s) +{ + if(s == mWatching) + return; + + /* We might not need the manager here, but we need to ensure we lock it before updating mWatching since + * the manager can read that value (could use our own mutex for that instead, but avoiding that for now + * for simplicity). + * + * Additionally, if registering, the lock prevents smRollingId from being corrupted by other instances + */ + auto man = SswManager::instance(); + mWatching = s; + + // Going to None doesn't require extra work since we stay registered + if(s == Signal::None) + return; + + // Register if not already + if(!isRegistered()) + { + mRepresentative = {.ptr = this, .id = smRollingId++}; + man->registerWatcher(mRepresentative); + } +} + +void SystemSignalWatcherPrivate::yield() +{ + if(!isRegistered()) + return; + + auto man = SswManager::instance(); + man->sendToBack(mRepresentative); +} + +SystemSignalWatcherPrivate::Signals SystemSignalWatcherPrivate::watching() const { return mWatching; } + +void SystemSignalWatcherPrivate::notify(Signal s) +{ + /* QMetaObject::invokeMethod() is thread-safe, so it's OK that the manager calls this without a lock. + * + * This function should always be invoked by a thread other than the one that the watcher lives in, + * but since we need to avoid deadlocks due to a signal coming in while the mutex to the manager + * is already locked, (should slots connected to `signaled` use the manager before the previous + * lock is released), and other unexpected processing order shenanigans, we explicitly queue up the + * signal emission here to be safe. + */ + Q_Q(SystemSignalWatcher); + + // Run this in the thread that the watcher lives + QMetaObject::invokeMethod(q, [q, s, this]{ + // Ignore signals that arrive late after a watch change + if(mWatching == Signal::None) + return; + + // Emit signal and handle result + bool handled = false; + emit q->signaled(s, &handled); + auto man = SswManager::instance(); + man->processResponse(mRepresentative, s, handled); + }, + Qt::QueuedConnection); +} + +//=============================================================================================================== +// SswManager +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------- +//Private: +SswManager::SswManager() = default; + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Private: +void SswManager::dispatch(Signal signal, const SswRep& disp) +{ + Q_ASSERT(mDispatchTracker.contains(signal)); + + if(disp.isValid()) // Watcher handler type + { + if(!mWatcherRegistry.contains(disp)) // Watcher was unregistered, sim response + processResponse(disp, signal, false); + else + disp.ptr->notify(signal); + } + else // Default handler type + { + auto daemon = SignalDaemon::instance(); + daemon->callDefaultHandler(signal); + + /* The above likely kills the program, but in case the default was replaced with + * something else before we installed our signal handlers, and we're still alive, + * keep going. + */ + processResponse(disp, signal, true); + } +} + +//Public: +void SswManager::registerWatcher(const SswRep& rep) +{ + Q_ASSERT(!mWatcherRegistry.contains(rep) && rep.isValid()); + mWatcherRegistry.append(rep); // Registry is checked backwards, so this is first + + // Update signal tracker + Signals watched = rep.ptr->watching(); + for(Signal s : ALL_SIGNALS) + { + if(watched.testFlag(s) && mSignalTracker.push(s)) + { + auto daemon = SignalDaemon::instance(); + daemon->addSignal(s); + } + } +} + +void SswManager::unregisterWatcher(const SswRep& rep) +{ + Q_ASSERT(!mWatcherRegistry.isEmpty() && rep.isValid()); + + // Remove from registry + qsizetype rem = mWatcherRegistry.removeIf([rep](const SswRep& registered) { return registered == rep; }); + Q_ASSERT(rem == 1); + + // Update signal tracker + Signals watched = rep.ptr->watching(); + for(Signal s : ALL_SIGNALS) + { + if(watched.testFlag(s) && mSignalTracker.pop(s)) + { + auto daemon = SignalDaemon::instance(); + daemon->removeSignal(s); + } + } + + // Advance any queues that are on the watcher + for(auto [sig, dSet] : mDispatchTracker.asKeyValueRange()) + { + if(dSet.empty()) + continue; + + // Safe to double dip queues, as there should be no empty dispatch groups in dispatch sets + SswRep disp = dSet.front().front(); + if(disp.isValid() && disp == rep) + processResponse(disp, sig, false); // Manually fire reponse since it won't be received now + } +} + +void SswManager::sendToBack(const SswRep& rep) +{ + Q_ASSERT(!mWatcherRegistry.isEmpty()); + if(mWatcherRegistry.size() < 2) + return; + + // Remove from registry + qsizetype rem = mWatcherRegistry.removeIf([rep](const SswRep& registered) { return registered == rep; }); + Q_ASSERT(rem == 1); + + // Re-insert at start (effectively end) + mWatcherRegistry.prepend(rep); +} + +void SswManager::processResponse(const SswRep& rep, Signal signal, bool handled) +{ + // Get current dispatch for signal + Q_ASSERT(mDispatchTracker.contains(signal)); + DispatchSet& ds = mDispatchTracker[signal]; Q_ASSERT(!ds.empty()); + DispatchGroup& dg = ds.front(); Q_ASSERT(!dg.empty()); + SswRep& curDispatch = dg.front(); Q_ASSERT(curDispatch == rep); + + if(!handled) + dg.pop(); // Remove individual dispatch + + if(handled || dg.empty()) + ds.pop(); // Remove whole group + + // Go next if not done + if(!ds.empty()) + { + dg = ds.front(); Q_ASSERT(!dg.empty()); // Never should have an empty group outside of modification + dispatch(signal, dg.front()); + } +} + +void SswManager::processSignal(Signal signal) +{ + DispatchSet& ds = mDispatchTracker[signal]; + bool processing = !ds.empty(); // Will be processing if not empty + + // Add new group + DispatchGroup& dg = ds.emplace(); + + // Fill group queue (last registered goes first) + for(auto itr = mWatcherRegistry.crbegin(); itr != mWatcherRegistry.crend(); ++itr) + if(itr->ptr->watching().testFlag(signal)) + dg.push(*itr); + + // Add default implementation dispatcher (i.e. invalid SswRep) last + dg.emplace(); + + // Start processing signals if not already + if(!processing) + dispatch(signal, dg.front()); +} + +/*! @endcond */ + +//=============================================================================================================== +// SystemSignalWatcher +//=============================================================================================================== + +/*! + * @class SystemSignalWatcher qx/core/qx-systemsignalwatcher.h + * @ingroup qx-core + * + * @brief The SystemSignalWatcher class provides a convenient and comprehensive way to react to system + * defined signals. + * + * SystemSignalWatcher acts as user friendly replacement for the troubled std::signal, in a similar vein to + * sigaction(), but is cross-platform and features a Qt-oriented interface, unlike the latter. + * + * Any number of watchers can be created in any thread and are initially inactive, with the first call to + * watch() with a value other than Signal::None registering the watcher to listen for system signals. Signals + * are delivered to watchers on a last-registered, first-served basis, where each watcher can choose whether + * to block the signal from further handling, or allow it to proceed to the next watcher in the list in + * a similar fashion to event filters. If no watcher marks the signal as handled, the default system handler + * for the signal is called. + * + * Which system signals a given watcher is waiting for can be changed at any time, but its position in the + * delivery priority list is maintained until it's destroyed or yield() is called. + * + * @sa watch() and signaled(). + */ + +//-Class Enums----------------------------------------------------------------------------------------------- +//Public: +/*! + * @enum SystemSignalWatcher::Signal + * + * This enum specifies the system signal that was received by the watcher. + * + * Each system signal is based on a standard POSIX signal, though only a subset are supported. On Windows, + * native signals are mapped to their nearest POSIX equivalents, or a reasonable alternative if there is + * none, as shown below: + * + * + * + *
Mapping of native signals to SystemSignalWatcher::Signal
Signal Linux Windows + *
Interrupt SIGINT CTRL_C_EVENT + *
HangUp SIGHUP CTRL_CLOSE_EVENT + *
Quit SIGQUIT CTRL_BREAK_EVENT + *
Terminate SIGTERM CTRL_SHUTDOWN_EVENT + *
Abort SIGABRT CTRL_LOGOFF_EVENT + *
+ * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::None + * No signals to watch for. + * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::Interrupt + * Terminal interrupt signal. + * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::HangUp + * Hangup signal. + * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::Quit + * Terminal quit signal. + * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::Terminate + * Termination signal. + * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::Abort + * Process abort signal. + * + * @qflag{SystemSignalWatcher::Signals, SystemSignalWatcher::Signal} + * + * @sa signaled(). + */ + +//-Constructor-------------------------------------------------------------------- +//Public: +/*! + * Creates an inactive SystemSignalWatcher without a dispatch priority. + */ +SystemSignalWatcher::SystemSignalWatcher() : d_ptr(std::make_unique(this)) {} + +//-Destructor-------------------------------------------------------------------- +//Public: +/*! + * Deactivates and deletes the SystemSignalWatcher. + */ +SystemSignalWatcher::~SystemSignalWatcher() +{ + /* Here instead of private in order to unregister as soon as possible. + * + * NOTE: Current implementation somewhat requires the watcher to stay registered until its destroyed, as + * if it could be unregistered at any time, it does not account for the case where the user causes it to + * be unregistered while in a slot connected to the notify signal. Right now, that would cause its queue + * item to be prematurely canceled and then its actual response given after (failed the assert). Working + * around this would likely require a flag to be set during signal emissions such that if the watcher + * were to be unregistered while high, block the unregistration until the slot has returned (i.e. post + * emit) and then unregister (or have an optional parameter for processResponse() in manager that causes + * unregistration right after the response is handled. The latter would avoid the manager dispatching + * to the watcher again if it happens to be next in queue (after a default handler). + */ + Q_D(SystemSignalWatcher); + if(d->isRegistered()) + { + auto man = SswManager::instance(); + man->unregisterWatcher(d->mRepresentative); + } +} + +/*! + * Starts watching for any of the system signals that make up @a s, or stops the watcher if @a s is only + * Signal::None, in which case the watch is stopped immediately and any "in-flight" signals are + * ignored. + * + * @note + * The first time this function is called with any signal flags set in @a s, the watcher is registered as + * the first to receive any applicable signals, and will remain so until additional watchers are registered, + * this watcher is destroyed, or yield() is called. The dispatch order is independent of signals watched, so + * once a watcher has been registered, calling this function again will only change which signals it pays + * attention to, but will not change its dispatch priority. + * + * @sa stop(), isRegistered(), and isWatching(). + */ +void SystemSignalWatcher::watch(Signals s) { Q_D(SystemSignalWatcher); d->watch(s); } + +/*! + * Stops the watcher from responding to any system signal. This is equivalent to: + * + * `myWatcher.watch(Signal::None)` + * + * @note + * Although this effectively disables the watcher, its position in the dispatch queue is not changed, + * should it start watching for system signals again later. + * + * @sa watch() and isWatching(). + */ +void SystemSignalWatcher::stop() { Q_D(SystemSignalWatcher); d->watch(Signal::None); } + +/*! + * If the watcher has been registered, it's moved to the back of dispatch priority list so that it is + * the last to be able to handle system signals; otherwise, this function does nothing. + * + * @sa stop(). + */ +void SystemSignalWatcher::yield() { Q_D(SystemSignalWatcher); d->yield(); } + +/*! + * Returns the system signals that the watcher is waiting for, or Signal::None if the watcher is not currently + * waiting for any. + * + * @sa isWatching(). + */ +SystemSignalWatcher::Signals SystemSignalWatcher::watching() const { Q_D(const SystemSignalWatcher); return d->watching(); } + +/*! + * Returns @c true if the watcher is waiting for at least one system signal; otherwise, returns @c false. + * + * @sa watching(). + */ +bool SystemSignalWatcher::isWatching() const { Q_D(const SystemSignalWatcher); return d->mWatching != Signal::None; } + +/*! + * Returns @c true if the watcher has been used before at any point (that is, watch() has been called with a + * value other than Signal::None), and has a position in the dispatch priority list; otherwise, returns @c + * false. + * + * @sa isWatching(). + */ +bool SystemSignalWatcher::isRegistered() const { Q_D(const SystemSignalWatcher); return d->isRegistered(); } + +/*! + * @fn void SystemSignalWatcher::signaled(Signal s, bool* handled) + * + * This signal is emitted when the watcher has received a system signal that it's waiting for, with @a s + * containing the signal. + * + * The slot connected to this signal should set @a handled to @c true in order to indicate that the system + * signal has been fully handled and it should be discarded, or @c false to allow the signal to pass on to + * the next SystemSignalWatcher in the dispatch list. If all applicable watchers set @a handled to @c false, + * the system default handler for @a s is triggered. + * + * @warning + * It is not possible to use a QueuedConnection to connect to this signal and control further handling of + * @a s, as the signal will return immediately and use the default value of @a handled (@c false). If you + * do connect to this signal with a QueuedConnection just to be notified of when @a s has been received, + * but not to have influence on whether or not further watchers should be notified, do not deference + * the @a handled pointer as it will no longer be valid. + */ +} diff --git a/lib/core/src/qx-systemsignalwatcher_p.h b/lib/core/src/qx-systemsignalwatcher_p.h new file mode 100644 index 00000000..865f641c --- /dev/null +++ b/lib/core/src/qx-systemsignalwatcher_p.h @@ -0,0 +1,142 @@ +#ifndef QX_SYSTEMSIGNALWATCHER_P_H +#define QX_SYSTEMSIGNALWATCHER_P_H + +// Unit Includes +#include "qx/core/qx-systemsignalwatcher.h" + +// Standard Library Includes +#include + +// Qt Includes +#include + +// Inter-component Includes +#include "qx/core/qx-threadsafesingleton.h" + +/*! @cond */ +namespace Qx +{ + +class SystemSignalWatcherPrivate +{ + Q_DECLARE_PUBLIC(SystemSignalWatcher); + +//-Class Types--------------------------------------------------------------------------------------------- +private: + using Signals = SystemSignalWatcher::Signals; + using Signal = SystemSignalWatcher::Signal; + +public: + struct Representative + { + SystemSignalWatcherPrivate* ptr = nullptr; + uint id = 0; + + inline bool isValid() const { return ptr != nullptr && id != 0; } + inline bool operator==(const Representative& other) const = default; + }; + +//-Class Variables---------------------------------------------------------------------------------------------------- +private: + static inline uint smRollingId = 1; + +//-Instance Variables------------------------------------------------------------------------------------------------- +private: + SystemSignalWatcher* const q_ptr; + Signals mWatching; + Representative mRepresentative; + +//-Constructor------------------------------------------------------------------------------------------------- +public: + SystemSignalWatcherPrivate(SystemSignalWatcher* q); + +//-Instance Functions-------------------------------------------------------------------------------------------- +private: + bool isRegistered() const; + +public: + void watch(Signals s); + void yield(); + + Signals watching() const; + + void notify(Signal s); +}; + +class SswManager : public ThreadSafeSingleton +{ + QX_THREAD_SAFE_SINGLETON(SswManager); +//-Class Types--------------------------------------------------------------------------------------------- +private: + using Ssw = SystemSignalWatcherPrivate; + using Signal = SystemSignalWatcher::Signal; + using Signals = SystemSignalWatcher::Signals; + using SswRep = Ssw::Representative; + + /* TODO: The registry being a list means that although it has good iteration speed, it has worse search speed. + * The only way to remedy this we have is to replace the list with a new template that combines a QSet and + * QList for ordering with good "contains" speed checking; however, this is hard to do without using a linked-list, + * which suffers from slower iteration speed (which might occur more than modification here). + * + * A queue of iterators and list queue using wrappers around shared_ptr and weak_ptr were initially tried instead of + * the current approach as alternatives for making sure that queue ptrs are immediately invalidated and to avoid + * pointer address reuse issues, but both were overly complex. + */ + using Registry = QList; + + class SignalTracker + { + private: + QHash mCounts; + + public: + bool push(Signal s) { return mCounts[s]++ == 0; } + bool pop(Signal s) { Q_ASSERT(mCounts[s] != 0); return --mCounts[s] == 0; } + }; + + using DispatchGroup = std::queue; + using DispatchSet = std::queue; + using DispatchTracker = QHash; // Sets added by signal on the fly + +//-Class Variables--------------------------------------------------------------------------------------------- +private: + // NOTE: ADD NEW SIGNALS HERE + // TODO: Don't feel like introducing magic_enum as a dep. here, so when on C++26 use reflection for this list + static constexpr std::array ALL_SIGNALS{ + Signal::Interrupt, + Signal::HangUp, + Signal::Quit, + Signal::Terminate, + Signal::Abort + }; + +//-Instance Variables------------------------------------------------------------------------------------------ +private: + Registry mWatcherRegistry; + DispatchTracker mDispatchTracker; + SignalTracker mSignalTracker; + +//-Constructor---------------------------------------------------------------------------------------------- +private: + explicit SswManager(); + +//-Instance Functions---------------------------------------------------------------------------------------------- +private: + void dispatch(Signal signal, const SswRep& disp); + +public: + // Watcher -> Manager + void registerWatcher(const SswRep& rep); + void unregisterWatcher(const SswRep& rep); + void sendToBack(const SswRep& rep); + void processResponse(const SswRep& rep, Signal signal, bool handled); + + // Signaler -> Manager + void processSignal(Signal signal); + +}; + +} +/*! @endcond */ + +#endif // QX_SYSTEMSIGNALWATCHER_P_H diff --git a/lib/core/src/qx-threadsafesingleton.dox b/lib/core/src/qx-threadsafesingleton.dox new file mode 100644 index 00000000..de3999e9 --- /dev/null +++ b/lib/core/src/qx-threadsafesingleton.dox @@ -0,0 +1,58 @@ +namespace Qx +{ +//=============================================================================================================== +// ThreadSafeSingleton +//=============================================================================================================== + +/*! + * @class ThreadSafeSingleton qx/core/qx-threadsafesingleton.h + * @ingroup qx-core + * + * @brief The ThreadSafeSingleton template class provides access to a static singleton instance in a thread-safe + * manner. + * + * This class allows one to easily utilize the singleton pattern while ensuring that access to the global shared + * instance remains thread-safe. This is achieved by providing basic scaffolding through a base class from which + * the actual singleton should derive. The instance() method provides a mutex protected pointer to the + * singleton instance via Qx::ExclusiveAccess. By declaring the final class' constructor as private, and accessing + * the class through instance(), one can be certain that the instance is only ever being used by one thread at a + * time. + * + * If code in your singleton calls external code that in turn results in instance() being called again + * from within the same thread, be sure to use QRecursiveMutex when instantiating this template or else you + * may cause a dead-lock. + * + * The following is a complete example for using Qx::ThreadSafeSingleton: + * + * @snippet qx-threadsafesingleton.cpp 0 + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Protected: +/*! + * @fn ThreadSafeSingleton::ThreadSafeSingleton() + * + * Constructs a ThreadSafeSingleton. + * + * @sa instance(). + */ + +//-Class Functions---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn Qx::ExclusiveAccess ThreadSafeSingleton::instance() + * + * Returns a handle to the singleton instance. + */ + +//-Macros---------------------------------------------------------------------------------------------------------- +/*! + * @def QX_THREAD_SAFE_SINGLETON(Singleton) + * + * This macro must be used within the class definition of any singleton that derived from this class, similar to + * Q_OBJECT for classes derived from QObject. + * + * The argument is to be the name of the derived class itself (similar to CRTP). + */ + +} diff --git a/lib/io/src/qx-textpos.cpp b/lib/io/src/qx-textpos.cpp index d4d142aa..bd0c728d 100644 --- a/lib/io/src/qx-textpos.cpp +++ b/lib/io/src/qx-textpos.cpp @@ -47,7 +47,7 @@ TextPos::TextPos(Extent e) break; default: - qCritical("Invalid extent"); + qFatal("Invalid extent"); } } diff --git a/lib/utility/CMakeLists.txt b/lib/utility/CMakeLists.txt index ff28ca27..84eb15f7 100644 --- a/lib/utility/CMakeLists.txt +++ b/lib/utility/CMakeLists.txt @@ -6,11 +6,13 @@ qx_add_component("Utility" qx-helpers.h qx-macros.h qx-stringliteral.h + qx-typetraits.h DOC_ONLY qx-concepts.dox qx-helpers.dox qx-macros.dox qx-stringliteral.dox + qx-typetraits.dox ) # Force conforming preprocessor for MSVC, required for diff --git a/lib/utility/doc/res/snippets/qx-helpers.cpp b/lib/utility/doc/res/snippets/qx-helpers.cpp index 5a366a63..c6408c3f 100644 --- a/lib/utility/doc/res/snippets/qx-helpers.cpp +++ b/lib/utility/doc/res/snippets/qx-helpers.cpp @@ -40,4 +40,74 @@ int main() // 1 (from bool lambda) // c (from wrapped char free function) // hello (from string named lambda) -//! [0] \ No newline at end of file +//! [0] + +//! [1] +template +class Container +{ + T mT; +public: + Container(const T& v) : mT(v) {} + + decltype(auto) operator->() const requires Qx::arrowable_container_type + { + return Qx::container_arrow_operator(mT); + } + + decltype(auto) operator->() requires Qx::arrowable_container_type + { + return Qx::container_arrow_operator(mT); + } +}; + +struct Foo +{ + int data = -1; + void print() const { std::cout << "Const " << data << std::endl; } + void print() { std::cout << "Non-const " << data << std::endl; } +}; + +int main() { + // Helper + Foo fooForPtr{4}; + Foo fooForRef{5}; + Foo eight{8}; + const Foo* fooForPtrRef{&eight}; + int ten{10}; + + // Demo + Foo f{0}; + Container cf(Foo{1}); + Container> cof(Foo{2}); + Container> csf = std::make_shared(3); + Container cpf = &fooForPtr; + Container crf = fooForRef; + const Container ccf(Foo{6}); + Container ccf2(Foo{7}); + Container ccfpr = fooForPtrRef; + Container ci = 9; + Container cpi = &ten; + + cf->print(); + cof->print(); + csf->print(); + cpf->print(); + crf->print(); + ccf->print(); + ccf2->print(); + ccfpr->print(); + //ci->print(); Constraints not satisfied, int doesn't make sense for operator->() + //cpi-print(); Constraints not satisfied, int doesn't make sense for operator->() +} + +//Output: +//Non-const 1 +//Non-const 2 +//Non-const 3 +//Non-const 4 +//Non-const 5 +//Const 6 +//Const 7 +//Const 8 +//! [1] \ No newline at end of file diff --git a/lib/utility/include/qx/utility/qx-concepts.h b/lib/utility/include/qx/utility/qx-concepts.h index f9cac1f4..e1906c4c 100644 --- a/lib/utility/include/qx/utility/qx-concepts.h +++ b/lib/utility/include/qx/utility/qx-concepts.h @@ -2,7 +2,6 @@ #define QX_CONCEPTS_H // Standard Library Includes -#include #include #include @@ -10,21 +9,8 @@ #include #include -/*! @cond */ -namespace QxConceptsPrivate -{ - -template class B> -struct is_specialization_of : std::false_type {}; - -template class B> -struct is_specialization_of, B> : std::true_type {}; - -template class B> -inline constexpr bool is_specialization_of_v = is_specialization_of::value; - -} -/*! @endcond */ +// Inter-component Includes +#include "qx/utility/qx-typetraits.h" namespace Qx { @@ -237,10 +223,11 @@ concept defines_address_of_s = requires(K klass) {{ &klass } -> std::same_as template concept defines_address_of = requires(K klass) {{ &klass };}; -/* TODO: Not sure how to do this one, there is a "b" parameter but its type could be anything - * template - * concept defines_member_ptr_s = requires(K klass, R ret) {{ klass-> } -> std::same_as;}; - */ +template +concept defines_member_ptr = requires(K klass) {{ klass.operator->() };}; + +template +concept defines_member_ptr_s = requires(K klass) {{ klass.operator->() } -> std::same_as;}; template concept defines_ptr_to_member_ptr_for_s = requires(K klass, T type) {{ klass->*type } -> std::same_as;}; @@ -267,7 +254,6 @@ concept defines_comma_for_s = requires(K klass, T type) {{ klass, type } -> std: template concept defines_comma_for = requires(K klass, T type) {{ klass, type };}; - // Arithmetic Operators template concept defines_unary_plus_s = requires(K klass) {{ +klass } -> std::same_as;}; @@ -504,6 +490,9 @@ concept traverseable = std::bidirectional_iterator & std::is_default_constructible_v && requires(K klass) {{ klass.size() } -> std::integral<>;}; +template +concept comparator = defines_call_for_s; + // Conversion template concept static_castable_to = requires(K klass) {{ static_cast(klass) };}; @@ -514,9 +503,9 @@ concept any_of = (std::same_as || ...); // Template template class L> -concept specializes = QxConceptsPrivate::is_specialization_of_v; +concept specializes = is_specialization_of_v; -// Similiar interface types +// Similar interface types template concept qassociative = specializes || specializes; diff --git a/lib/utility/include/qx/utility/qx-helpers.h b/lib/utility/include/qx/utility/qx-helpers.h index 9d7311bf..da3d624b 100644 --- a/lib/utility/include/qx/utility/qx-helpers.h +++ b/lib/utility/include/qx/utility/qx-helpers.h @@ -3,6 +3,66 @@ // Standard Library Includes #include +#include + +// Qt Includes +#include +#include + +// Inner-component Includes +#include "qx/utility/qx-typetraits.h" +#include "qx/utility/qx-concepts.h" + +namespace Qx +{ + +class ScopedConnection +{ + Q_DISABLE_COPY(ScopedConnection); +//-Instance Variables------------------------------------------------------------------------------------------ +private: + QMetaObject::Connection mConnection; + +//-Constructor------------------------------------------------------------------------------------------------- +public: + inline ScopedConnection(const QMetaObject::Connection& connection) : mConnection(connection) {} + inline ScopedConnection(ScopedConnection&& other) = default; + +//-Destructor------------------------------------------------------------------------------------------------- +public: + inline ~ScopedConnection() { if(mConnection) QObject::disconnect(mConnection); } + +//-Operators-------------------------------------------------------------------------------------------------- +public: + inline ScopedConnection& operator=(ScopedConnection&& other) = default; + inline operator bool() const { return static_cast(mConnection); } +}; + +//Namespace Functions----------------------------------------------------------------------------------------- +template +ScopedConnection scopedConnect(const QObject* sender, PointerToMemberFunction signal, Functor&& functor) +{ + return QObject::connect(sender, std::forward(signal), std::forward(functor)); +} + +//TODO: Might want to move this to a container tools specific header (along with some other todos related to them) +template +using container_arrow_result = std::conditional_t, T&, + std::conditional_t>, target_type*, void>>; + +template +concept arrowable_container_type = defines_member_ptr || std::is_class_v>; + +template +container_arrow_result container_arrow_operator(T& data) +{ + if constexpr(defines_member_ptr || std::is_pointer_v) + return data; + else + return &data; +} + +} //Non-namespace Structs---------------------------------------------------------- /* TODO: Figure out how to constrain this to only accept functors, issue is at least as of C++20 @@ -14,6 +74,17 @@ struct qxFuncAggregate : Functors... { using Functors::operator()...; }; +/* Explicit deduction guide. Shouldn't be needed as of C++20, but some compilers are late to the party. + * We'd like to check for #if __cpp_deduction_guides < 201907 to see if the compiler has the feature, + * but it seems that GCC 10.x still won't compile without this even though it reports 201907 + * implementation. This might have been an oversight, or have to due with P2082R1 + * (Fixing CTAD for aggregates) which wasn't implemented in GCC until 11.x + */ +/*! @cond */ +template +qxFuncAggregate(Ts...) -> qxFuncAggregate; +/*! @endcond */ + //Non-namespace Functions---------------------------------------------------------- template const T qxAsConst(T&& t) { return std::move(t); } diff --git a/lib/utility/include/qx/utility/qx-typetraits.h b/lib/utility/include/qx/utility/qx-typetraits.h new file mode 100644 index 00000000..92250105 --- /dev/null +++ b/lib/utility/include/qx/utility/qx-typetraits.h @@ -0,0 +1,26 @@ +#ifndef QX_TYPETRAITS_H +#define QX_TYPETRAITS_H + +// Standard Library Includes +#include + +namespace Qx +{ + +// Specialization +template class B> +struct is_specialization_of : std::false_type {}; + +template class B> +struct is_specialization_of, B> : std::true_type {}; + +template class B> +inline constexpr bool is_specialization_of_v = is_specialization_of::value; + +// Qualifiers +template +using target_type = std::remove_pointer_t>; + +} + +#endif // QX_TYPETRAITS_H diff --git a/lib/utility/src/qx-concepts.dox b/lib/utility/src/qx-concepts.dox index 60d9a015..622e2548 100644 --- a/lib/utility/src/qx-concepts.dox +++ b/lib/utility/src/qx-concepts.dox @@ -12,8 +12,6 @@ * The constrained variants enforce a strict return type that matches the standard * return type for the default implementation of that operator, or a user defined type, @c R, if there * is none. - * - * @todo Figure out how to make a concept that uses the member of pointer (arrow) operator. */ namespace Qx @@ -1101,6 +1099,16 @@ namespace Qx * is default constructable, and defines a member function 'size' that returns an integral type. */ +/*! + * @concept comparator + * @brief Specifies that a type can act as a comparator. + * + * Satisfied if @c F returns a @c bool when called with two values of T. + * + * Generally it is assumed that the comparator will return @c true if the values are equal, and + * @c false otherwise. + */ + // Conversion /*! * @concept static_castable_to diff --git a/lib/utility/src/qx-helpers.dox b/lib/utility/src/qx-helpers.dox index 2ab839d5..ee58ad88 100644 --- a/lib/utility/src/qx-helpers.dox +++ b/lib/utility/src/qx-helpers.dox @@ -6,6 +6,88 @@ * are designed to facilitate common fundamental tasks with as brief syntax as possible. */ +namespace Qx +{ +//=============================================================================================================== +// ScopedConnection +//=============================================================================================================== + +/*! + * @class ScopedConnection qx/utility/qx-helpers.h + * @ingroup qx-core + * + * @brief The ScopedConnection class disconnects a connection when it goes out of scope + * + * This is useful when you have a connection that you want disconnected when a certain context is left, + * but do not have a QObject based context object to use. + * + * @sa scopedConnect(). + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn ScopedConnection::ScopedConnection(const QMetaObject::Connection& connection) + * + * Creates a scoped connection guard from @a connection. + */ + +/*! + * @fn ScopedConnection::ScopedConnection(ScopedConnection&& other) + * + * Move constructs a ScopedConnection from @a other. + */ + +//-Destructor------------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn ScopedConnection::~ScopedConnection() + * + * Destroys the scoped connection guard. The underlying connection will be disconnected if it's valid. + */ + +//-Operators-------------------------------------------------------------------------------------------------- +//Public: +/*! + * @fn ScopedConnection::operator bool() const + * + * Returns @c true if the underlying connection is valid (was successful when connect was called); otherwise, + * returns @c false. + */ + +//Public: +/*! + * @fn ScopedConnection& ScopedConnection::operator=(ScopedConnection&& other) + * + * Move assigns this ScopedCOnnection using @a other. + */ + +//Namespace Functions---------------------------------------------------------- +/*! + * @fn ScopedConnection scopedConnect(const QObject* sender, PointerToMemberFunction signal, Functor&& functor) + * + * The same as QObject::connect(const QObject* sender, PointerToMemberFunction signal, Functor functor), but + * the resulting connection handle is returned as a ScopedConnection instead. + */ + +/*! + * @typedef container_arrow_result + * See container_arrow_operator(). + * + * @concept arrowable_container_type + * See container_arrow_operator(). + * + * @fn container_arrow_result container_arrow_operator(T& data) + * + * This helper function exists so that most types of containers can more easily implement operator->(), for + * any template where it makes sense to have one, and have chaining work correctly. + * + * Example: + * @snippet qx-helpers.cpp 1 + */ + +} + //Non-namespace Structs---------------------------------------------------------- /*! * @struct qxFuncAggregate diff --git a/lib/utility/src/qx-typetraits.dox b/lib/utility/src/qx-typetraits.dox new file mode 100644 index 00000000..08223b55 --- /dev/null +++ b/lib/utility/src/qx-typetraits.dox @@ -0,0 +1,38 @@ +/*! + * @file qx-typetraits.h + * @ingroup qx-utility + * + * @brief The qx-typetraits header file provides a library of general purpose type-traits as an extension of + * the standard type-traits library. + */ + +namespace Qx +{ +// Specialization +/*! + * @struct is_specialization_of + * @brief Checks whether A is a specialization of B. + * @sa is_specialization_of_v + * + * @var is_specialization_of_v + * @brief See is_specialization_of. + */ + +// Qualifiers +/*! + * @typedef target_type + * + * Returns T with the top most reference and pointer removed so that: + * - @c U& + * - @c U* + * - @c U*& + * - @c U + * + * all become @c U. + * + * In other words, returns type U that type T "targets", unless T is already a pure type. + * + * Does not influence constness or other qualifiers. + */ + +} diff --git a/lib/widgets/CMakeLists.txt b/lib/widgets/CMakeLists.txt index 4eff3681..471edc45 100644 --- a/lib/widgets/CMakeLists.txt +++ b/lib/widgets/CMakeLists.txt @@ -1,11 +1,13 @@ #================= Add Component ========================== qx_add_component("Widgets" HEADERS_API + qx-buttongroup.h qx-common-widgets.h qx-logindialog.h qx-standarditemmodel.h qx-treeinputdialog.h IMPLEMENTATION + qx-buttongroup.cpp qx-common-widgets.cpp qx-common-widgets_p.h qx-common-widgets_p.cpp diff --git a/lib/widgets/include/qx/widgets/qx-buttongroup.h b/lib/widgets/include/qx/widgets/qx-buttongroup.h new file mode 100644 index 00000000..48423031 --- /dev/null +++ b/lib/widgets/include/qx/widgets/qx-buttongroup.h @@ -0,0 +1,42 @@ +#ifndef QX_BUTTONGROUP_H +#define QX_BUTTONGROUP_H + +// Shared Lib Support +#include "qx/widgets/qx_widgets_export.h" + +// Qt Includes +#include +#include + +namespace Qx +{ + +// Just adds a property and signal +class QX_WIDGETS_EXPORT ButtonGroup : public QButtonGroup +{ + Q_OBJECT + Q_PROPERTY(QAbstractButton* checkedButton READ checkedButton NOTIFY checkedButtonChanged); +//-Instance Members--------------------------------------------------------------------------------------------------- +private: + QAbstractButton* mCheckedButton; + +//-Constructor------------------------------------------------------------------------------------------------------- +public: + explicit ButtonGroup(QObject* parent = nullptr); + +//-Instance Functions---------------------------------------------------------------------------------------------- +private: + void updateCheckedButton(); + +public: + void addButton(QAbstractButton* button, int id = -1); + void removeButton(QAbstractButton* button); + +//-Signals & Slots---------------------------------------------------------------------------------------------------------- +signals: + void checkedButtonChanged(QAbstractButton* button); +}; + +} + +#endif // QX_BUTTONGROUP_H diff --git a/lib/widgets/include/qx/widgets/qx-standarditemmodel.h b/lib/widgets/include/qx/widgets/qx-standarditemmodel.h index 9caa18a5..6ecfa0b5 100644 --- a/lib/widgets/include/qx/widgets/qx-standarditemmodel.h +++ b/lib/widgets/include/qx/widgets/qx-standarditemmodel.h @@ -19,19 +19,19 @@ class QX_WIDGETS_EXPORT StandardItemModel : public QStandardItemModel //-Constructor------------------------------------------------------------------------------------------------------- public: - StandardItemModel(int rows, int columns, QObject *parent = nullptr); - StandardItemModel(QObject *parent = nullptr); + StandardItemModel(int rows, int columns, QObject* parent = nullptr); + StandardItemModel(QObject* parent = nullptr); //-Instance Functions----------------------------------------------------------------------------------------------- private: - void autoTristateChildren(QStandardItem* changingItem, const QVariant& value, int role); - void autoTristateParents(QStandardItem* changingItem, const QVariant& changingValue); + void autoTristateChildren(QStandardItem* changingItem, const QVariant& value, int role); + void autoTristateParents(QStandardItem* changingItem, const QVariant& changingValue); public: - virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); + virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; bool isAutoTristate(); void setAutoTristate(bool autoTristate); - void forEachItem(std::function const& func, QModelIndex parent = QModelIndex()); + void forEachItem(const std::function& func, QModelIndex parent = QModelIndex()) const; void selectAll(); void selectNone(); diff --git a/lib/widgets/src/qx-buttongroup.cpp b/lib/widgets/src/qx-buttongroup.cpp new file mode 100644 index 00000000..645d6346 --- /dev/null +++ b/lib/widgets/src/qx-buttongroup.cpp @@ -0,0 +1,79 @@ +// Unit Includes +#include "qx/widgets/qx-buttongroup.h" + +namespace Qx +{ + +//=============================================================================================================== +// ButtonGroup +//=============================================================================================================== + +/*! + * @class LoginDialog qx/widgets/qx-buttongroup.h + * @ingroup qx-widgets + * + * @brief The ButtonGroup class provides a container to organize groups of button widgets + * + * This class is the same as QButtonGroup, with a property and change signal for the currently checked button + */ + +//-Constructor--------------------------------------------------------------------------------------------------- +//Public: +/*! + * Constructs a new, empty button group with the given parent. + * + * @sa addButton() and setExclusive(). + */ +ButtonGroup::ButtonGroup(QObject* parent) : + QButtonGroup(parent), + mCheckedButton(nullptr) +{ + connect(this, &QButtonGroup::buttonToggled, this, &ButtonGroup::updateCheckedButton); +} + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: +void ButtonGroup::updateCheckedButton() +{ + auto baseChecked = QButtonGroup::checkedButton(); + if(mCheckedButton != baseChecked) + { + mCheckedButton = baseChecked; + emit checkedButtonChanged(mCheckedButton); + } +} + +//Public: +/*! + * Adds the given button to the button group. If id is -1, an id will be assigned to the button. + * Automatically assigned ids are guaranteed to be negative, starting with -2. If you are assigning + * your own ids, use positive values to avoid conflicts. + * + * @sa removeButton() and buttons(). + */ +void ButtonGroup::addButton(QAbstractButton* button, int id) +{ + QButtonGroup::addButton(button, id); + updateCheckedButton(); +} + +/*! + * Removes the given button from the button group. + * + * @sa addButton() and buttons(). + */ +void ButtonGroup::removeButton(QAbstractButton* button) +{ + QButtonGroup::removeButton(button); + updateCheckedButton(); +} + +//-Signals & Slots--------------------------------------------------------------------------------------------- +//Public Signals: +/*! + * @fn void ButtonGroup::checkedButtonChanged(QAbstractButton* button) + * + * This signal is emitted whenever the button group's checked button changes. @a button can be @c nullptr. + */ + +} diff --git a/lib/widgets/src/qx-standarditemmodel.cpp b/lib/widgets/src/qx-standarditemmodel.cpp index 0c860073..96756cd0 100644 --- a/lib/widgets/src/qx-standarditemmodel.cpp +++ b/lib/widgets/src/qx-standarditemmodel.cpp @@ -137,7 +137,7 @@ void StandardItemModel::setAutoTristate(bool autoTristate) { mAutoTristate = aut * @param parent A model index pointing to the item for processing to start at. A null index causes * processing to start at the root item of the model, thereby calling the routine on all items. */ -void StandardItemModel::forEachItem(const std::function& func, QModelIndex parent) +void StandardItemModel::forEachItem(const std::function& func, QModelIndex parent) const { for(int r = 0; r < rowCount(parent); ++r) { diff --git a/lib/windows-gui/src/qx-taskbarbutton.cpp b/lib/windows-gui/src/qx-taskbarbutton.cpp index 1d99f2f9..8242e842 100644 --- a/lib/windows-gui/src/qx-taskbarbutton.cpp +++ b/lib/windows-gui/src/qx-taskbarbutton.cpp @@ -3,6 +3,11 @@ // Windows Includes #include "qx_windows.h" +#ifdef Q_CC_MINGW // MinGW headers for this are less fine-grained + #include "Shlobj.h" +#else + #include "ShlObj_core.h" +#endif // Intra-component Includes #include "qx/windows-gui/qx-winguievent.h" diff --git a/lib/windows/CMakeLists.txt b/lib/windows/CMakeLists.txt index 71d85c96..22d99349 100644 --- a/lib/windows/CMakeLists.txt +++ b/lib/windows/CMakeLists.txt @@ -6,6 +6,8 @@ qx_add_component("Windows" qx-windefs.h IMPLEMENTATION qx-common-windows.cpp + qx-common-windows_p.h + qx-common-windows_p.cpp qx-filedetails.cpp DOC_ONLY qx-windefs.dox diff --git a/lib/windows/include/qx_windows.h b/lib/windows/include/qx_windows.h index c44e5cb7..907f7648 100644 --- a/lib/windows/include/qx_windows.h +++ b/lib/windows/include/qx_windows.h @@ -1,4 +1,26 @@ -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX +// ifdefs are to avoid macro redefinition warnings +#ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOATOM + #define NOATOM +#endif +#ifndef NOGDI + #define NOGDI +#endif +#ifndef NOMINMAX + #define NOMINMAX +#endif +#ifndef NOSOUND + #define NOSOUND +#endif +#ifndef NOHELP + #define NOHELP +#endif +#ifndef NOMCX + #define NOMCX +#endif + #include "windows.h" -#include "ShObjIdl_core.h" \ No newline at end of file + +// See windows.h for more "NOXXX" options diff --git a/lib/windows/src/qx-common-windows.cpp b/lib/windows/src/qx-common-windows.cpp index f9db5222..ccc42144 100644 --- a/lib/windows/src/qx-common-windows.cpp +++ b/lib/windows/src/qx-common-windows.cpp @@ -1,5 +1,6 @@ // Unit Includes #include "qx/windows/qx-common-windows.h" +#include "qx-common-windows_p.h" // Qt Includes #include @@ -9,12 +10,14 @@ // Windows Includes #include "qx_windows.h" #include "TlHelp32.h" -#include "comdef.h" -#include "ShlGuid.h" -#include "atlbase.h" +#ifdef Q_CC_MINGW // MinGW headers for this are less fine-grained + #include "Shlobj.h" +#else + #include "ShlObj_core.h" +#endif +#include // Extra-component Includes -#include "qx/core/qx-bitarray.h" #include "qx/core/qx-system.h" /*! @@ -380,6 +383,7 @@ SystemError getLastError() return SystemError::fromHresult(HRESULT_FROM_WIN32(error)); } + /*! * Creates a shortcut on the user's filesystem at the path @a shortcutPath, with the given * shortcut properties @a sp. @@ -390,83 +394,59 @@ SystemError createShortcut(QString shortcutPath, ShortcutProperties sp) if(sp.target.isEmpty() || shortcutPath.isEmpty() || sp.iconIndex < 0) return SystemError::fromHresult(E_INVALIDARG); - // Working vars - HRESULT hRes; - CComPtr ipShellLink; - - // Get full path of target - QFileInfo targetInfo(sp.target); - QString fullTargetPath = targetInfo.absoluteFilePath(); - - - // Get a pointer to the IShellLink interface - auto getIShellLinkPtr = [](CComPtr& ptrShellLnk){ - return CoCreateInstance(CLSID_ShellLink, - NULL, - CLSCTX_INPROC_SERVER, - IID_IShellLink, - (void**)&ptrShellLnk); - }; - - hRes = getIShellLinkPtr(ipShellLink); - - // Handle the case for if the COM server wasn't already initialized in this thread - QScopeGuard COMGuard([]{ CoUninitialize(); }); - if(hRes == CO_E_NOTINITIALIZED) + // Ensure COM is ready + ScopedCom com; + if(!com) + return com.error(); + + // Access IShelLink interface + using namespace Microsoft::WRL; + ComPtr ipShellLink; + HRESULT hRes = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ipShellLink)); + if(FAILED(hRes)) + return SystemError::fromHresult(hRes); + + // Get a pointer to the IPersistFile interface + ComPtr ipPersistFile; + hRes = ipShellLink.As(&ipPersistFile); + if(FAILED(hRes)) + return SystemError::fromHresult(hRes); + + // Set shortcut properties + // NOTE: The string casts here are only valid on Windows where wchat_t is 2-bytes in size + QString tgtPath = QFileInfo(sp.target).absoluteFilePath(); + hRes = ipShellLink->SetPath((const wchar_t*)tgtPath.utf16()); + if(FAILED(hRes)) return SystemError::fromHresult(hRes); + + if(!sp.targetArgs.isEmpty()) { - CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); - hRes = getIShellLinkPtr(ipShellLink); + hRes = ipShellLink->SetArguments((const wchar_t*)sp.targetArgs.utf16()); + if (FAILED(hRes)) return SystemError::fromHresult(hRes); } - else - COMGuard.dismiss(); - // Commence shortcut creation if COM server was properly connected to - if(SUCCEEDED(hRes)) + if(!sp.startIn.isEmpty()) { - // Get a pointer to the IPersistFile interface - CComQIPtr ipPersistFile(ipShellLink); - - // Set shortcut properties - hRes = ipShellLink->SetPath((const wchar_t*)fullTargetPath.utf16()); - if(FAILED(hRes)) - return SystemError::fromHresult(hRes); - - if(!sp.targetArgs.isEmpty()) - { - hRes = ipShellLink->SetArguments((const wchar_t*)sp.targetArgs.utf16()); - if (FAILED(hRes)) - return SystemError::fromHresult(hRes); - } - - if(!sp.startIn.isEmpty()) - { - hRes = ipShellLink->SetWorkingDirectory((const wchar_t*)sp.startIn.utf16()); - if (FAILED(hRes)) - return SystemError::fromHresult(hRes); - } - - if(!sp.comment.isEmpty()) - { - hRes = ipShellLink->SetDescription((const wchar_t*)sp.comment.utf16()); - if (FAILED(hRes)) - return SystemError::fromHresult(hRes); - } - - if(!sp.iconFilePath.isEmpty()) - { - hRes = ipShellLink->SetIconLocation((const wchar_t*)sp.iconFilePath.utf16(), sp.iconIndex); - if (FAILED(hRes)) - return SystemError::fromHresult(hRes); - } + hRes = ipShellLink->SetWorkingDirectory((const wchar_t*)sp.startIn.utf16()); + if (FAILED(hRes)) return SystemError::fromHresult(hRes); + } - hRes = ipShellLink->SetShowCmd(nativeShowMode(sp.showMode)); - if(FAILED(hRes)) - return SystemError::fromHresult(hRes); + if(!sp.comment.isEmpty()) + { + hRes = ipShellLink->SetDescription((const wchar_t*)sp.comment.utf16()); + if (FAILED(hRes)) return SystemError::fromHresult(hRes); + } - // Write the shortcut to disk - hRes = ipPersistFile->Save((const wchar_t*)shortcutPath.utf16(), TRUE); + if(!sp.iconFilePath.isEmpty()) + { + hRes = ipShellLink->SetIconLocation((const wchar_t*)sp.iconFilePath.utf16(), sp.iconIndex); + if (FAILED(hRes)) return SystemError::fromHresult(hRes); } + hRes = ipShellLink->SetShowCmd(nativeShowMode(sp.showMode)); + if(FAILED(hRes)) return SystemError::fromHresult(hRes); + + // Write the shortcut to disk + hRes = ipPersistFile->Save((const wchar_t*)shortcutPath.utf16(), TRUE); return SystemError::fromHresult(hRes); } diff --git a/lib/windows/src/qx-common-windows_p.cpp b/lib/windows/src/qx-common-windows_p.cpp new file mode 100644 index 00000000..7e88fc4c --- /dev/null +++ b/lib/windows/src/qx-common-windows_p.cpp @@ -0,0 +1,70 @@ +// Unit Includes +#include "qx-common-windows_p.h" + +// Qt Includes +#include +#include + +// Windows Includes +#include "combaseapi.h" + +namespace Qx +{ +/*! @cond */ +//=============================================================================================================== +// ScopedCom +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------- +//Public: +ScopedCom::ScopedCom() : + mThreadId(GetCurrentThreadId()), + mCleanup(false) +{ + // Check if COM is initialized (parameter return values are ignored) + APTTYPE aptType; + APTTYPEQUALIFIER aptTypeQualifier; + HRESULT hRes = CoGetApartmentType(&aptType, &aptTypeQualifier); + if(SUCCEEDED(hRes)) + return; // COM is ready, do nothing + else if(hRes != CO_E_NOTINITIALIZED) + { + // True error + mError = SystemError::fromHresult(hRes); + return; + } + + // Init COM (shouldn't ever be needed in the main thread, but we check just in-case) + auto app = QCoreApplication::instance(); + bool inMainThread = app && app->thread() == QThread::currentThread(); // TODO: Use Qt 6.8 isMainThread() + int tm = (inMainThread ? COINIT_APARTMENTTHREADED : COINIT_MULTITHREADED) | COINIT_DISABLE_OLE1DDE; + hRes = CoInitializeEx(NULL, tm); + if(!SUCCEEDED(hRes)) // Should never fail, but hey... + { + mError = SystemError::fromHresult(hRes); + return; + } + + mCleanup = true; +} + +//-Destructor-------------------------------------------------------------------------------------------------- +//Public: +ScopedCom::~ScopedCom() +{ + Q_ASSERT(mThreadId == GetCurrentThreadId()); + if(mCleanup) + CoUninitialize(); +} + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Public: +bool ScopedCom::hasError() const { return mError.isValid(); } +SystemError ScopedCom::error() const { return mError; } + +//-Operators-------------------------------------------------------------------------------------------- +//Public: +ScopedCom::operator bool() const { return !hasError(); } + +/*! @endcond */ +} diff --git a/lib/windows/src/qx-common-windows_p.h b/lib/windows/src/qx-common-windows_p.h new file mode 100644 index 00000000..04b274e0 --- /dev/null +++ b/lib/windows/src/qx-common-windows_p.h @@ -0,0 +1,38 @@ +#ifndef QX_WINDOWS_COMMON_P_H +#define QX_WINDOWS_COMMON_P_H + +// Qt Includes +#include + +// Inter-component Includes +#include "qx/windows/qx-windefs.h" + +// Extra-component Includes +#include "qx/core/qx-systemerror.h" + +namespace Qx +{ +/*! @cond */ + +// Makes sure COM is initialized, and cleans up on deletion. +// Based on QComHelper, but slightly more flexible in that it just cares COM is initialized one way or another. +class ScopedCom +{ + Q_DISABLE_COPY_MOVE(ScopedCom); +private: + SystemError mError; + DWORD mThreadId; // For safety check + bool mCleanup; + +public: + ScopedCom(); + ~ScopedCom(); + + bool hasError() const; + SystemError error() const; + explicit operator bool() const; +}; + +/*! @endcond */ +} +#endif // QX_WINDOWS_COMMON_P_H diff --git a/lib/windows/src/qx-filedetails.cpp b/lib/windows/src/qx-filedetails.cpp index ea8abb2e..fd9bd5f7 100644 --- a/lib/windows/src/qx-filedetails.cpp +++ b/lib/windows/src/qx-filedetails.cpp @@ -225,7 +225,7 @@ FileDetails FileDetails::readFileDetails(QString filePath) { DWORD verInfoHandle, verInfoSize = GetFileVersionInfoSize((const wchar_t*)filePath.utf16(), &verInfoHandle); - if (verInfoSize != NULL) + if (verInfoSize != 0) { LPSTR verInfo = new char[verInfoSize];