diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 22a2411f..87db274f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,8 +7,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ubuntu-latest]
- #os: [ubuntu-latest, macos-latest]
+ os: [ubuntu-latest, macos-latest]
runs-on: ${{matrix.os}}
@@ -45,10 +44,9 @@ jobs:
if: startsWith(matrix.os, 'macos')
run: |
brew install opencascade
- GH_CASCADE_BASE_DIR=`brew --cellar opencascade`
- GH_CASCADE_VERSION=`brew info opencascade | grep -E --only-matching --max-count=1 "[0-9]\.[0-9]\.[0-9]"`
- echo "GH_CASCADE_INC_DIR=$GH_CASCADE_BASE_DIR/$GH_CASCADE_VERSION/include/opencascade" >> $GITHUB_ENV
- echo "GH_CASCADE_LIB_DIR=$GH_CASCADE_BASE_DIR/$GH_CASCADE_VERSION/lib" >> $GITHUB_ENV
+ GH_CASCADE_BASE_DIR=`brew info opencascade | grep -E -i --only-matching --max-count=1 "^(/[a-z\.\-_0-9]+)+"`
+ echo "GH_CASCADE_INC_DIR=$GH_CASCADE_BASE_DIR/include/opencascade" >> $GITHUB_ENV
+ echo "GH_CASCADE_LIB_DIR=$GH_CASCADE_BASE_DIR/lib" >> $GITHUB_ENV
- name: Get count of CPU cores
uses: SimenB/github-actions-cpu-cores@v1
@@ -62,6 +60,8 @@ jobs:
run: |
echo CASCADE_INC_DIR=${{env.GH_CASCADE_INC_DIR}}
echo CASCADE_LIB_DIR=${{env.GH_CASCADE_LIB_DIR}}
+ [ ! -d $CASCADE_INC_DIR ] && echo "ERROR: OpenCascade include dir doesn't exist"
+ [ ! -d $CASCADE_LIB_DIR ] && echo "ERROR: OpenCascade lib dir doesn't exist"
qmake ../source CASCADE_INC_DIR=${{env.GH_CASCADE_INC_DIR}} CASCADE_LIB_DIR=${{env.GH_CASCADE_LIB_DIR}} CONFIG+=withtests
- name: Build
@@ -75,8 +75,14 @@ jobs:
env:
DISPLAY: :0
run: |
+ # Install french locale for testing case
+ echo "fr_FR ISO-8859-1" | sudo tee -a /etc/locale.gen
+ sudo locale-gen
+ localectl list-locales
+ # Start X11 display server
Xvfb $DISPLAY -screen 0 1280x1024x24 &
sleep 5s
+ # Run tests
./mayo --runtests
- name: Execute Unit Tests[macOS]
diff --git a/README.md b/README.md
index db904fbe..e8051a4e 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,11 @@
[![CI](https://github.com/fougue/mayo/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/fougue/mayo/actions/workflows/ci.yml)
-[![Build status](https://ci.appveyor.com/api/projects/status/6d1w0d6gw28npxpf?svg=true)](https://ci.appveyor.com/project/HuguesDelorme/mayo)
+[![Build status](https://ci.appveyor.com/api/projects/status/6d1w0d6gw28npxpf/branch/develop?svg=true)](https://ci.appveyor.com/project/HuguesDelorme/mayo)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/d51f8ca6fea34886b8308ff0246172ce)](https://www.codacy.com/gh/fougue/mayo/dashboard?utm_source=github.com&utm_medium=referral&utm_content=fougue/mayo&utm_campaign=Badge_Grade)
[![Downloads](https://img.shields.io/github/downloads/fougue/mayo/total.svg)](https://github.com/fougue/mayo/releases)
[![License](https://img.shields.io/badge/license-BSD%202--clause-blue.svg)](https://github.com/fougue/mayo/blob/develop/LICENSE.txt)
-[![Version](https://img.shields.io/badge/version-v0.6.0-blue.svg?style=flat)](https://github.com/fougue/mayo/releases)
+[![Version](https://img.shields.io/badge/version-v0.7.0-blue.svg?style=flat)](https://github.com/fougue/mayo/releases)
@@ -30,6 +30,10 @@ Mayo runs on Windows, Linux and macOS
- **Solid foundations**
Mayo is developed in modern C++ with [Qt](https://www.qt.io) and [OpenCascade](https://dev.opencascade.org)
+For more details have a look at this fine review [Introducing Mayo](https://librearts.org/2023/01/introducing-mayo-free-cad-files-viewer) by Libre Arts
+There's also a complete [video](https://www.youtube.com/watch?v=qg6IamnlfxE&ab_channel=LibreArts) on YouTube
+
+
## :zap: Features
- **3D clip planes** with configurable capping
@@ -56,10 +60,11 @@ BREP | :white_check_mark: | :white_check_mark: | OpenCascade format
DXF | :white_check_mark: | :x: |
OBJ | :white_check_mark: | :white_check_mark: |
glTF | :white_check_mark: | :white_check_mark: | 1.0, 2.0 and GLB
-VRML | :x: | :white_check_mark: | v2.0 UTF8
+VRML | :white_check_mark: | :white_check_mark: | v2.0 UTF8
STL | :white_check_mark: | :white_check_mark: | ASCII/binary
AMF | :x: | :white_check_mark: | v1.2 Text/ZIP
PLY | :white_check_mark: | :white_check_mark: | ASCII/binary
+OFF | :white_check_mark: | :white_check_mark: |
Image | :x: | :white_check_mark: | PNG, JPEG, ...
See also this dedicated [wikipage](https://github.com/fougue/mayo/wiki/Supported-formats) for more details
@@ -86,12 +91,20 @@ Mayo supports also multiple 3D viewer navigation styles to mimic common CAD appl
## :clapper: Gallery
-
-
+_Easy to use command-line utility for batch conversion of CAD files_
+
+
+_Import of glTF file with textures_
+_Import of STEP file with many parts_
+_Import of PLY file defining point cloud_
+
+
+_Options dialog with import/export configuration per CAD format_
+_Home page with quick access to recent files_
diff --git a/appveyor.yml b/appveyor.yml
index 18fae026..4f7157bf 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 0.6_build{build}
+version: 0.7_build{build}
image: Visual Studio 2017
platform: x64
@@ -6,24 +6,28 @@ configuration: Release
clone_folder: c:\projects\mayo
-branches:
- only:
- - develop
- - master
+#branches:
+# only:
+# - develop
+# - master
matrix:
fast_finish: true
environment:
matrix:
+ - APPVEYOR_OCC_VERSION: 7.3.0
- APPVEYOR_OCC_VERSION: 7.4.0
- APPVEYOR_OCC_VERSION: 7.5.0
- APPVEYOR_OCC_VERSION: 7.6.0
+ - APPVEYOR_OCC_VERSION: 7.7.0
cache:
+ - OpenCASCADE-7.3.0-vc14-64.rar
- OpenCASCADE-7.4.0-vc14-64.rar
- OpenCASCADE-7.5.0-vc14-64.rar
- OpenCASCADE-7.6.0-vc14-64.rar
+ - OpenCASCADE-7.7.0-vc14-64.rar
install:
- if not exist OpenCASCADE-%APPVEYOR_OCC_VERSION%-vc14-64.rar
diff --git a/doc/3rdparty_licenses.md b/doc/3rdparty_licenses.md
new file mode 100644
index 00000000..93fb0e89
--- /dev/null
+++ b/doc/3rdparty_licenses.md
@@ -0,0 +1,1015 @@
+# Copyrights and licenses of third party libraries used by Mayo
+
+## [Qt](https://www.qt.io)
+````
+GNU LESSER GENERAL PUBLIC LICENSE
+
+The Qt Toolkit is Copyright (C) 2015 The Qt Company Ltd.
+Contact: http://www.qt.io/licensing/
+
+You may use, distribute and copy the Qt Toolkit under the terms of
+GNU Lesser General Public License version 3, which is displayed below.
+This license makes reference to the version 3 of the GNU General
+Public License, which you can find in the LICENSE.GPLv3 file.
+
+-------------------------------------------------------------------------
+
+GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+Copyright © 2007 Free Software Foundation, Inc.
+Everyone is permitted to copy and distribute verbatim copies of this
+licensedocument, but changing it is not allowed.
+
+This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+0. Additional Definitions.
+
+As used herein, “this License” refers to version 3 of the GNU Lesser
+General Public License, and the “GNU GPL” refers to version 3 of the
+GNU General Public License.
+
+“The Library” refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+An “Application” is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+A “Combined Work” is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the “Linked
+Version”.
+
+The “Minimal Corresponding Source” for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+The “Corresponding Application Code” for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+1. Exception to Section 3 of the GNU GPL.
+
+You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+2. Conveying Modified Versions.
+
+If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+a) under this License, provided that you make a good faith effort
+to ensure that, in the event an Application does not supply the
+function or data, the facility still operates, and performs
+whatever part of its purpose remains meaningful, or
+
+b) under the GNU GPL, with none of the additional permissions of
+this License applicable to that copy.
+
+3. Object Code Incorporating Material from Library Header Files.
+
+The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+a) Give prominent notice with each copy of the object code that
+the Library is used in it and that the Library and its use are
+covered by this License.
+
+b) Accompany the object code with a copy of the GNU GPL and this
+license document.
+
+4. Combined Works.
+
+You may convey a Combined Work under terms of your choice that, taken
+together, effectively do not restrict modification of the portions of
+the Library contained in the Combined Work and reverse engineering for
+debugging such modifications, if you also do each of the following:
+
+a) Give prominent notice with each copy of the Combined Work that
+the Library is used in it and that the Library and its use are
+covered by this License.
+
+b) Accompany the Combined Work with a copy of the GNU GPL and this
+license document.
+
+c) For a Combined Work that displays copyright notices during
+execution, include the copyright notice for the Library among
+these notices, as well as a reference directing the user to the
+copies of the GNU GPL and this license document.
+
+d) Do one of the following:
+
+0) Convey the Minimal Corresponding Source under the terms of
+this License, and the Corresponding Application Code in a form
+suitable for, and under terms that permit, the user to
+recombine or relink the Application with a modified version of
+the Linked Version to produce a modified Combined Work, in the
+manner specified by section 6 of the GNU GPL for conveying
+Corresponding Source.
+
+1) Use a suitable shared library mechanism for linking with
+the Library. A suitable mechanism is one that (a) uses at run
+time a copy of the Library already present on the user's
+computer system, and (b) will operate properly with a modified
+version of the Library that is interface-compatible with the
+Linked Version.
+
+e) Provide Installation Information, but only if you would
+otherwise be required to provide such information under section 6
+of the GNU GPL, and only to the extent that such information is
+necessary to install and execute a modified version of the
+Combined Work produced by recombining or relinking the Application
+with a modified version of the Linked Version. (If you use option
+4d0, the Installation Information must accompany the Minimal
+Corresponding Source and Corresponding Application Code. If you
+use option 4d1, you must provide the Installation Information in
+the manner specified by section 6 of the GNU GPL for conveying
+Corresponding Source.)
+
+5. Combined Libraries.
+
+You may place library facilities that are a work based on the Library
+side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+a) Accompany the combined library with a copy of the same work
+based on the Library, uncombined with any other library
+facilities, conveyed under the terms of this License.
+
+b) Give prominent notice with the combined library that part of
+it is a work based on the Library, and explaining where to find
+the accompanying uncombined form of the same work.
+
+6. Revised Versions of the GNU Lesser General Public License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+as you received it specifies that a certain numbered version of the
+GNU Lesser General Public License “or any later version” applies to
+it, you have the option of following the terms and conditions either
+of that published version or of any later version published by the
+Free Software Foundation. If the Library as you received it does not
+specify a version number of the GNU Lesser General Public License,
+you may choose any version of the GNU Lesser General Public License
+ever published by the Free Software Foundation.
+
+If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the Library.
+````
+
+## [Open Cascade](https://github.com/Open-Cascade-SAS/OCCT)
+````
+GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+as the successor of the GNU Library Public License, version 2, hence
+the version number 2.1.]
+
+ Preamble
+
+The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+GNU LESSER GENERAL PUBLIC LICENSE
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+"Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+a) The modified work must itself be a software library.
+
+b) You must cause the files modified to carry prominent notices
+stating that you changed the files and the date of any change.
+
+c) You must cause the whole of the work to be licensed at no
+charge to all third parties under the terms of this License.
+
+d) If a facility in the modified Library refers to a function or a
+table of data to be supplied by an application program that uses
+the facility, other than as an argument passed when the facility
+is invoked, then you must make a good faith effort to ensure that,
+in the event an application does not supply such function or
+table, the facility still operates, and performs whatever part of
+its purpose remains meaningful.
+
+(For example, a function in a library to compute square roots has
+a purpose that is entirely well-defined independent of the
+application. Therefore, Subsection 2d requires that any
+application-supplied function or table used by this function must
+be optional: if the application does not supply it, the square
+root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+a) Accompany the work with the complete corresponding
+machine-readable source code for the Library including whatever
+changes were used in the work (which must be distributed under
+Sections 1 and 2 above); and, if the work is an executable linked
+with the Library, with the complete machine-readable "work that
+uses the Library", as object code and/or source code, so that the
+user can modify the Library and then relink to produce a modified
+executable containing the modified Library. (It is understood
+that the user who changes the contents of definitions files in the
+Library will not necessarily be able to recompile the application
+to use the modified definitions.)
+
+b) Use a suitable shared library mechanism for linking with the
+Library. A suitable mechanism is one that (1) uses at run time a
+copy of the library already present on the user's computer system,
+rather than copying library functions into the executable, and (2)
+will operate properly with a modified version of the library, if
+the user installs one, as long as the modified version is
+interface-compatible with the version that the work was made with.
+
+c) Accompany the work with a written offer, valid for at
+least three years, to give the same user the materials
+specified in Subsection 6a, above, for a charge no more
+than the cost of performing this distribution.
+
+d) If distribution of the work is made by offering access to copy
+from a designated place, offer equivalent access to copy the above
+specified materials from the same place.
+
+e) Verify that the user has already received a copy of these
+materials or that you have already sent this user a copy.
+
+For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+a) Accompany the combined library with a copy of the same work
+based on the Library, uncombined with any other library
+facilities. This must be distributed under the terms of the
+Sections above.
+
+b) Give prominent notice with the combined library of the fact
+that part of it is a work based on the Library, and explaining
+where to find the accompanying uncombined form of the same work.
+
+8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Libraries
+
+If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+Copyright (C)
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+Yoyodyne, Inc., hereby disclaims all copyright interest in the
+library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+, 1 April 1990
+Ty Coon, President of Vice
+
+That's all there is to it!
+
+
+OCCT_LGPL_EXCEPTION
+Open CASCADE exception (version 1.0) to GNU LGPL version 2.1.
+
+The object code (i.e. not a source) form of a "work that uses the Library"
+can incorporate material from a header file that is part of the Library.
+As a special exception to the GNU Lesser General Public License version 2.1,
+you may distribute such object code incorporating material from header files
+provided with the Open CASCADE Technology libraries (including code of CDL
+generic classes) under terms of your choice, provided that you give
+prominent notice in supporting documentation to this code that it makes use
+of or is based on facilities provided by the Open CASCADE Technology software.
+````
+
+## [fast_float](https://github.com/fastfloat/fast_float)
+````
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction,
+and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by
+the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all
+other entities that control, are controlled by, or are under common
+control with that entity. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or
+otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity
+exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation
+source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical
+transformation or translation of a Source form, including but
+not limited to compiled object code, generated documentation,
+and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or
+Object form, made available under the License, as indicated by a
+copyright notice that is included in or attached to the work
+(an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object
+form, that is based on (or derived from) the Work and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship. For the purposes
+of this License, Derivative Works shall not include works that remain
+separable from, or merely link (or bind by name) to the interfaces of,
+the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including
+the original version of the Work and any modifications or additions
+to that Work or Derivative Works thereof, that is intentionally
+submitted to Licensor for inclusion in the Work by the copyright owner
+or by an individual or Legal Entity authorized to submit on behalf of
+the copyright owner. For the purposes of this definition, "submitted"
+means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems,
+and issue tracking systems that are managed by, or on behalf of, the
+Licensor for the purpose of discussing and improving the Work, but
+excluding communication that is conspicuously marked or otherwise
+designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity
+on behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the
+Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+(except as stated in this section) patent license to make, have made,
+use, offer to sell, sell, import, and otherwise transfer the Work,
+where such license applies only to those patent claims licensable
+by such Contributor that are necessarily infringed by their
+Contribution(s) alone or by combination of their Contribution(s)
+with the Work to which such Contribution(s) was submitted. If You
+institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work
+or a Contribution incorporated within the Work constitutes direct
+or contributory patent infringement, then any patent licenses
+granted to You under this License for that Work shall terminate
+as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+Work or Derivative Works thereof in any medium, with or without
+modifications, and in Source or Object form, provided that You
+meet the following conditions:
+
+(a) You must give any other recipients of the Work or
+Derivative Works a copy of this License; and
+
+(b) You must cause any modified files to carry prominent notices
+stating that You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works
+that You distribute, all copyright, patent, trademark, and
+attribution notices from the Source form of the Work,
+excluding those notices that do not pertain to any part of
+the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its
+distribution, then any Derivative Works that You distribute must
+include a readable copy of the attribution notices contained
+within such NOTICE file, excluding those notices that do not
+pertain to any part of the Derivative Works, in at least one
+of the following places: within a NOTICE text file distributed
+as part of the Derivative Works; within the Source form or
+documentation, if provided along with the Derivative Works; or,
+within a display generated by the Derivative Works, if and
+wherever such third-party notices normally appear. The contents
+of the NOTICE file are for informational purposes only and
+do not modify the License. You may add Your own attribution
+notices within Derivative Works that You distribute, alongside
+or as an addendum to the NOTICE text from the Work, provided
+that such additional attribution notices cannot be construed
+as modifying the License.
+
+You may add Your own copyright statement to Your modifications and
+may provide additional or different license terms and conditions
+for use, reproduction, or distribution of Your modifications, or
+for any such Derivative Works as a whole, provided Your use,
+reproduction, and distribution of the Work otherwise complies with
+the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+any Contribution intentionally submitted for inclusion in the Work
+by You to the Licensor shall be under the terms and conditions of
+this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify
+the terms of any separate license agreement you may have executed
+with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+names, trademarks, service marks, or product names of the Licensor,
+except as required for reasonable and customary use in describing the
+origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+agreed to in writing, Licensor provides the Work (and each
+Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied, including, without limitation, any warranties or conditions
+of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Work and assume any
+risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+whether in tort (including negligence), contract, or otherwise,
+unless required by applicable law (such as deliberate and grossly
+negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special,
+incidental, or consequential damages of any character arising as a
+result of this License or out of the use or inability to use the
+Work (including but not limited to damages for loss of goodwill,
+work stoppage, computer failure or malfunction, or any and all
+other commercial damages or losses), even if such Contributor
+has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+the Work or Derivative Works thereof, You may choose to offer,
+and charge a fee for, acceptance of support, warranty, indemnity,
+or other liability obligations and/or rights consistent with this
+License. However, in accepting such obligations, You may act only
+on Your own behalf and on Your sole responsibility, not on behalf
+of any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason
+of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+Copyright 2021 The fast_float authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+````
+
+## [fmt](https://github.com/fmtlib/fmt)
+````
+Copyright (c) 2012 - present, Victor Zverovich
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+--- Optional exception to the license ---
+
+As an exception, if, as a result of your compiling your source code, portions of this Software are embedded into a machine-executable object form of such source code, you may redistribute such embedded portions in such object form without including the above copyright and permission notices.
+````
+
+## [GSL](https://github.com/microsoft/GSL)
+````
+Copyright (c) 2015 Microsoft Corporation. All rights reserved.
+
+This code is licensed under the MIT License (MIT).
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+````
+
+## [KDBindings](https://github.com/KDAB/KDBindings)
+````
+MIT License
+
+Copyright (c) 2020-2021 Klarälvdalens Datakonsult AB. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+````
+
+## [magic_enum](https://github.com/Neargye/magic_enum)
+````
+MIT License
+
+Copyright (c) 2019 - 2022 Daniil Goncharov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+````
+
+## [miniply](https://github.com/vilya/miniply)
+````
+MIT License
+
+Copyright (c) 2019 Vilya Harvey
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+````
+
+## [gmio](https://github.com/fougue/gmio)
+````
+BSD 2-Clause License
+
+Copyright (c) 2017, Fougue Ltd.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+````
diff --git a/doc/screenshot_2.png b/doc/screenshot_2.png
index 6b46fbfd..6290a107 100644
Binary files a/doc/screenshot_2.png and b/doc/screenshot_2.png differ
diff --git a/doc/screenshot_3.png b/doc/screenshot_3.png
index a5dcbc3c..e3cb9951 100644
Binary files a/doc/screenshot_3.png and b/doc/screenshot_3.png differ
diff --git a/doc/screenshot_4.png b/doc/screenshot_4.png
index 7d5d56e4..e7ad3fce 100644
Binary files a/doc/screenshot_4.png and b/doc/screenshot_4.png differ
diff --git a/doc/screenshot_6.png b/doc/screenshot_6.png
new file mode 100644
index 00000000..ae064bbc
Binary files /dev/null and b/doc/screenshot_6.png differ
diff --git a/doc/src_modules.odg b/doc/src_modules.odg
index e90a5ce0..c7fdb55a 100644
Binary files a/doc/src_modules.odg and b/doc/src_modules.odg differ
diff --git a/doc/src_modules.png b/doc/src_modules.png
index a21dbdcd..1c73d9f6 100644
Binary files a/doc/src_modules.png and b/doc/src_modules.png differ
diff --git a/i18n/mayo_en.qm b/i18n/mayo_en.qm
index fb879dde..2bd1c216 100644
Binary files a/i18n/mayo_en.qm and b/i18n/mayo_en.qm differ
diff --git a/i18n/mayo_en.ts b/i18n/mayo_en.ts
index 25f56156..43b76ad8 100644
--- a/i18n/mayo_en.ts
+++ b/i18n/mayo_en.ts
@@ -4,45 +4,39 @@
AppModule
-
- Very Coarse
+ Very Coarse
-
- Coarse
+ Coarse
-
- Normal
+ Normal
-
- Precise
+ Precise
-
- Very Precise
+ Very Precise
-
- User Defined
+ User Defined
Mayo::AppModule
-
+
English
-
+
French
@@ -163,28 +157,34 @@
Export
+
- Very Coarse
+ Very Coarse
+
- Coarse
+ Coarse
+
- Normal
+ Normal
+
- Precise
+ Precise
+
- Very Precise
+ Very Precise
+
- User Defined
+ User Defined
@@ -230,66 +230,82 @@
Mesh Defaults
-
+
Import
-
+
Export
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
@@ -322,72 +338,82 @@ If activated, deflection used for the polygonalisation of each edge will be `Cho
Link With Document Selector
-
+
+
+ Force OpenGL Fallback Widget
+
+
+
Quality
-
+
Chordal Deflection
-
+
Angular Deflection
-
+
Relative
-
+
View Navigation Style
-
+
Show Origin Trihedron By Default
-
+
Instant Zoom Factor
+
+ Turn View Angle Increment
+
+
+
Capping
-
+
Capping Hatch
-
+
Color
-
+
Edge Color
-
+
Material
-
+
Show Edges
-
+
Show Nodes
@@ -395,281 +421,508 @@ If activated, deflection used for the polygonalisation of each edge will be `Cho
Mayo::Application
-
+
-
+
-
- Mayo::BRepMeasureError
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Mayo::CliExport
-
+
-
+
-
+
-
+
-
+
- Mayo::DialogAbout
+ Mayo::Command
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
- Mayo::DialogInspectXde
-
-
-
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
- Shape
+
+
+ %1 is the format identifier and %2 is the file filters string
+
-
-
- Color
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
+
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
- Mayo::DialogOptions
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
+ Import
-
-
-
+
+
+
-
-
-
+
+
+
+
-
-
-
+
+
-
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mayo::DialogAbout
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mayo::DialogInspectXde
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Shape
+
+
+
+
+ Color
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mayo::DialogOptions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
@@ -803,37 +1056,37 @@ If activated, deflection used for the polygonalisation of each edge will be `Cho
Mayo::GraphicsMeshObjectDriver
-
+
[Mesh] Wireframe
-
+
[Mesh] Shaded
-
+
[Mesh] Shrink
-
+
Color
-
+
Edge Color
-
+
Show Edges
-
+
Show Nodes
@@ -891,22 +1144,22 @@ If activated, deflection used for the polygonalisation of each edge will be `Cho
Mayo::GraphicsShapeObjectDriver
-
+
[Shape] Wireframe
-
+
[Shape] Hidden Line Removal
-
+
[Shape] Shaded
-
+
[Shape] Shaded With Edges
@@ -914,42 +1167,42 @@ If activated, deflection used for the polygonalisation of each edge will be `Cho
Mayo::IO::DxfReader::Properties
-
+
-
+
-
-
+
+
-
+
-
+
Scaling
-
+
Import annotations
-
+
Group objects by layer
-
+
Font for TEXT objects
@@ -957,69 +1210,69 @@ If activated, deflection used for the polygonalisation of each edge will be `Cho
Mayo::IO::GmioAmfWriter::Properties
-
-
-
-
-
-
+
-
+
-
+
-
-
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
64bit Float Format
-
+
64bit Float Precision
-
+
Create ZIP Archive
-
+
ZIP Entry Filename
-
+
Use ZIP64 extensions
@@ -1129,9 +1382,68 @@ Only applicable if option `{}` is on
- Mayo::IO::OccGltfReader::Properties
+ Mayo::IO::OccCommon
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mayo::IO::OccGltfReader::Properties
+
+
Skip Empty Nodes
@@ -1154,67 +1466,71 @@ Only applicable if option `{}` is on
Mayo::IO::OccGltfWriter::Properties
-
- Coordinates Converter
+ Coordinates Converter
-
+
Transformation Format
-
+
Format
-
+
Force UV Export
-
+
-
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
Node Name Format
-
+
Mesh Name Format
-
+
Embed Textures
-
+
Merge Faces
-
+
Keep 16bit Indices
-
+
@@ -1401,128 +1727,142 @@ The processor also decides to re-compute either the 3D or the 2D curve even if b
Mayo::IO::OccObjWriter::Properties
+
+
+ Coordinates Converter
+
+
+
+
+
+
-
+
-
-
- Coordinates Converter
+
+
+
+
+
+
+
+
Mayo::IO::OccStepReader::Properties
-
+
Product Context
-
+
Assembly Level
-
+
Preferred Shape Representation
-
+
Read Shape Aspect
-
+
Read Names of sub Shapes
-
+
Encoding
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1530,108 +1870,108 @@ This kind of association was used for the representation of hybrid models (i.e.
Mayo::IO::OccStepWriter::Properties
-
+
Schema
-
+
Length Unit
-
+
Assembly Mode
-
+
Mode for Free Vertices
-
+
Write Parametric Curves
-
+
Write Names of sub Shapes
-
+
Author(header)
-
+
Organization(header)
-
+
Originating system(header)
-
+
Description(header)
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1639,23 +1979,38 @@ It can be disabled in order to minimize the size of the resulting file.
Mayo::IO::OccStlWriter::Properties
-
- Target Format
+ Target Format
+
- Text
+ Text
+
- Binary
+ Binary
+
+
+
+ Mayo::IO::OccStlWriterI18N
+
+
+
+ Target Format
+
+
+
+
+
+
Mayo::IO::OccVrmlWriter::Properties
-
+
Shape Representation
@@ -1663,663 +2018,366 @@ It can be disabled in order to minimize the size of the resulting file.
Mayo::IO::PlyWriterI18N
-
+
-
+
Target Format
-
+
Write Colors
-
+
Default Color
-
+
Comment
-
+
-
+
-
-
-
-
- Mayo::IO::System
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Mayo::Main
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- File Path
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Mayo::MainWindow
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Import
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Mayo::IO::System
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+
+
+ Mayo::Main
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
- %1 is the format identifier and %2 is the file filters string
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
+
+ File Path
-
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
+
+ Mayo::MainWindow
-
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
- Mayo::MeasureDisplayI18N
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+ Import
-
-
+
+
-
-
+
+
-
-
+
+
+
-
-
-
+
+
+
-
-
+
+
+
+
+ Mayo::MeasureDisplayI18N
-
- Area
+ Area
Mayo::Mesh_DocumentTreeNodeProperties
-
+
Count Of Nodes
-
+
Count Of Triangles
-
+
Area
-
+
Volume
+
+ Mayo::PointCloud_DocumentTreeNodeProperties
+
+
+
+ Point Count
+
+
+
+
+ Has Colors
+
+
+
+
+ Corner Min
+
+
+
+
+ Corner Max
+
+
Mayo::PropertyEditorI18N
@@ -2331,33 +2389,33 @@ It can be disabled in order to minimize the size of the resulting file.
Mayo::PropertyItemDelegate
-
+
-
+
-
+
-
+
-
-
+
+
-
+
@@ -2494,7 +2552,7 @@ It can be disabled in order to minimize the size of the resulting file.
Mayo::WidgetFileSystem
-
+
@@ -2504,62 +2562,62 @@ Last modified: %3
Mayo::WidgetGuiDocument
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -2758,7 +2816,7 @@ Read: %5
-
+
@@ -2784,15 +2842,30 @@ Read: %5
Mayo::WidgetModelTreeBuilder_Xde
-
+
Name Format Of Assembly Instances
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Mayo::WidgetPropertiesEditor
@@ -2815,82 +2888,82 @@ Read: %5
Mayo::XCaf_DocumentTreeNodeProperties
-
+
Name
-
+
Shape
-
+
XDE Shape Type
-
+
Layer
-
+
Color
-
+
Location
-
+
Centroid
-
+
Area
-
+
Volume
-
+
Material Density
-
+
Material Name
-
+
Product Name
-
+
Product Color
-
+
Product Centroid
-
+
Product Area
-
+
Product Volume
@@ -2919,76 +2992,15 @@ Read: %5
Sub
-
- OccCommon
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
OccStlWriter::Properties
-
- Text
+ Text
-
- Binary
+ Binary
@@ -3162,22 +3174,4 @@ Read: %5
-
- WidgetModelTreeBuilder_Xde
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/i18n/mayo_fr.qm b/i18n/mayo_fr.qm
index aa56f32f..d0002376 100644
Binary files a/i18n/mayo_fr.qm and b/i18n/mayo_fr.qm differ
diff --git a/i18n/mayo_fr.ts b/i18n/mayo_fr.ts
index 1702cfe3..502e1072 100644
--- a/i18n/mayo_fr.ts
+++ b/i18n/mayo_fr.ts
@@ -4,45 +4,39 @@
AppModule
-
- Très grossière
+ Très grossière
-
- Grossière
+ Grossière
-
- Normale
+ Normale
-
- Précise
+ Précise
-
- Très précise
+ Très précise
-
- Custom
+ Custom
Mayo::AppModule
-
+
Anglais
-
+
Français
@@ -203,28 +197,34 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Export
+
- Très grossière
+ Très grossière
+
- Grossière
+ Grossière
+
- Normale
+ Normale
+
- Précise
+ Précise
+
- Très précise
+ Très précise
+
- Custom
+ Custom
@@ -270,42 +270,67 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Maillage par défauts
-
+
Import
-
+
Export
-
+
Langage de l'application. Tout changement sera effectif après redémarrage de l'application
-
+
Dans le cas où plusieurs documents sont ouverts, fait en sort que le document affiché dans la vue 2D correspond à ce qui est sélectionné dans l'arborescence du modèle
-
+
+
+ Force l'utilisation du widget Qt de secours pour le rendu des graphismes OpenGL.
+
+Quand l'option est désactivée alors l'application essaiera d'employer un framebuffer OpenGL pour le rendu, permettant la translucidité du fond des widgets Qt (voir le panneau des outils de mesure). Mais la technique du framebuffer OpenGL peut causer des problèmes pour certains utilisateurs (par ex : fenêtre 3D vide) en particulier sur macOS.
+
+Quand l'option est activée alors l'application utilisera un widget Qt normal pour le rendu OpenGL, solution ayant fait ses preuves et mieux supportée.
+
+Cette option est appliquable seulement avec la version ≥ 7.6 d'OpenCascade. Tout changement sera effectif après redémarrage de l'application
+
+
+
Contrôle la précision du maillage calculé à partir de la forme BRep
-
-
+
+
Pour la tesselation des faces, la déflection chordale limite la distance entre une courbe et sa discrétisation
-
-
+
+
Pour la tesselation des faces, la déflection angulaire limite l'angle entre les segments successifs d'une polyligne
-
+
+ Pour la tesselation des faces, la déflection chordale limite la distance entre une courbe et sa discrétisation
+
+
+
+ Pour la tesselation des faces, la déflection angulaire limite l'angle entre les segments successifs d'une polyligne
+
+
+
@@ -314,22 +339,27 @@ If activated, deflection used for the polygonalisation of each edge will be `Cho
Si actif, la déflection utilisée pour la polygonisation de chaque arête sera de `DéflectionChordale` × `TailleArête`. La déflection utilisée pour les faces sera la déflection maximale de ses arêtes.
-
+
Configuration des raccourcis pour manipuler la vue 3D, permet d'imiter les autres application CAO
-
+
+
+ Incrément angulaire utilisé pour tourner la vue 3D autour de la normale au plan de vue (axe Z de référence)
+
+
+
Montrer/cacher par défaut le trièdre positionné à l'orgine "monde". N'affecte pas la vue 3D des documents actuellement ouverts
-
+
Activer le bouchage des graphismes actuellement coupés
-
+
Activer le hachage texturé pour le bouchage des graphismes actuellement coupés
@@ -364,72 +394,82 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Lier au sélecteur de documents
-
+
+
+ Forcer usage du widget OpenGL de secours
+
+
+
Qualité
-
+
Déflection chordale
-
+
Déflection angulaire
-
+
Relatif
-
+
Style de navigation de la vue
-
+
Afficher le trihèdre Origine par défaut
-
+
Coefficient du zoom instantané
+
+ Incrément de rotation de la vue
+
+
+
Bouchage
-
+
Bouchages avec hachures
-
+
Couleur
-
+
Couleur des arêtes
-
+
Matériau
-
+
Afficher les arêtes
-
+
Afficher les nœuds
@@ -437,12 +477,12 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Mayo::Application
-
+
-
+
@@ -454,49 +494,40 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Mayo::BRepMeasureError
-
- L'entité doit être un sommet
+ L'entité doit être un sommet
-
- L'entité doit être une arête circulaire
+ L'entité doit être une arête circulaire
-
- L'entité doit une forme BREP
+ L'entité doit une forme BREP
-
- Échec du calcul de la distance minimum
+ Échec du calcul de la distance minimum
-
- Toutes les entités doivent être des arêtes
+ Toutes les entités doivent être des arêtes
-
- L'entité doit une arête linéaire
+ L'entité doit une arête linéaire
-
- Toutes les entités doivent être des faces
+ Toutes les entités doivent être des faces
-
- Les entités ne doivent pas être parallèles
+ Les entités ne doivent pas être parallèles
-
- Erreur inconnue
+ Erreur inconnue
@@ -527,6 +558,282 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Export de {} en cours ...
+
+ Mayo::Command
+
+
+
+ Orthographique
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mode
+
+
+
+
+ Montrer le trihèdre Origine
+
+
+
+
+ Montrer/cacher le trihèdre Origine
+
+
+
+
+ Montrer les statistiques de rendu
+
+
+
+
+ Montrer/cacher les statistiques de rendu
+
+
+
+
+ Zoom avant
+
+
+
+
+ Zoom arrière
+
+
+
+
+ Tourner dans le sens anti-horaire
+
+
+
+
+ Tourner dans le sens horaire
+
+
+
+
+ %1 is the format identifier and %2 is the file filters string
+ %1 fichiers (%2)
+
+
+
+
+ Tous les fichiers (*.*)
+
+
+
+
+ Selectionner fichier pièce
+
+
+
+
+
+ Maillage des formes BRep
+
+
+
+
+
+ Durée import: {}ms
+
+
+
+
+ Nouveau
+
+
+
+
+ Nouveau Document
+
+
+
+
+ Anonyme%1
+
+
+
+
+ Ouvrir
+
+
+
+
+ Ouvrir des documents
+
+
+
+
+ Fichiers récents
+
+
+
+
+
+
+
+
+
+ Vider le menu
+
+
+
+
+
+ Importer
+
+
+
+
+ Importer dans le document courant
+
+
+
+
+
+ Exporter les éléments sélectionnées
+
+
+
+
+ Aucun élément sélectionné pour l'export
+
+
+
+
+ Sélection fichier de sortie
+
+
+
+
+ Durée export: {}ms
+
+
+
+
+
+ Fermer "%1"
+
+
+
+
+ Fermer
+
+
+
+
+ Tout fermer
+
+
+
+
+ Fermer tous les documents
+
+
+
+
+
+ Tout fermer sauf document courant
+
+
+
+
+ Tout fermer sauf document courant
+
+
+
+
+ Tout fermer sauf "%1"
+
+
+
+
+ Quitter
+
+
+
+
+ Signaler un bug
+
+
+
+
+ À propos %1
+
+
+
+
+
+ Sauvegarder la vue vers une image
+
+
+
+
+
+ Inspection XDE
+
+
+
+
+
+ Options
+
+
+
+
+ Plein-écran
+
+
+
+
+ Basculer plein-écran/normal
+
+
+
+
+ Montrer le bandeau vertical fixé à gauche
+
+
+
+
+ Montrer/cacher le bandeau vertical fixé à gauche
+
+
+
+
+
+ Document précédent
+
+
+
+
+
+ Document suivant
+
+
+
+ Mayo::CommandCloseCurrentDocument
+
+
+ Fermer "%1"
+
+
+
+ Fermer
+
+
Mayo::DialogAbout
@@ -578,67 +885,77 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
XDE
-
+
-
+
Oui
-
+
Non
-
+
+
+
+
+
+
+
+
+
+
+
Forme
-
+
Couleur
-
+
-
+
-
+
-
+
-
+
-
+
Erreur
-
+
Ce document n'est pas XDE-compatible
-
+
Attributs
@@ -656,66 +973,66 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Restaurer les valeurs par défaut
-
+
%1 / %2
-
+
Échanger
-
+
Charger le fichier ...
-
+
Sauvergarder vers ...
-
-
+
+
Choisir fichier INI
-
-
+
+
Fichiers INI(*.ini)
+
-
-
+
Erreur
-
+
'%1' n'existe pas
-
+
'%1' ne dispose pas des permissions de lecture
-
+
Erreur lors de l'écriture vers '%1'
-
+
Restaurer les valeurs seulement pour la section par défaut
-
+
Restaure les valeurs pour tout le groupe
@@ -849,37 +1166,37 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Mayo::GraphicsMeshObjectDriver
-
+
[Maillage] Filaire
-
+
[Maillage] Ombré
-
+
[Maillage] Rétréci
-
+
Couleur
-
+
Couleur des arêtes
-
+
Montrer les arêtes
-
+
Montrer les nœuds
@@ -937,22 +1254,22 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Mayo::GraphicsShapeObjectDriver
-
+
[Forme] Filaire
-
+
[Forme] Suppression des arêtes cachées
-
+
[Forme] Ombré
-
+
[Forme] Ombré avec arêtes
@@ -960,42 +1277,46 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Mayo::IO::DxfReader::Properties
-
+
Redimensionner les entités selon un facteur d'échelle
-
+
Importer les entités texte et dimension
-
+ Grouper toutes les entités d'une même couche dans une forme compound BREP
+
+
+
+
Grouper toutes les entités d'une même couche dans une forme compound BREP
-
+
Nom de la police de caractères à utiliser lors de la création des formes correspondantes aux entités TEXT
-
+
Mise à l'échelle
-
+
Import des annotations
-
+
Grouper les entitiés par couche
-
+
Police de caractères pour le texte
@@ -1003,44 +1324,42 @@ Si actif, la déflection utilisée pour la polygonisation de chaque arête sera
Mayo::IO::GmioAmfWriter::Properties
-
- Format à utiliser lors de l'écriture des valeurs de type `double`en chaînes de caractères
+ Format à utiliser lors de l'écriture des valeurs de type `double`en chaînes de caractères
-
+
Décimal à virgule flottante (ex: 392,65)
-
+
Notation scientifique (ex: 3,9265E+2)
-
+
Utiliser la notation la plus compacte : décimale ou scientifique
-
- Nombre maximal de chiffres significatifs lors de l'écriture de valeurs de type `double`
+ Nombre maximal de chiffres significatifs lors de l'écriture de valeurs de type `double`
-
+
Écrire le document AMF dans une archive ZIP contenant une entrée de fichier
-
+
Nom de l'entrée du fichier AMF dans l'archive ZIP.
Seulement applicable si l'option `{}` est activée
-
+
Utiliser les extensions de format ZIP64.
@@ -1059,27 +1378,37 @@ Only applicable if option `%1` is on
Seulement applicable si l'option `%1` est activée
-
+
+
+ Format à utiliser lors de l'écriture des valeurs de type `double`en chaînes de caractères
+
+
+
+
+ Nombre maximal de chiffres significatifs lors de l'écriture de valeurs de type `double`
+
+
+
Format nombres flottants 64bit
-
+
Précision nombres flottants 64bit
-
+
Créer une archive ZIP
-
+
Nom du fichier de l'entrée ZIP
-
+
Utiliser les extensions ZIP64
@@ -1203,48 +1532,60 @@ Seulement applicable si l'option `%1` est activée
Mayo::IO::OccCommon
+
+
- Indéfini
+ Indéfini
+
- +Zup
+ +Zup
+
- +Yup
+ +Yup
+
- Micromètre
+ Micromètre
+
- Millimètre
+ Millimètre
+
- Centimètre
+ Centimètre
+
- Mètre
+ Mètre
+
- Kilomètre
+ Kilomètre
+
- Pouce
+ Pouce
+
- Pied
+ Pied
+
- Mile
+ Mile
@@ -1277,67 +1618,75 @@ Seulement applicable si l'option `%1` est activée
Mayo::IO::OccGltfWriter::Properties
-
- Convertisseur de coordonnées
+ Convertisseur de coordonnées
-
+
Format de transformation
-
+
Format
-
+
Forcer l'export UV
-
- Transformation des coordonnées d'OpenCascade vers glTF
+ Transformation des coordonnées d'OpenCascade vers glTF
+
+
+
+
+ Transformation du système de coordonnées source
+
+ Transformation du système de coordonnées cible
+
+
+
Transformation préférée pour l'écriture des fichiers glTF
-
+
Exporter les coordonnées UV même si aucune texture mappée
-
+
Choisir automatiquement la représentation la plus compacte entre Mat4 et TRS
-
+
Mactrice de transformation 4x4
-
+
Transformation décomposée en vecteur de translation, quaternion de rotation et facteur d'échelle (T x R x S)
-
+
Format du nom utilisé pour exporter la hiérarchie de nœuds
-
+
Format du nom utilisé pour exporter la hiérarchie de maillages
-
+
-
+
@@ -1371,7 +1720,7 @@ May reduce JSON size thanks to smaller number of primitive arrays
Peut réduire la taille JSON grâce à une quantité réduite de tableaux de primitives
-
+
-
+
+
+ Système de coordonnées d'entrée
+
+
+
+
+ Système de coordonnées de sortie
+
+
+
Format du nom pour les nœuds
-
+
Format du nom pour les maillages
-
+
Incorporer les textures dans le même fichier cible
-
+
Fusionner les faces
-
+
Utiliser des indices 16bit
-
+
Option prise en charge à partir de OpenCascade ≥ v7.6 [option={}, version actuelle={}]
@@ -1547,127 +1906,141 @@ The processor also decides to re-compute either the 3D or the 2D curve even if b
Mayo::IO::OccObjWriter::Properties
-
-
-
+
+ Convertisseur de coordonnées
+
+
+
+
+ Transformation du système de coordonnées source
+
+
+
+
+ Transformation du système de coordonnées cible
+
+
+
+
+ Système de coordonnées d'entrée
-
-
- Convertisseur de coordonnées
+
+
+ Système de coordonnées de sortie
Mayo::IO::OccStepReader::Properties
-
+
Context du produit
-
+
Niveau assemblage
-
+
Représentation des formes préférée
-
+
Lire l'aspect des formes
-
+
Lire le nom des sous-formes
-
+
Encodage
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1675,108 +2048,108 @@ This kind of association was used for the representation of hybrid models (i.e.
Mayo::IO::OccStepWriter::Properties
-
+
Schéma
-
+
Unité de longueur
-
+
Mode de l'assemblage
-
+
Mode des sommets libres
-
+
Écrire les courbes paramétriques
-
+
Écrire le nom des sous-formes
-
+
Auteur (en-tête)
-
+
Organisation (en-tête)
-
+
Système source (en-tête)
-
+
Description (en-tête)
-
+
Version du schéma à utiliser pour le fichier STEP de sortie
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1784,23 +2157,38 @@ It can be disabled in order to minimize the size of the resulting file.
Mayo::IO::OccStlWriter::Properties
-
- Format cible
+ Format cible
+
- Texte
+ Texte
+
- Binaire
+ Binaire
+
+
+
+ Mayo::IO::OccStlWriterI18N
+
+
+
+ Format cible
+
+
+
+
+
+ Les faces BRep ne sont pas toutes maillées
Mayo::IO::OccVrmlWriter::Properties
-
+
Représentation des formes
@@ -1808,37 +2196,37 @@ It can be disabled in order to minimize the size of the resulting file.
Mayo::IO::PlyWriterI18N
-
+
Texte qui apparaîtra dans l'en-tête
-
+
Format cible
-
+
Écrire la couleur des sommets
-
+
Couleur par défaut
-
+
Commentaire
-
+
Impossible d'ouvrir le fichier
-
+
Boutisme du CPU inconnu
@@ -1935,62 +2323,62 @@ It can be disabled in order to minimize the size of the resulting file.
Mayo, une visionneuse 3D en code libre basée surQt5/OpenCascade
-
+
Thème de l'IHM (classic|dark)
-
+
nom
-
+
Écrit les messages de log dans un fichier de sortie
-
+
Ne pas filtrer les messages de debug dans la version "release"
-
+
Désactiver l'indicateur de progression dans la sortie console (mode CLI seulement)
-
+
files
-
+
Fichiers à ouvrir au démarrage, optionnel
-
+
[fichiers ...]
-
+
Exécuter les tests unitaires et quitter l'application
-
+
Le fichier de configuration OpenCascade n'existe pas ou non lisible [chemin=%1]
-
+
Le fichier de configuration OpenCascade n'a pu être chargé par QSettings [chemin=%1]
-
+
Échec chargement du fichier de traductions [chemin=%1]
@@ -2003,7 +2391,7 @@ It can be disabled in order to minimize the size of the resulting file.
Export de {} en cours ...
-
+
Échec chargement du fichier de configuration [chemin=%1]
@@ -2014,24 +2402,24 @@ It can be disabled in order to minimize the size of the resulting file.
-
+
Fichier de configuration (format INI) à charger au démarrage
-
+
Mayo le visualiseur et convertisseur 3D pour la CAO
-
-
-
+
+
+
-
+
Exporter des fichiers dans un fichier de sortie, répétable selon les différents formats supportés (par exemple -e file.stp -e file.igs ...)
@@ -2056,12 +2444,12 @@ It can be disabled in order to minimize the size of the resulting file.
Export de %1 en cours ...
-
+
Auncun fichier en entrée -> aucun export
-
+
Impossible de charger le thème '%1'
@@ -2129,415 +2517,287 @@ It can be disabled in order to minimize the size of the resulting file.
&Fichier
-
+
&Aide
-
+
&Outils
-
+
F&enêtre
-
+
A&ffichage
-
-
-
-
-
-
- Nouveau
-
-
-
-
-
+ Nouveau
-
-
- Importer
+ Importer
-
- Quitter
+ Quitter
-
- Ouvrir
-
-
-
-
-
+ Ouvrir
-
- À propos de Mayo
+ À propos de Mayo
-
- Signaler un bug
+ Signaler un bug
-
-
+
-
- Sauvegarder la vue vers une image
+ Sauvegarder la vue vers une image
-
- Exporter les éléments sélectionnées
+ Exporter les éléments sélectionnées
-
- Inspection XDE
+ Inspection XDE
-
-
- Document précédent
-
-
-
-
-
+ Document précédent
-
-
- Document suivant
-
-
-
-
-
+ Document suivant
-
- Fermer "%1"
-
-
-
-
-
+ Fermer "%1"
-
- Plein-écran
+ Plein-écran
-
- Basculer plein-écran/normal
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Basculer plein-écran/normal
-
- Fichiers récents
+ Fichiers récents
-
- Montrer le trihèdre Origine
+ Montrer le trihèdre Origine
-
- Montrer/cacher le trihèdre Origine
+ Montrer/cacher le trihèdre Origine
-
- Zoom avant
-
-
-
-
-
+ Zoom avant
-
- Zoom arrière
-
-
-
-
-
+ Zoom arrière
-
- Tout fermer
-
-
-
-
-
+ Tout fermer
-
- Tout fermer sauf "%1"
-
-
-
-
-
+ Tout fermer sauf "%1"
-
- Orthographique
+ Orthographique
-
- Mode
+ Mode
-
- Montrer les statistiques de rendu
+ Montrer les statistiques de rendu
-
- Montrer/cacher les statistiques de rendu
+ Montrer/cacher les statistiques de rendu
-
%1 is the format identifier and %2 is the file filters string
- %1 fichiers (%2)
+ %1 fichiers (%2)
-
- Tous les fichiers (*.*)
+ Tous les fichiers (*.*)
-
- Selectionner fichier pièce
+ Selectionner fichier pièce
-
+
Avertissement
-
-
+
+
Erreur
-
- À propos %1
+ À propos %1
-
- Anonyme%1
+ Anonyme%1
-
-
- Maillage des formes BRep
+ Maillage des formes BRep
-
-
- Durée import: {}ms
+ Durée import: {}ms
-
- Durée export: {}ms
+ Durée export: {}ms
Temps import : %1ms
-
- Sélection fichier de sortie
+ Sélection fichier de sortie
Temps export : %1ms
-
-
+
+
Données
-
+
Graphismes
-
- Fermer %1
+ Fermer %1
-
- Fermer
+ Fermer
-
- Tout fermer sauf %1
+ Tout fermer sauf %1
-
- Tout fermer sauf document courant
-
-
-
-
-
+ Tout fermer sauf document courant
-
- Vider le menu
+ Vider le menu
Mayo::MeasureDisplayI18N
-
- Total
-
-
-
-
-
-
-
-
-
-
+ Total
-
- Diamètre: {0}{1}
-
-
-
-
-
+ Diamètre: {0}{1}
-
- Distance Min: {0}{1}<br>Point1: {2}<br>Point2: {3}
-
-
-
-
-
-
-
-
-
-
-
+ Distance Min: {0}{1}<br>Point1: {2}<br>Point2: {3}
-
- Longueur
+ Longueur
-
- Aire
+ Aire
Mayo::Mesh_DocumentTreeNodeProperties
-
+
Nombre de nœuds
-
+
Nombre de triangles
-
+
Aire
-
+
Volume
+
+ Mayo::PointCloud_DocumentTreeNodeProperties
+
+
+
+ Nombre de points
+
+
+
+
+ Couleurs
+
+
+
+
+ Coin min
+
+
+
+
+ Coin max
+
+
Mayo::PropertyEditorI18N
@@ -2549,33 +2809,33 @@ It can be disabled in order to minimize the size of the resulting file.
Mayo::PropertyItemDelegate
-
+
%1j
-
+
%1h
-
+
%1min
-
+
%1s
-
-
+
+
%1%2
-
+
ERREUR aucune transformation en string pour les propriétés de type '%1'
@@ -2743,7 +3003,7 @@ It can be disabled in order to minimize the size of the resulting file.
Mayo::WidgetFileSystem
-
+
@@ -2757,62 +3017,62 @@ Modifié le: %3 {1
Mayo::WidgetGuiDocument
-
+
Adapter à tout
-
+
Éditer les plans de coupe
-
+
Éclater l'assemblage
-
+
Mesures
-
+
Isométrique
-
+
Arrière
-
+
Devant
-
+
Gauche
-
+
Droit
-
+
Haut
-
+
Bas
-
+
<b>Click gauche</b> : menu déroulant des vues pré-définies
@@ -3043,7 +3303,7 @@ Lu: %5
Unité angle
-
+
Sélectionner les entités à mesurer
@@ -3069,12 +3329,12 @@ Lu: %5
Mayo::WidgetModelTreeBuilder_Xde
-
+
Format des noms d'instance
-
+
Montrer {}
@@ -3083,16 +3343,19 @@ Lu: %5
Montrer %1
+
- Instance
+ Instance
+
- Produit
+ Produit
+
- Les Deux
+ Les Deux
@@ -3116,82 +3379,82 @@ Lu: %5
Mayo::XCaf_DocumentTreeNodeProperties
-
+
Nom
-
+
Forme
-
+
Forme XDE
-
+
Couche
-
+
Couleur
-
+
Placement
-
+
Centroïde
-
+
Aire
-
+
Volume
-
+
Densité matière
-
+
Nom matière
-
+
Nom du produit
-
+
Couleur du produit
-
+
Centroïde du produit
-
+
Aire du produit
-
+
Volume du produit
@@ -3223,73 +3486,59 @@ Lu: %5
OccCommon
-
-
- Indéfini
+ Indéfini
-
- +Zup
+ +Zup
-
- +Yup
+ +Yup
-
- Micromètre
+ Micromètre
-
- Millimètre
+ Millimètre
-
- Centimètre
+ Centimètre
-
- Mètre
+ Mètre
-
- Kilomètre
+ Kilomètre
-
- Pouce
+ Pouce
-
- Pied
+ Pied
-
- Mile
+ Mile
OccStlWriter::Properties
-
- Texte
+ Texte
-
- Binaire
+ Binaire
@@ -3466,19 +3715,16 @@ Lu: %5
WidgetModelTreeBuilder_Xde
-
- Instance
+ Instance
-
- Produit
+ Produit
-
- Les Deux
+ Les Deux
diff --git a/images/themes/classic/turn-ccw.svg b/images/themes/classic/turn-ccw.svg
new file mode 100644
index 00000000..1d43ffcc
--- /dev/null
+++ b/images/themes/classic/turn-ccw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/themes/classic/turn-cw.svg b/images/themes/classic/turn-cw.svg
new file mode 100644
index 00000000..407b77f2
--- /dev/null
+++ b/images/themes/classic/turn-cw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/themes/dark/turn-ccw.svg b/images/themes/dark/turn-ccw.svg
new file mode 100644
index 00000000..1d43ffcc
--- /dev/null
+++ b/images/themes/dark/turn-ccw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/themes/dark/turn-cw.svg b/images/themes/dark/turn-cw.svg
new file mode 100644
index 00000000..407b77f2
--- /dev/null
+++ b/images/themes/dark/turn-cw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/mayo.pro b/mayo.pro
index 1fd67082..770c796f 100644
--- a/mayo.pro
+++ b/mayo.pro
@@ -30,7 +30,7 @@ CONFIG(debug, debug|release) {
DEFINES += \
QT_DISABLE_DEPRECATED_BEFORE=0x050F00 \
- QT_IMPLICIT_QFILEINFO_CONSTRUCTION
+ QT_IMPLICIT_QFILEINFO_CONSTRUCTION \
release_with_debuginfo:msvc {
# https://docs.microsoft.com/en-us/cpp/build/reference/how-to-debug-a-release-build
@@ -42,6 +42,10 @@ msvc {
DEFINES += NOMINMAX
QMAKE_CXXFLAGS += /we4150 # Deletion of pointer to incomplete type 'XXXX'; no destructor called
QMAKE_CXXFLAGS += /std:c++17
+
+ greaterThan(QT_MAJOR_VERSION, 5) {
+ DEFINES += _USE_MATH_DEFINES
+ }
}
gcc|clang {
QMAKE_CXXFLAGS += -std=c++17
@@ -49,14 +53,13 @@ gcc|clang {
clang {
# Silent Clang warnings about instantiation of variable 'Mayo::GenericProperty::TypeName'
QMAKE_CXXFLAGS += -Wno-undefined-var-template
-}
-*clang-libc++* {
# See https://libcxx.llvm.org/docs/UsingLibcxx.html
- LIBS += -lc++fs
+ # LIBS += -lstdc++fs
}
macx {
DEFINES += GL_SILENCE_DEPRECATION
QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.15
+ LIBS += -liconv
# QMAKE_CXXFLAGS += -mmacosx-version-min=10.15
}
win32 {
@@ -69,22 +72,26 @@ INCLUDEPATH += \
HEADERS += \
$$files(src/base/*.h) \
- $$files(src/io_occ/*.h) \
$$files(src/io_dxf/*.h) \
$$files(src/io_image/*.h) \
+ $$files(src/io_occ/*.h) \
+ $$files(src/io_off/*.h) \
$$files(src/io_ply/*.h) \
$$files(src/graphics/*.h) \
$$files(src/gui/*.h) \
+ $$files(src/measure/*.h) \
$$files(src/app/*.h) \
SOURCES += \
$$files(src/base/*.cpp) \
- $$files(src/io_occ/*.cpp) \
$$files(src/io_dxf/*.cpp) \
$$files(src/io_image/*.cpp) \
+ $$files(src/io_occ/*.cpp) \
+ $$files(src/io_off/*.cpp) \
$$files(src/io_ply/*.cpp) \
$$files(src/graphics/*.cpp) \
$$files(src/gui/*.cpp) \
+ $$files(src/measure/*.cpp) \
$$files(src/app/*.cpp) \
\
src/3rdparty/fmt/src/format.cc \
@@ -148,6 +155,10 @@ LIBS += \
-lTKXmlXCAF \
-lTKXSBase \
+minOpenCascadeVersion(7, 7, 0) {
+ LIBS += -lTKXDE
+}
+
# -- IGES support
LIBS += -lTKIGES -lTKXDEIGES
# -- STEP support
@@ -173,6 +184,9 @@ minOpenCascadeVersion(7, 4, 0) {
}
# -- VRML support
LIBS += -lTKVRML
+!minOpenCascadeVersion(7, 7, 0) {
+ SOURCES -= src/io_occ/io_occ_vrml_reader.cpp
+}
# gmio
isEmpty(GMIO_ROOT) {
diff --git a/mayo.qrc b/mayo.qrc
index d59ddd42..aecced61 100644
--- a/mayo.qrc
+++ b/mayo.qrc
@@ -78,5 +78,9 @@
images/themes/dark/reload.svg
images/themes/classic/measure.svg
images/themes/dark/measure.svg
+ images/themes/classic/turn-ccw.svg
+ images/themes/classic/turn-cw.svg
+ images/themes/dark/turn-ccw.svg
+ images/themes/dark/turn-cw.svg
diff --git a/src/3rdparty/commit_kdbindings.txt b/src/3rdparty/commit_kdbindings.txt
new file mode 100644
index 00000000..b4bb8816
--- /dev/null
+++ b/src/3rdparty/commit_kdbindings.txt
@@ -0,0 +1,2 @@
+055b3fa320e98516555e68f36d8529e24ee9d60e
+Branch main
\ No newline at end of file
diff --git a/src/3rdparty/kdbindings/genindex_array.h b/src/3rdparty/kdbindings/genindex_array.h
new file mode 100644
index 00000000..ab5cddfe
--- /dev/null
+++ b/src/3rdparty/kdbindings/genindex_array.h
@@ -0,0 +1,207 @@
+/*
+This code has been adapted from MIT licensed code, originally by Jeremy burns and available at
+https://gist.github.com/jaburns/ca72487198832f6203e831133ffdfff4.
+The original license is provided below:
+
+Copyright 2021 Jeremy Burns
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
+(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace KDBindings {
+
+namespace Private {
+
+struct GenerationalIndex {
+ uint32_t index = 0;
+ uint32_t generation = 0;
+};
+
+class GenerationalIndexAllocator
+{
+ struct AllocatorEntry {
+ bool isLive = false;
+ uint32_t generation = 0;
+ };
+
+ std::vector m_entries;
+ std::vector m_freeIndices;
+
+public:
+ GenerationalIndex allocate()
+ {
+ if (m_freeIndices.size() > 0) {
+ uint32_t index = m_freeIndices.back();
+ m_freeIndices.pop_back();
+
+ m_entries[index].generation += 1;
+ m_entries[index].isLive = true;
+
+ return { index, m_entries[index].generation };
+ } else {
+ // check that we are still within the bounds of uint32_t
+ if (m_entries.size() + 1 >= std::numeric_limits::max()) {
+ throw std::length_error(std::string("Maximum number of values inside GenerationalIndexArray reached: ") + std::to_string(m_entries.size()));
+ }
+
+ m_entries.push_back({ true, 0 });
+ return { static_cast(m_entries.size()) - 1, 0 };
+ }
+ }
+
+ bool deallocate(GenerationalIndex index)
+ {
+ if (isLive(index)) {
+ m_entries[index.index].isLive = false;
+ m_freeIndices.emplace_back(index.index);
+ return true;
+ }
+
+ return false;
+ }
+
+ bool isLive(GenerationalIndex index) const noexcept
+ {
+ return index.index < m_entries.size() &&
+ m_entries[index.index].generation == index.generation &&
+ m_entries[index.index].isLive;
+ }
+};
+
+// A GenerationalIndexArray stores elements in contiguous memory just like an std::vector
+// and also allows items to be retrieved in constant time through indexed access, but it keeps
+// track of the "version"/generation of values at indices so that it can inform an accessor
+// when the item at the index it is trying to access is no longer the item that it wants.
+template
+class GenerationalIndexArray
+{
+ struct Entry {
+ uint32_t generation;
+ T value;
+ };
+
+ // TODO: m_entries never shrinks after an entry has been deleted, it might be
+ // a good idea to add a "trim" function at some point if this becomes an issue
+
+ std::vector> m_entries;
+ GenerationalIndexAllocator m_allocator;
+
+public:
+ // Sets the value at a specific index inside the array
+ void set(const GenerationalIndex index, T &&value)
+ {
+ while (m_entries.size() <= index.index)
+ m_entries.emplace_back(std::nullopt);
+
+#ifndef NDEBUG
+ uint32_t previousGeneration = 0;
+
+ const auto &previousEntry = m_entries[index.index];
+ if (previousEntry)
+ previousGeneration = previousEntry->generation;
+
+ assert(index.generation >= previousGeneration);
+#endif
+
+ m_entries[index.index] = std::optional{ { index.generation, std::move(value) } };
+ }
+
+ // Insert a value at the first free index and get the index back
+ GenerationalIndex insert(T &&value)
+ {
+ const auto index = m_allocator.allocate();
+ set(index, std::move(value));
+ return index;
+ }
+
+ // Erase the value at the specified index and free up the index again
+ void erase(GenerationalIndex index)
+ {
+ if (m_allocator.deallocate(index))
+ m_entries[index.index] = std::nullopt;
+ }
+
+ // Get a pointer to the value at the specified index
+ T *get(GenerationalIndex index)
+ {
+ if (index.index >= m_entries.size())
+ return nullptr;
+
+ auto &entry = m_entries[index.index];
+ if (entry && entry->generation == index.generation) {
+ return &entry->value;
+ }
+
+ return nullptr;
+ }
+
+ // Get a const pointer to the value at the specified index
+ const T *get(GenerationalIndex index) const noexcept
+ {
+ return const_cast(const_cast(this)->get(index));
+ }
+
+ // Erase all the values in the array and thus free up all indices too
+ void clear()
+ {
+ const auto numEntries = entriesSize();
+
+ for (auto i = decltype(numEntries){ 0 }; i < numEntries; ++i) {
+ const auto index = indexAtEntry(i);
+
+ if (index != std::nullopt)
+ erase(*index);
+ }
+ }
+
+ // The number entries currently in the array, not all necessarily correspond to valid indices,
+ // use "indexAtEntry" to translate from an entry index to a optional GenerationalIndex
+ uint32_t entriesSize() const noexcept
+ {
+ // this cast is safe because the allocator checks that we never exceed the capacity of uint32_t
+ return static_cast(m_entries.size());
+ }
+
+ // Convert an entry index into a GenerationalIndex, if possible otherwise returns nullopt
+ std::optional indexAtEntry(uint32_t entryIndex) const
+ {
+ if (entryIndex >= entriesSize())
+ return std::nullopt;
+
+ const auto &entry = m_entries[entryIndex];
+ if (!entry)
+ return std::nullopt;
+
+ GenerationalIndex index = { entryIndex, entry->generation };
+
+ if (m_allocator.isLive(index))
+ return index;
+
+ return std::nullopt;
+ }
+};
+
+} //namespace Private
+
+} // namespace KDBindings
diff --git a/src/3rdparty/kdbindings/signal.h b/src/3rdparty/kdbindings/signal.h
new file mode 100644
index 00000000..c141ee23
--- /dev/null
+++ b/src/3rdparty/kdbindings/signal.h
@@ -0,0 +1,720 @@
+/*
+ This file is part of KDBindings.
+
+ SPDX-FileCopyrightText: 2021-2022 Klarälvdalens Datakonsult AB, a KDAB Group company
+ Author: Sean Harmer
+
+ SPDX-License-Identifier: MIT
+
+ Contact KDAB at for commercial licensing options.
+*/
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+/**
+ * @brief The main namespace of the KDBindings library.
+ *
+ * All public parts of KDBindings are members of this namespace.
+ */
+namespace KDBindings {
+
+template
+class Signal;
+
+namespace Private {
+//
+// This class defines a virtual interface, that the Signal this ConnectionHandle refers
+// to must implement.
+// It allows ConnectionHandle to refer to this non-template class, which then dispatches
+// to the template implementation using virtual function calls.
+// It allows ConnectionHandle to be a non-template class.
+class SignalImplBase
+{
+public:
+ SignalImplBase() = default;
+
+ virtual ~SignalImplBase() = default;
+
+ virtual void disconnect(const GenerationalIndex &id) = 0;
+ virtual bool blockConnection(const GenerationalIndex &id, bool blocked) = 0;
+ virtual bool isConnectionActive(const GenerationalIndex &id) const = 0;
+ virtual bool isConnectionBlocked(const GenerationalIndex &id) const = 0;
+};
+
+} // namespace Private
+
+/**
+ * @brief A ConnectionHandle represents the connection of a Signal
+ * to a slot (i.e. a function that is called when the Signal is emitted).
+ *
+ * It is returned from a Signal when a connection is created and used to
+ * manage the connection by disconnecting, (un)blocking it and checking its state.
+ *
+ * To make sure a Connection to an object is disconnected correctly, consider
+ * storing a ScopedConnection to its ConnectionHandle inside the object.
+ **/
+class ConnectionHandle
+{
+public:
+ /**
+ * A ConnectionHandle can be default constructed.
+ * In this case the ConnectionHandle will not reference any active connection (i.e. isActive() will return false),
+ * and not belong to any Signal.
+ **/
+ ConnectionHandle() = default;
+
+ /**
+ * A ConnectionHandle can be copied.
+ **/
+ ConnectionHandle(const ConnectionHandle &) = default;
+ ConnectionHandle &operator=(const ConnectionHandle &) = default;
+
+ /**
+ * A ConnectionHandle can be moved.
+ **/
+ ConnectionHandle(ConnectionHandle &&) = default;
+ ConnectionHandle &operator=(ConnectionHandle &&) = default;
+
+ /**
+ * Disconnect the slot.
+ *
+ * When this function is called, the function that was passed to Signal::connect
+ * to create this ConnectionHandle will no longer be called when the Signal is emitted.
+ *
+ * If the ConnectionHandle is not active or the connection has already been disconnected,
+ * nothing happens.
+ *
+ * After this call, the ConnectionHandle will be inactive (i.e. isActive() returns false)
+ * and will no longer belong to any Signal (i.e. belongsTo returns false).
+ **/
+ void disconnect()
+ {
+ if (auto shared_impl = checkedLock()) {
+ shared_impl->disconnect(m_id);
+ }
+ // ConnectionHandle is no longer active;
+ m_signalImpl.reset();
+ }
+
+ /**
+ * Check whether the connection of this ConnectionHandle is active.
+ *
+ * @return true if the ConnectionHandle refers to an active Signal
+ * and the connection was not disconnected previously, false otherwise.
+ **/
+ bool isActive() const
+ {
+ return static_cast(checkedLock());
+ }
+
+ /**
+ * Sets the block state of the connection.
+ * If a connection is blocked, emitting the Signal will no longer call this
+ * connections slot, until the connection is unblocked.
+ *
+ * Behaves the same as calling Signal::blockConnection with this
+ * ConnectionHandle as argument.
+ *
+ * To temporarily block a connection, consider using an instance of ConnectionBlocker,
+ * which offers a RAII-style implementation that makes sure the connection is always
+ * returned to its original state.
+ *
+ * @param blocked The new blocked state of the connection.
+ * @return whether the connection was previously blocked.
+ * @throw std::out_of_range Throws if the connection is not active (i.e. isActive() returns false).
+ **/
+ bool block(bool blocked)
+ {
+ if (auto shared_impl = checkedLock()) {
+ return shared_impl->blockConnection(m_id, blocked);
+ }
+ throw std::out_of_range("Cannot block a non-active connection!");
+ }
+
+ /**
+ * Checks whether the connection is currently blocked.
+ *
+ * To change the blocked state of a connection, call ConnectionHandle::block.
+ *
+ * @return whether the connection is currently blocked.
+ **/
+ bool isBlocked() const
+ {
+ if (auto shared_impl = checkedLock()) {
+ return shared_impl->isConnectionBlocked(m_id);
+ }
+ throw std::out_of_range("Cannot check whether a non-active connection is blocked!");
+ }
+
+ /**
+ * Check whether this ConnectionHandle belongs to the given Signal.
+ *
+ * @return true if this ConnectionHandle refers to a connection within the given Signal
+ **/
+ template
+ bool belongsTo(const Signal &signal) const
+ {
+ auto shared_impl = m_signalImpl.lock();
+ return shared_impl && shared_impl == std::static_pointer_cast(signal.m_impl);
+ }
+
+private:
+ template
+ friend class Signal;
+
+ std::weak_ptr m_signalImpl;
+ Private::GenerationalIndex m_id;
+
+ // private, so it is only available from Signal
+ ConnectionHandle(std::weak_ptr signalImpl, Private::GenerationalIndex id)
+ : m_signalImpl{ std::move(signalImpl) }, m_id{ std::move(id) }
+ {
+ }
+
+ // Checks that the weak_ptr can be locked and that the connection is
+ // still active
+ std::shared_ptr checkedLock() const
+ {
+ auto shared_impl = m_signalImpl.lock();
+ if (shared_impl && shared_impl->isConnectionActive(m_id)) {
+ return shared_impl;
+ }
+ return nullptr;
+ }
+};
+
+/**
+ * @brief A Signal provides a mechanism for communication between objects.
+ *
+ * KDBindings::Signal recreates the Qt's Signals & Slots mechanism in pure C++17.
+ * A Signal can be used to notify any number of slots that a certain event has occurred.
+ *
+ * The slot can be almost any callable object, including member functions and lambdas.
+ *
+ * This connection happens in a type-safe manner, as a slot can only be connected to
+ * a Signal when the arguments of the slot match the values the Signal emits.
+ *
+ * The Args type parameter pack describe which value types the Signal will emit.
+ *
+ * Examples:
+ * - @ref 01-simple-connection/main.cpp
+ * - @ref 02-signal-member/main.cpp
+ * - @ref 03-member-arguments/main.cpp
+ * - @ref 07-advanced-connections/main.cpp
+ */
+template
+class Signal
+{
+ static_assert(
+ std::conjunction>...>::value,
+ "R-value references are not allowed as Signal parameters!");
+
+ // The Signal::Impl class exists, so Signals can be implemented in a PIMPL-like way.
+ // This allows us to easily move Signals without losing their ConnectionHandles, as well as
+ // making an unconnected Signal only sizeof(shared_ptr).
+ class Impl : public Private::SignalImplBase
+ {
+ public:
+ Impl() noexcept { }
+
+ ~Impl() noexcept { }
+
+ // Signal::Impls are not copyable
+ Impl(Impl const &other) = delete;
+ Impl &operator=(Impl const &other) = delete;
+
+ // Signal::Impls are not moveable, this would break the ConnectionHandles
+ Impl(Impl &&other) = delete;
+ Impl &operator=(Impl &&other) = delete;
+
+ // Connects a std::function to the signal. The returned
+ // value can be used to disconnect the function again.
+ Private::GenerationalIndex connect(std::function const &slot)
+ {
+ return m_connections.insert({ slot });
+ }
+
+ // Disconnects a previously connected function
+ void disconnect(const Private::GenerationalIndex &id) override
+ {
+ m_connections.erase(id);
+ }
+
+ // Disconnects all previously connected functions
+ void disconnectAll()
+ {
+ m_connections.clear();
+ }
+
+ bool blockConnection(const Private::GenerationalIndex &id, bool blocked) override
+ {
+ Connection *connection = m_connections.get(id);
+ if (connection) {
+ const bool wasBlocked = connection->blocked;
+ connection->blocked = blocked;
+ return wasBlocked;
+ } else {
+ throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
+ }
+ }
+
+ bool isConnectionActive(const Private::GenerationalIndex &id) const override
+ {
+ return m_connections.get(id);
+ }
+
+ bool isConnectionBlocked(const Private::GenerationalIndex &id) const override
+ {
+ auto connection = m_connections.get(id);
+ if (connection) {
+ return connection->blocked;
+ } else {
+ throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
+ }
+ }
+
+ // Calls all connected functions
+ void emit(Args... p) const
+ {
+ const auto numEntries = m_connections.entriesSize();
+
+ // This loop can tolerate signal handles being disconnected inside a slot,
+ // but adding new connections to a signal inside a slot will still be undefined behaviour
+ for (auto i = decltype(numEntries){ 0 }; i < numEntries; ++i) {
+ const auto index = m_connections.indexAtEntry(i);
+
+ if (index) {
+ const auto con = m_connections.get(*index);
+
+ if (!con->blocked)
+ con->slot(p...);
+ }
+ }
+ }
+
+ private:
+ struct Connection {
+ std::function slot;
+ bool blocked{ false };
+ };
+ mutable Private::GenerationalIndexArray m_connections;
+ };
+
+public:
+ /** Signals are default constructible */
+ Signal() = default;
+
+ /**
+ * Signals cannot be copied.
+ **/
+ Signal(const Signal &) = delete;
+ Signal &operator=(Signal const &other) = delete;
+
+ /** Signals can be moved */
+ Signal(Signal &&other) noexcept = default;
+ Signal &operator=(Signal &&other) noexcept = default;
+
+ /**
+ * A signal disconnects all slots when it is destructed
+ *
+ * Therefore, all active ConnectionHandles that belonged to this Signal
+ * will no longer be active (i.e. ConnectionHandle::isActive will return false).
+ */
+ ~Signal()
+ {
+ disconnectAll();
+ }
+
+ /**
+ * Connects a std::function to the signal.
+ *
+ * When emit() is called on the Signal, the functions will be called with
+ * the arguments provided to emit().
+ *
+ * @return An instance of ConnectionHandle, that can be used to disconnect
+ * or temporarily block the connection.
+ */
+ ConnectionHandle connect(std::function const &slot)
+ {
+ ensureImpl();
+
+ return ConnectionHandle{ m_impl, m_impl->connect(slot) };
+ }
+
+ /**
+ * A template overload of Signal::connect that makes it easier to connect arbitrary functions to this
+ * Signal.
+ * It connects a function to this Signal, binds any provided arguments to that function and discards
+ * any values emitted by this Signal that aren't needed by the resulting function.
+ *
+ * This is especially useful for connecting member functions to signals.
+ *
+ * Examples:
+ * @code
+ * Signal signal;
+ * std::vector numbers{ 1, 2, 3 };
+ * bool emitted = false;
+ *
+ * // disambiguation necessary, as push_back is overloaded.
+ * void (std::vector::*push_back)(const int &) = &std::vector::push_back;
+ * signal.connect(push_back, &numbers);
+ *
+ * // this slot doesn't require the int argument, so it will be discarded.
+ * signal.connect([&emitted]() { emitted = true; });
+ *
+ * signal.emit(4); // Will add 4 to the vector and set emitted to true
+ * @endcode
+ *
+ * For more examples see the @ref 07-advanced-connections/main.cpp example.
+ *
+ * @return An instance of a Signal::ConnectionHandle that refers to this connection.
+ * Warning: When connecting a member function you must use the returned ConnectionHandle
+ * to disconnect when the object containing the slot goes out of scope!
+ **/
+ // The enable_if_t makes sure that this connect function specialization is only
+ // available if we provide a function that cannot be otherwise converted to a
+ // std::function, as it otherwise tries to take precedence
+ // over the normal connect function.
+ template>>, std::integral_constant>>>
+ ConnectionHandle connect(Func &&slot, FuncArgs &&...args)
+ {
+ std::function bound = Private::bind_first(std::forward(slot), std::forward(args)...);
+ return connect(bound);
+ }
+
+ /**
+ * Disconnect a previously connected slot.
+ *
+ * After the slot was successfully disconnected, the ConnectionHandle will no
+ * longer be active. (i.e. ConnectionHandle::isActive will return false).
+ *
+ * @throw std::out_of_range - If the ConnectionHandle does not belong to this
+ * Signal (i.e. ConnectionHandle::belongsTo returns false).
+ */
+ void disconnect(const ConnectionHandle &handle)
+ {
+ if (m_impl && handle.belongsTo(*this)) {
+ m_impl->disconnect(handle.m_id);
+ // TODO check if Impl is now empty and reset
+ } else {
+ throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
+ }
+ }
+
+ /**
+ * Disconnect all previously connected functions.
+ *
+ * All currently active ConnectionHandles that belong to this Signal will no
+ * longer be active afterwards. (i.e. ConnectionHandle::isActive will return false).
+ */
+ void disconnectAll()
+ {
+ if (m_impl) {
+ m_impl->disconnectAll();
+ // Once all connections are disconnected, we can release ownership of the Impl.
+ // This does not destroy the Signal itself, just the Impl object.
+ // If another slot is connected, another Impl object will be constructed.
+ m_impl.reset();
+ }
+ // If m_impl is nullptr, we don't have any connections to disconnect
+ }
+
+ /**
+ * Sets the block state of the connection.
+ * If a connection is blocked, emitting the Signal will no longer call this
+ * connections slot, until the connection is unblocked.
+ *
+ * ConnectionHandle::block can be used as an alternative.
+ *
+ * To temporarily block a connection, consider using an instance of ConnectionBlocker,
+ * which offers a RAII-style implementation that makes sure the connection is always
+ * returned to its original state.
+ *
+ * @param blocked Whether the connection should be blocked from now on.
+ * @param handle The ConnectionHandle to block.
+ * @return Whether the connection was previously blocked.
+ * @throw std::out_of_range - If the ConnectionHandle does not belong to this
+ * Signal (i.e. ConnectionHandle::belongsTo returns false).
+ */
+ bool blockConnection(const ConnectionHandle &handle, bool blocked)
+ {
+ if (m_impl && handle.belongsTo(*this)) {
+ return m_impl->blockConnection(handle.m_id, blocked);
+ } else {
+ throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
+ }
+ }
+
+ /**
+ * Checks whether the connection is currently blocked.
+ *
+ * To change the blocked state of a connection, call blockConnection().
+ *
+ * @return Whether the connection is currently blocked
+ * @throw std::out_of_range - If the ConnectionHandle does not belong to this
+ * Signal (i.e. ConnectionHandle::belongsTo returns false).
+ */
+ bool isConnectionBlocked(const ConnectionHandle &handle) const
+ {
+ assert(handle.belongsTo(*this));
+ if (!m_impl) {
+ throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
+ }
+
+ return m_impl->isConnectionBlocked(handle.m_id);
+ }
+
+ /**
+ * Emits the Signal, which causes all connected slots to be called,
+ * as long as they are not blocked.
+ *
+ * The arguments provided to emit will be passed to each slot by copy,
+ * therefore consider using (const) references as the Args to the Signal
+ * wherever possible.
+ *
+ * Note: Slots may disconnect themselves during an emit, however it is
+ * undefined whether a slot that is connected during the emit function
+ * of the Signal will also be called during this emit, or only at the next
+ * emit.
+ */
+ void emit(Args... p) const
+ {
+ if (m_impl)
+ m_impl->emit(p...);
+
+ // if m_impl is nullptr, we don't have any slots connected, don't bother emitting
+ }
+
+private:
+ friend class ConnectionHandle;
+
+ void ensureImpl()
+ {
+ if (!m_impl) {
+ m_impl = std::make_shared();
+ }
+ }
+
+ // shared_ptr is used here instead of unique_ptr, so ConnectionHandle instances can
+ // use a weak_ptr to check if the Signal::Impl they reference is still alive.
+ //
+ // This makes Signals easily copyable in theory, but the semantics of this are unclear.
+ // Copying could either simply copy the shared_ptr, which means the copy would share
+ // the connections of the original, which is possibly unintuitive, or the Impl would
+ // have to be copied as well.
+ // This would however leave connections without handles to disconnect them.
+ // So copying is forbidden for now.
+ //
+ // Think of this shared_ptr more like a unique_ptr with additional weak_ptr's
+ // in ConnectionHandle that can check whether the Impl object is still alive.
+ mutable std::shared_ptr m_impl;
+};
+
+/**
+ * @brief A ScopedConnection is a RAII-style way to make sure a Connection is disconnected.
+ *
+ * When the ScopedConnections scope ends, the connection this ScopedConnection guards will be disconnected.
+ *
+ * Example:
+ * - @ref 08-managing-connections/main.cpp
+ */
+class ScopedConnection
+{
+public:
+ /**
+ * @brief A ScopedConnection can be default constructed
+ *
+ * A default constructed ScopedConnection has no connection to guard.
+ * Therefore it does nothing when it is destructed, unless a ConnectionHandle is assigned to it.
+ */
+ ScopedConnection() = default;
+
+ /** A ScopedConnection can be move constructed */
+ ScopedConnection(ScopedConnection &&) = default;
+
+ /** A ScopedConnection cannot be copied */
+ ScopedConnection(const ScopedConnection &) = delete;
+ /** A ScopedConnection cannot be copied */
+ ScopedConnection &operator=(const ScopedConnection &) = delete;
+
+ /** A ScopedConnection can be move assigned */
+ ScopedConnection &operator=(ScopedConnection &&other)
+ {
+ m_connection.disconnect();
+ m_connection = std::move(other.m_connection);
+ return *this;
+ }
+
+ /**
+ * A ScopedConnection can be constructed from a ConnectionHandle
+ */
+ ScopedConnection(ConnectionHandle &&h)
+ : m_connection(std::move(h))
+ {
+ }
+
+ /**
+ * A ScopedConnection can be assigned from a ConnectionHandle
+ */
+ ScopedConnection &operator=(ConnectionHandle &&h)
+ {
+ return *this = ScopedConnection(std::move(h));
+ }
+
+ /**
+ * @return the handle to the connection this instance is managing
+ */
+ ConnectionHandle &handle()
+ {
+ return m_connection;
+ }
+
+ /**
+ * @overload
+ */
+ const ConnectionHandle &handle() const
+ {
+ return m_connection;
+ }
+
+ /**
+ * Convenience access to the underlying ConnectionHandle using the `->` operator.
+ */
+ ConnectionHandle *operator->()
+ {
+ return &m_connection;
+ }
+
+ /**
+ * @overload
+ */
+ const ConnectionHandle *operator->() const
+ {
+ return &m_connection;
+ }
+
+ /**
+ * When a ConnectionHandle is destructed it disconnects the connection it guards.
+ */
+ ~ScopedConnection()
+ {
+ m_connection.disconnect();
+ }
+
+private:
+ ConnectionHandle m_connection;
+};
+
+/**
+ * @brief A ConnectionBlocker is a convenient RAII-style mechanism for temporarily blocking a connection.
+ *
+ * When a ConnectionBlocker is constructed, it will block the connection.
+ *
+ * When it is destructed, it will return the connection to the blocked state it was in
+ * before the ConnectionBlocker was constructed.
+ *
+ * Example:
+ * - @ref 08-managing-connections/main.cpp
+ */
+class ConnectionBlocker
+{
+public:
+ /**
+ * Constructs a new ConnectionBlocker and blocks the connection this ConnectionHandle
+ * refers to.
+ *
+ * @throw std::out_of_range If the connection is not active (i.e. ConnectionHandle::isActive() returns false).
+ */
+ explicit ConnectionBlocker(const ConnectionHandle &handle)
+ : m_handle{ handle }
+ {
+ m_wasBlocked = m_handle.block(true);
+ }
+
+ /**
+ * Destructs the ConnectionBlocker and returns the connection into the blocked state it was in
+ * before the ConnectionBlocker was constructed.
+ */
+ ~ConnectionBlocker()
+ {
+ m_handle.block(m_wasBlocked);
+ }
+
+private:
+ ConnectionHandle m_handle;
+ bool m_wasBlocked{ false };
+};
+
+/**
+ * @example 01-simple-connection/main.cpp
+ *
+ * A simple example of how to create a KDBindings::Signal and connect a lambda to it.
+ *
+ * The output of this example is:
+ * ```
+ * The answer: 42
+ * ```
+ */
+
+/**
+ * @example 02-signal-member/main.cpp
+ *
+ * An example of how to connect a member function to a KDBindings::Signal.
+ *
+ * The output of this example is:
+ * ```
+ * Hello World!
+ * ```
+ */
+
+/**
+ * @example 03-member-arguments/main.cpp
+ *
+ * An example of how to connect a member function with arguments to a KDBindings::Signal.
+ *
+ * The output of this example is:
+ * ```
+ * Bob received: Have a nice day!
+ * Alice received: Thank you!
+ * ```
+ */
+
+/**
+ * @example 07-advanced-connections/main.cpp
+ *
+ * An example of how to use the KDBindings::Signal::connect() overloaded function for advanced slot connections.
+ *
+ * The output of this example is:
+ * ```
+ * Hello World!
+ * Emitted value: 5
+ * true
+ * ```
+ */
+
+/**
+ * @example 08-managing-connections/main.cpp
+ *
+ * An example of how to use a ScopedConnection and ConnectionBlocker to manage
+ * when a Connection is disconnected or blocked.
+ *
+ * Expected output:
+ * ```
+ * Guard is connected: 1
+ * Connection is not blocked: 3
+ * Connection is not blocked: 5
+ * ```
+ */
+
+} // namespace KDBindings
diff --git a/src/3rdparty/kdbindings/utils.h b/src/3rdparty/kdbindings/utils.h
new file mode 100644
index 00000000..f9ef8643
--- /dev/null
+++ b/src/3rdparty/kdbindings/utils.h
@@ -0,0 +1,169 @@
+/*
+ This file is part of KDBindings.
+
+ SPDX-FileCopyrightText: 2021-2022 Klarälvdalens Datakonsult AB, a KDAB Group company
+ Author: Leon Matthes
+
+ SPDX-License-Identifier: MIT
+
+ Contact KDAB at for commercial licensing options.
+*/
+#pragma once
+
+#include
+#include
+#include
+
+namespace KDBindings {
+
+/**
+* The contents of this namespace may only be accessed by the implementation of KDBindings, they
+* are not part of KDBindings public API and may be altered at any time and provide no guarantees
+* of any kind when used directly.
+**/
+namespace Private {
+
+// ------------------------ get_arity --------------------------
+// get_arity is a template function that returns the number of arguments
+// of (almost) any callable object.
+// The easiest way is to simply call get_arity() for callable type T.
+// It needs to be constexpr in order so it can be used in template arguments.
+
+// To overload get_arity, it needs a marker type, as C++ doesn't allow partial
+// function specialization.
+template
+struct TypeMarker {
+ constexpr TypeMarker() = default;
+};
+
+// base implementation of get_arity refers to specialized implementations for each
+// type of callable object by using the overload for its specialized TypeMarker.
+template
+constexpr size_t get_arity()
+{
+ return get_arity(TypeMarker>{});
+}
+
+// Syntactic sugar version of get_arity, allows to pass any callable object
+// to get_arity, instead of having to pass its decltype as a template argument.
+template
+constexpr size_t get_arity(const T &)
+{
+ return get_arity();
+}
+
+// The arity of a function pointer is simply its number of arguments.
+template
+constexpr size_t get_arity(TypeMarker)
+{
+ return sizeof...(Arguments);
+}
+
+template
+constexpr size_t get_arity(TypeMarker)
+{
+ return sizeof...(Arguments);
+}
+
+// The arity of a generic callable object is the arity of its operator() - 1, as the this
+// pointer is already known for such an object.
+template
+constexpr size_t get_arity(TypeMarker)
+{
+ return get_arity(TypeMarker{}) - 1;
+}
+
+// Macro to help define most combinations of possible member function qualifiers.
+// Add + 1 to sizeof...(Arguments) here as the "this" pointer is an implicit argument to any member function.
+#define KDBINDINGS_DEFINE_MEMBER_GET_ARITY(MODIFIERS) \
+ template \
+ constexpr size_t get_arity(::KDBindings::Private::TypeMarker) \
+ { \
+ return sizeof...(Arguments) + 1; \
+ }
+
+// Define the get_arity version without modifiers without using the macro.
+// MSVC otherwise complains about a call to the macro with too few arguments
+template
+constexpr size_t get_arity(::KDBindings::Private::TypeMarker)
+{
+ return sizeof...(Arguments) + 1;
+}
+
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(&)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const &)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(&&)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const &&)
+
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile &)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const &)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile &&)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const &&)
+
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(noexcept)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const noexcept)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(&noexcept)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const &noexcept)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(&&noexcept)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const &&noexcept)
+
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile noexcept)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const noexcept)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile &noexcept)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const &noexcept)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile &&noexcept)
+KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const &&noexcept)
+
+// -------------------- placeholder and bind_first ---------------------
+// Inspired by https://gist.github.com/engelmarkus/fc1678adbed1b630584c90219f77eb48
+// A placeholder provides a way to construct something equivalent to a std::placeholders::_N
+// with N as a template argument.
+//
+// Note: As placeholders start at 1, therefore placeholder<0> is NOT a valid placeholder.
+template
+struct placeholder {
+};
+
+template
+auto bind_first_helper(std::index_sequence, Func &&fun, Args... args)
+{
+ return std::bind(std::forward(fun), std::forward(args)..., placeholder{}...);
+}
+
+// bind_first binds the first arguments to the callable object (i.e. function) to the values provided by args.
+// The return value is a new function taking get_arity - sizeof...(Args) many arguments, with the first
+// sizeof...(Args) arguments bound to the values of args.
+// This is different to a call with std::bind(fun, args...), as the callable object created by std::bind would
+// in this case now take zero arguments, whilst bind_first still expects the remaining arguments to be provided
+//
+// For now, providing instances of std::placeholders in Args is not allowed, as the implications of this are
+// unclear if sizeof...(Args) != get_arity. The enable_if_t makes sure none of the Args value is a placeholder.
+//
+// In the future, we could provide another overload of this function that allows placeholders, as long as all arguments
+// are bound.
+template<
+ typename Func,
+ typename... Args,
+ /*Disallow any placeholder arguments, they would mess with the number and ordering of required and bound arguments, and are, for now, unsupported*/
+ typename = std::enable_if_t>...>>>
+auto bind_first(Func &&fun, Args &&...args)
+{
+ return bind_first_helper(std::make_index_sequence() - sizeof...(Args)>{}, std::forward(fun), std::forward(args)...);
+}
+
+} // namespace Private
+
+} // namespace KDBindings
+
+namespace std {
+
+// This allows a placeholder to be used as a replacement of a std::placeholders.
+template
+struct is_placeholder>
+ : integral_constant {
+};
+
+} // namespace std
diff --git a/src/app/app_context.cpp b/src/app/app_context.cpp
new file mode 100644
index 00000000..a4778116
--- /dev/null
+++ b/src/app/app_context.cpp
@@ -0,0 +1,129 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#include "app_context.h"
+
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+#include "widget_gui_document.h"
+
+namespace Mayo {
+
+AppContext::AppContext(MainWindow* wnd)
+ : m_wnd(wnd)
+{
+ QObject::connect(
+ m_wnd->m_ui->combo_GuiDocuments, qOverload(&QComboBox::currentIndexChanged),
+ this, &AppContext::onCurrentDocumentIndexChanged
+ );
+}
+
+GuiApplication* AppContext::guiApp() const
+{
+ return m_wnd->m_guiApp;
+}
+
+TaskManager* AppContext::taskMgr() const
+{
+ return &m_wnd->m_taskMgr;
+}
+
+QWidget* AppContext::widgetMain() const
+{
+ return m_wnd;
+}
+
+QWidget* AppContext::widgetLeftSidebar() const
+{
+ return m_wnd->m_ui->widget_Left;
+}
+
+IAppContext::ModeWidgetMain AppContext::modeWidgetMain() const
+{
+ auto widget = m_wnd->m_ui->stack_Main->currentWidget();
+ if (widget == m_wnd->m_ui->page_MainHome)
+ return ModeWidgetMain::Home;
+ else if (widget == m_wnd->m_ui->page_MainControl)
+ return ModeWidgetMain::Documents;
+
+ return ModeWidgetMain::Unknown;
+}
+
+V3dViewController* AppContext::v3dViewController(const GuiDocument* guiDoc) const
+{
+ auto widgetDoc = this->findWidgetGuiDocument([=](WidgetGuiDocument* candidate) {
+ return candidate->guiDocument() == guiDoc;
+ });
+ return widgetDoc ? widgetDoc->controller() : nullptr;
+}
+
+int AppContext::findDocumentIndex(Document::Identifier docId) const
+{
+ int index = -1;
+ auto widgetDoc = this->findWidgetGuiDocument([&](WidgetGuiDocument* candidate) {
+ ++index;
+ return candidate->documentIdentifier() == docId;
+ });
+ return widgetDoc ? index : -1;
+}
+
+Document::Identifier AppContext::findDocumentFromIndex(int index) const
+{
+ auto widgetDoc = m_wnd->widgetGuiDocument(index);
+ return widgetDoc ? widgetDoc->documentIdentifier() : -1;
+}
+
+Document::Identifier AppContext::currentDocument() const
+{
+ const int index = m_wnd->m_ui->combo_GuiDocuments->currentIndex();
+ auto widgetDoc = m_wnd->widgetGuiDocument(index);
+ return widgetDoc ? widgetDoc->documentIdentifier() : -1;
+}
+
+void AppContext::setCurrentDocument(Document::Identifier docId)
+{
+ auto widgetDoc = this->findWidgetGuiDocument([=](WidgetGuiDocument* widgetDoc) {
+ return widgetDoc->documentIdentifier() == docId;
+ });
+ const int docIndex = m_wnd->m_ui->stack_GuiDocuments->indexOf(widgetDoc);
+ m_wnd->m_ui->combo_GuiDocuments->setCurrentIndex(docIndex);
+}
+
+void AppContext::updateControlsEnabledStatus()
+{
+ m_wnd->updateControlsActivation();
+}
+
+void AppContext::deleteDocumentWidget(const DocumentPtr& doc)
+{
+ QWidget* widgetDoc = this->findWidgetGuiDocument([&](WidgetGuiDocument* widgetDoc) {
+ return widgetDoc->documentIdentifier() == doc->identifier();
+ });
+ if (widgetDoc) {
+ m_wnd->m_ui->stack_GuiDocuments->removeWidget(widgetDoc);
+ widgetDoc->deleteLater();
+ }
+}
+
+WidgetGuiDocument* AppContext::findWidgetGuiDocument(std::function fn) const
+{
+ const int widgetCount = m_wnd->m_ui->stack_GuiDocuments->count();
+ for (int i = 0; i < widgetCount; ++i) {
+ auto candidate = m_wnd->widgetGuiDocument(i);
+ if (candidate && fn(candidate))
+ return candidate;
+ }
+
+ return nullptr;
+}
+
+void AppContext::onCurrentDocumentIndexChanged(int docIndex)
+{
+ auto widgetDoc = m_wnd->widgetGuiDocument(docIndex);
+ emit this->currentDocumentChanged(widgetDoc ? widgetDoc->documentIdentifier() : -1);
+}
+
+} // namespace Mayo
diff --git a/src/app/app_context.h b/src/app/app_context.h
new file mode 100644
index 00000000..583ef5e4
--- /dev/null
+++ b/src/app/app_context.h
@@ -0,0 +1,45 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#pragma once
+
+#include "commands_api.h"
+
+namespace Mayo {
+
+class MainWindow;
+class WidgetGuiDocument;
+
+// Provides implementation of IAppContext based on MainWindow
+class AppContext : public IAppContext {
+public:
+ AppContext(MainWindow* wnd);
+
+ GuiApplication* guiApp() const override;
+ TaskManager* taskMgr() const override;
+ QWidget* widgetMain() const override;
+ QWidget* widgetLeftSidebar() const override;
+ ModeWidgetMain modeWidgetMain() const override;
+ V3dViewController* v3dViewController(const GuiDocument* guiDoc) const override;
+
+ int findDocumentIndex(Document::Identifier docId) const override;
+ Document::Identifier findDocumentFromIndex(int index) const override;
+
+ Document::Identifier currentDocument() const override;
+ void setCurrentDocument(Document::Identifier docId) override;
+
+ void updateControlsEnabledStatus() override;
+ void deleteDocumentWidget(const DocumentPtr& doc) override;
+
+private:
+ WidgetGuiDocument* findWidgetGuiDocument(std::function fn) const;
+
+ void onCurrentDocumentIndexChanged(int docIndex);
+
+ MainWindow* m_wnd = nullptr;
+};
+
+} // namespace Mayo
diff --git a/src/app/app_module.cpp b/src/app/app_module.cpp
index 5a33e23d..46e87e49 100644
--- a/src/app/app_module.cpp
+++ b/src/app/app_module.cpp
@@ -31,9 +31,10 @@
namespace Mayo {
AppModule::AppModule()
- : m_settings(new Settings(this)),
+ : m_settings(new Settings),
m_props(m_settings),
- m_locale(QLocale::system())
+ m_stdLocale(std::locale("")),
+ m_qtLocale(QLocale::system())
{
static bool metaTypesRegistered = false;
if (!metaTypesRegistered) {
@@ -47,15 +48,20 @@ AppModule::AppModule()
QStringUtils::TextOptions AppModule::defaultTextOptions() const
{
QStringUtils::TextOptions opts;
- opts.locale = this->locale();
+ opts.locale = this->qtLocale();
opts.unitDecimals = m_props.unitSystemDecimals;
opts.unitSchema = m_props.unitSystemSchema;
return opts;
}
-const QLocale& AppModule::locale() const
+const std::locale& AppModule::stdLocale() const
{
- return m_locale;
+ return m_stdLocale;
+}
+
+const QLocale& AppModule::qtLocale() const
+{
+ return m_qtLocale;
}
const Enumeration& AppModule::languages()
@@ -135,22 +141,23 @@ bool AppModule::fromVariant(Property* prop, const Settings::Variant& variant) co
void AppModule::emitMessage(Messenger::MessageType msgType, std::string_view text)
{
+ const QString qtext = to_QString(text);
{
- std::lock_guard lock(m_mutexMessageLog);
- m_messageLog.push_back({ msgType, to_QString(text) });
+ [[maybe_unused]] std::lock_guard lock(m_mutexMessageLog);
+ m_messageLog.push_back({ msgType, qtext });
}
- emit this->message(msgType, to_QString(text));
+ this->signalMessage.send(msgType, qtext);
}
void AppModule::clearMessageLog()
{
{
- std::lock_guard lock(m_mutexMessageLog);
+ [[maybe_unused]] std::lock_guard lock(m_mutexMessageLog);
m_messageLog.clear();
}
- emit this->messageLogCleared();
+ this->signalMessageLogCleared.send();
}
void AppModule::prependRecentFile(const FilePath& fp)
@@ -334,4 +341,10 @@ AppModule* AppModule::get()
return &appModule;
}
+AppModule::~AppModule()
+{
+ delete m_settings;
+ m_settings = nullptr;
+}
+
} // namespace Mayo
diff --git a/src/app/app_module.h b/src/app/app_module.h
index f96dbd20..5fadb7a7 100644
--- a/src/app/app_module.h
+++ b/src/app/app_module.h
@@ -18,7 +18,7 @@
#include "../base/settings.h"
#include "../base/unit_system.h"
-#include
+#include
#include
class TDF_Label;
@@ -33,12 +33,10 @@ class TaskProgress;
// Provides the root application object as a singleton
// Implements also the behavior specific to the application
class AppModule :
- public QObject,
public IO::ParametersProvider,
public PropertyValueConversion,
public Messenger
{
- Q_OBJECT
MAYO_DECLARE_TEXT_ID_FUNCTIONS(Mayo::AppModule)
public:
// Loggable message
@@ -50,6 +48,8 @@ class AppModule :
// Query singleton instance
static AppModule* get();
+ ~AppModule();
+
// Settings
const AppModuleProperties* properties() const { return &m_props; }
AppModuleProperties* properties() { return &m_props; }
@@ -62,7 +62,8 @@ class AppModule :
QStringUtils::TextOptions defaultTextOptions() const;
// Current locale used by the application
- const QLocale& locale() const;
+ const std::locale& stdLocale() const;
+ const QLocale& qtLocale() const;
// Available supported languages
static const Enumeration& languages();
@@ -73,8 +74,8 @@ class AppModule :
// Logging
void clearMessageLog();
Span messageLog() const { return m_messageLog; }
- Q_SIGNAL void message(Messenger::MessageType msgType, const QString& text);
- Q_SIGNAL void messageLogCleared();
+ Signal signalMessage;
+ Signal<> signalMessageLogCleared;
// Recent files
void prependRecentFile(const FilePath& fp);
@@ -117,7 +118,8 @@ class AppModule :
AppModuleProperties m_props;
std::vector m_messageLog;
std::mutex m_mutexMessageLog;
- QLocale m_locale;
+ std::locale m_stdLocale;
+ QLocale m_qtLocale;
std::vector> m_vecDocTreeNodePropsProvider;
};
diff --git a/src/app/app_module_properties.cpp b/src/app/app_module_properties.cpp
index 823b9927..a66b597f 100644
--- a/src/app/app_module_properties.cpp
+++ b/src/app/app_module_properties.cpp
@@ -46,6 +46,7 @@ AppModuleProperties::AppModuleProperties(Settings* settings)
settings->addSetting(&this->lastOpenDir, groupId_application);
settings->addSetting(&this->lastSelectedFormatFilter, groupId_application);
settings->addSetting(&this->linkWithDocumentSelector, groupId_application);
+ settings->addSetting(&this->forceOpenGlFallbackWidget, groupId_application);
this->recentFiles.setUserVisible(false);
this->lastOpenDir.setUserVisible(false);
this->lastSelectedFormatFilter.setUserVisible(false);
@@ -61,6 +62,7 @@ AppModuleProperties::AppModuleProperties(Settings* settings)
settings->addSetting(&this->navigationStyle, groupId_graphics);
settings->addSetting(&this->defaultShowOriginTrihedron, groupId_graphics);
settings->addSetting(&this->instantZoomFactor, groupId_graphics);
+ settings->addSetting(&this->turnViewAngleIncrement, groupId_graphics);
// -- Clip planes
settings->addSetting(&this->clipPlanesCappingOn, sectionId_graphicsClipPlanes);
settings->addSetting(&this->clipPlanesCappingHatchOn, sectionId_graphicsClipPlanes);
@@ -82,11 +84,17 @@ AppModuleProperties::AppModuleProperties(Settings* settings)
this->lastOpenDir.setValue({});
this->lastSelectedFormatFilter.setValue({});
this->linkWithDocumentSelector.setValue(true);
+#ifndef MAYO_OS_MAC
+ this->forceOpenGlFallbackWidget.setValue(false);
+#else
+ this->forceOpenGlFallbackWidget.setValue(true);
+#endif
});
settings->addResetFunction(groupId_graphics, [=]{
this->navigationStyle.setValue(WidgetOccViewController::NavigationStyle::Mayo);
this->defaultShowOriginTrihedron.setValue(true);
this->instantZoomFactor.setValue(5.);
+ this->turnViewAngleIncrement.setQuantity(5 * Quantity_Degree);
});
settings->addResetFunction(groupId_meshing, [&]{
this->meshingQuality.setValue(BRepMeshQuality::Normal);
@@ -147,26 +155,47 @@ void AppModuleProperties::IO_bindParameters(const IO::System* ioSystem)
void AppModuleProperties::retranslate()
{
+ // Application
this->language.setDescription(
textIdTr("Language used for the application. Change will take effect after application restart"));
this->linkWithDocumentSelector.setDescription(
textIdTr("In case where multiple documents are opened, make sure the document displayed in "
"the 3D view corresponds to what is selected in the model tree"));
+ this->forceOpenGlFallbackWidget.setDescription(
+ textIdTr("Force usage of the fallback Qt widget to display OpenGL graphics.\n\n"
+ "When `OFF` the application will try to use OpenGL framebuffer for rendering, "
+ "this allows to display overlay widgets(eg measure tools panel) with translucid "
+ "background. "
+ "However using OpenGL framebuffer might cause troubles for some users(eg empty "
+ "3D window) especially on macOS.\n\n"
+ "When `ON` the application will use a regular Qt widget for rendering which "
+ "proved to be more supported.\n\n"
+ "This option is applicable when OpenCascade ≥ 7.6 version. "
+ "Change will take effect after application restart")
+ );
+
+ // Meshing
this->meshingQuality.setDescription(
textIdTr("Controls precision of the mesh to be computed from the BRep shape"));
this->meshingChordalDeflection.setDescription(
- textIdTr("For the tesselation of faces the chordal deflection limits the distance between "
+ textIdTr("For the tessellation of faces the chordal deflection limits the distance between "
"a curve and its tessellation"));
this->meshingAngularDeflection.setDescription(
- textIdTr("For the tesselation of faces the angular deflection limits the angle between "
+ textIdTr("For the tessellation of faces the angular deflection limits the angle between "
"subsequent segments in a polyline"));
this->meshingRelative.setDescription(
textIdTr("Relative computation of edge tolerance\n\n"
"If activated, deflection used for the polygonalisation of each edge will be "
"`ChordalDeflection` × `SizeOfEdge`. The deflection used for the faces will be "
"the maximum deflection of their edges."));
+
+ // Graphics
this->navigationStyle.setDescription(
textIdTr("3D view manipulation shortcuts configuration to mimic other common CAD applications"));
+ this->turnViewAngleIncrement.setDescription(
+ textIdTr("Angle increment used to turn(rotate) the 3D view around the normal of the view plane(Z axis frame reference)"));
+
+ // -- Graphics/ClipPlanes
this->defaultShowOriginTrihedron.setDescription(
textIdTr("Show or hide by default the trihedron centered at world origin. "
"This doesn't affect 3D view of currently opened documents"));
diff --git a/src/app/app_module_properties.h b/src/app/app_module_properties.h
index 267632a3..d8874672 100644
--- a/src/app/app_module_properties.h
+++ b/src/app/app_module_properties.h
@@ -52,6 +52,7 @@ class AppModuleProperties : public PropertyGroup {
PropertyFilePath lastOpenDir{ this, textId("lastOpenFolder") };
PropertyString lastSelectedFormatFilter{ this, textId("lastSelectedFormatFilter") };
PropertyBool linkWithDocumentSelector{ this, textId("linkWithDocumentSelector") };
+ PropertyBool forceOpenGlFallbackWidget{ this, textId("forceOpenGlFallbackWidget") };
// Meshing
enum class BRepMeshQuality { VeryCoarse, Coarse, Normal, Precise, VeryPrecise, UserDefined };
PropertyEnum meshingQuality{ this, textId("meshingQuality") };
@@ -62,6 +63,7 @@ class AppModuleProperties : public PropertyGroup {
PropertyEnum navigationStyle{ this, textId("navigationStyle") };
PropertyBool defaultShowOriginTrihedron{ this, textId("defaultShowOriginTrihedron") };
PropertyDouble instantZoomFactor{ this, textId("instantZoomFactor") };
+ PropertyAngle turnViewAngleIncrement{ this, textId("turnViewAngleIncrement") };
// -- Graphics/ClipPlanes
PropertyBool clipPlanesCappingOn{ this, textId("cappingOn") };
PropertyBool clipPlanesCappingHatchOn{ this, textId("cappingHatchOn") };
diff --git a/src/app/button_flat.h b/src/app/button_flat.h
index a740a7c9..5e5ad350 100644
--- a/src/app/button_flat.h
+++ b/src/app/button_flat.h
@@ -6,7 +6,7 @@
#pragma once
-#include "widgets_utils.h"
+#include "qtwidgets_utils.h"
#include
#include
diff --git a/src/app/cli_export.cpp b/src/app/cli_export.cpp
index 3134baf4..f5d7d428 100644
--- a/src/app/cli_export.cpp
+++ b/src/app/cli_export.cpp
@@ -187,13 +187,13 @@ void cli_asyncExportDocuments(
};
// Show progress/traces corresponding to task events
- QObject::connect(taskMgr, &TaskManager::started, app, [=](TaskId taskId) {
+ taskMgr->signalStarted.connectSlot([=](TaskId taskId) {
if (args.progressReport)
fnPrintProgress();
else
qInfo() << to_QString(taskMgr->title(taskId));
});
- QObject::connect(taskMgr, &TaskManager::ended, app, [=](TaskId taskId) {
+ taskMgr->signalEnded.connectSlot([=](TaskId taskId) {
if (args.progressReport) {
fnPrintProgress();
}
@@ -204,13 +204,13 @@ void cli_asyncExportDocuments(
qCritical() << to_QString(taskMgr->title(taskId));
}
});
- QObject::connect(taskMgr, &TaskManager::progressChanged, app, [=]{
+ taskMgr->signalProgressChanged.connectSlot([=]{
if (args.progressReport)
fnPrintProgress();
});
helper->exportTaskCount = int(args.filesToExport.size());
- QObject::connect(taskMgr, &TaskManager::ended, app, [=]{
+ taskMgr->signalEnded.connectSlot([=]{
if (helper->exportTaskCount == 0) {
bool okExport = true;
for (const auto& mapPair : helper->mapTaskStatus) {
diff --git a/src/app/commands_api.cpp b/src/app/commands_api.cpp
new file mode 100644
index 00000000..5ed5b40f
--- /dev/null
+++ b/src/app/commands_api.cpp
@@ -0,0 +1,54 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#include "commands_api.h"
+
+#include "../base/application.h"
+#include "../gui/gui_application.h"
+
+#include
+
+namespace Mayo {
+
+IAppContext::IAppContext(QObject* parent)
+ : QObject(parent)
+{
+}
+
+Command::Command(IAppContext* context)
+ : QObject(context ? context->widgetMain() : nullptr),
+ m_context(context)
+{
+}
+
+Application* Command::app() const
+{
+ return m_context->guiApp()->application().get();
+}
+
+GuiDocument* Command::currentGuiDocument() const
+{
+ DocumentPtr doc = this->app()->findDocumentByIdentifier(this->currentDocument());
+ return this->guiApp()->findGuiDocument(doc);
+}
+
+int Command::currentDocumentIndex() const
+{
+ return this->context()->findDocumentIndex(this->currentDocument());
+}
+
+void Command::setCurrentDocument(const DocumentPtr& doc)
+{
+ m_context->setCurrentDocument(doc->identifier());
+}
+
+void Command::setAction(QAction* action)
+{
+ m_action = action;
+ QObject::connect(action, &QAction::triggered, this, &Command::execute);
+}
+
+} // namespace Mayo
diff --git a/src/app/commands_api.h b/src/app/commands_api.h
new file mode 100644
index 00000000..755ce7e1
--- /dev/null
+++ b/src/app/commands_api.h
@@ -0,0 +1,85 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#pragma once
+
+#include "../base/document.h"
+#include "../base/filepath.h"
+#include "../base/text_id.h"
+
+#include
+#include // WARNING Qt5 / Qt6
+class QWidget;
+
+namespace Mayo {
+
+class Application;
+class GuiApplication;
+class GuiDocument;
+class V3dViewController;
+class TaskManager;
+
+// Provides interface to access/interact with application
+class IAppContext : public QObject {
+ Q_OBJECT
+public:
+ enum class ModeWidgetMain { Unknown, Home, Documents };
+
+ IAppContext(QObject* parent = nullptr);
+
+ virtual GuiApplication* guiApp() const = 0;
+ virtual TaskManager* taskMgr() const = 0;
+ virtual QWidget* widgetMain() const = 0;
+ virtual QWidget* widgetLeftSidebar() const = 0;
+ virtual ModeWidgetMain modeWidgetMain() const = 0;
+ virtual V3dViewController* v3dViewController(const GuiDocument* guiDoc) const = 0;
+
+ virtual Document::Identifier currentDocument() const = 0;
+ virtual void setCurrentDocument(Document::Identifier docId) = 0;
+
+ virtual int findDocumentIndex(Document::Identifier docId) const = 0;
+ virtual Document::Identifier findDocumentFromIndex(int index) const = 0;
+
+ virtual void updateControlsEnabledStatus() = 0;
+ virtual void deleteDocumentWidget(const DocumentPtr& doc) = 0;
+
+signals:
+ void currentDocumentChanged(Mayo::Document::Identifier docId);
+};
+
+// Represents a single action in the application
+class Command : public QObject {
+ Q_OBJECT
+ MAYO_DECLARE_TEXT_ID_FUNCTIONS(Mayo::Command)
+public:
+ Command(IAppContext* context);
+ virtual ~Command() = default;
+
+ virtual void execute() = 0;
+
+ IAppContext* context() const { return m_context; }
+
+ QAction* action() const { return m_action; }
+ virtual bool getEnabledStatus() const { return true; }
+
+protected:
+ Application* app() const;
+ GuiApplication* guiApp() const { return m_context->guiApp(); }
+ TaskManager* taskMgr() const { return m_context->taskMgr(); }
+ QWidget* widgetMain() const { return m_context->widgetMain(); }
+ Document::Identifier currentDocument() const { return m_context->currentDocument(); }
+ GuiDocument* currentGuiDocument() const;
+ int currentDocumentIndex() const;
+
+ void setCurrentDocument(const DocumentPtr& doc);
+ void setAction(QAction* action);
+
+private:
+ IAppContext* m_context = nullptr;
+ QAction* m_action = nullptr;
+};
+
+} // namespace Mayo
diff --git a/src/app/commands_display.cpp b/src/app/commands_display.cpp
new file mode 100644
index 00000000..1b021a6b
--- /dev/null
+++ b/src/app/commands_display.cpp
@@ -0,0 +1,334 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#include "commands_display.h"
+
+#include "../base/application.h"
+#include "../base/cpp_utils.h"
+#include "../base/unit_system.h"
+#include "../gui/gui_application.h"
+#include "../gui/gui_document.h"
+#include "../gui/v3d_view_controller.h"
+#include "app_module.h"
+#include "qstring_conv.h"
+#include "theme.h"
+
+#include
+#include
+#include // WARNING Qt5 / Qt6
+#include
+
+namespace Mayo {
+
+CommandChangeProjection::CommandChangeProjection(IAppContext* context)
+ : Command(context)
+{
+ m_actionOrtho = new QAction(Command::tr("Orthographic"), this);
+ m_actionPersp = new QAction(Command::tr("Perspective"), this);
+ m_actionOrtho->setCheckable(true);
+ m_actionPersp->setCheckable(true);
+
+ auto menu = new QMenu(this->widgetMain());
+ menu->addAction(m_actionOrtho);
+ menu->addAction(m_actionPersp);
+
+ auto group = new QActionGroup(menu);
+ group->setExclusive(true);
+ group->addAction(m_actionOrtho);
+ group->addAction(m_actionPersp);
+
+ auto action = new QAction(this);
+ action->setText(Command::tr("Projection"));
+ action->setMenu(menu);
+ this->setAction(action);
+
+ QObject::connect(group, &QActionGroup::triggered, this, [=](const QAction* action) {
+ GuiDocument* guiDoc = this->currentGuiDocument();
+ if (guiDoc) {
+ guiDoc->v3dView()->Camera()->SetProjectionType(
+ action == m_actionOrtho ?
+ Graphic3d_Camera::Projection_Orthographic :
+ Graphic3d_Camera::Projection_Perspective
+ );
+ guiDoc->graphicsView().redraw();
+ }
+ });
+
+ QObject::connect(
+ context, &IAppContext::currentDocumentChanged,
+ this, &CommandChangeProjection::onCurrentDocumentChanged
+ );
+}
+
+void CommandChangeProjection::execute()
+{
+}
+
+bool CommandChangeProjection::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+void CommandChangeProjection::onCurrentDocumentChanged()
+{
+ const GuiDocument* guiDoc = this->currentGuiDocument();
+ if (!guiDoc)
+ return;
+
+ // Sync menu with current projection type
+ const auto viewProjType = guiDoc->v3dView()->Camera()->ProjectionType();
+ Q_ASSERT(viewProjType == Graphic3d_Camera::Projection_Perspective
+ || viewProjType == Graphic3d_Camera::Projection_Orthographic
+ );
+ QAction* actionProj = viewProjType == Graphic3d_Camera::Projection_Perspective ? m_actionPersp : m_actionOrtho;
+ [[maybe_unused]] QSignalBlocker sigBlk(this->action());
+ actionProj->setChecked(true);
+}
+
+CommandChangeDisplayMode::CommandChangeDisplayMode(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Mode"));
+ this->setAction(action);
+}
+
+CommandChangeDisplayMode::CommandChangeDisplayMode(IAppContext* context, QMenu* containerMenu)
+ : CommandChangeDisplayMode(context)
+{
+ QObject::connect(
+ containerMenu, &QMenu::aboutToShow,
+ this, &CommandChangeDisplayMode::recreateMenuDisplayMode
+ );
+}
+
+void CommandChangeDisplayMode::execute()
+{
+}
+
+bool CommandChangeDisplayMode::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+void CommandChangeDisplayMode::recreateMenuDisplayMode()
+{
+ QMenu* menu = this->action()->menu();
+ if (!menu) {
+ menu = new QMenu(this->widgetMain());
+ this->action()->setMenu(menu);
+ }
+
+ menu->clear();
+
+ GuiDocument* guiDoc = this->currentGuiDocument();
+ if (!guiDoc)
+ return;
+
+ const auto spanDrivers = this->guiApp()->graphicsObjectDrivers();
+ for (const GraphicsObjectDriverPtr& driver : spanDrivers) {
+ if (driver->displayModes().empty())
+ continue; // Skip
+
+ if (driver != spanDrivers.front())
+ menu->addSeparator();
+
+ auto group = new QActionGroup(menu);
+ group->setExclusive(true);
+ for (const Enumeration::Item& displayMode : driver->displayModes().items()) {
+ auto action = new QAction(to_QString(displayMode.name.tr()), menu);
+ action->setCheckable(true);
+ action->setData(displayMode.value);
+ menu->addAction(action);
+ group->addAction(action);
+ if (displayMode.value == guiDoc->activeDisplayMode(driver))
+ action->setChecked(true);
+ }
+
+ QObject::connect(group, &QActionGroup::triggered, this, [=](QAction* action) {
+ guiDoc->setActiveDisplayMode(driver, action->data().toInt());
+ guiDoc->graphicsView().redraw();
+ });
+ }
+}
+
+CommandToggleOriginTrihedron::CommandToggleOriginTrihedron(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Show Origin Trihedron"));
+ action->setToolTip(Command::tr("Show/Hide Origin Trihedron"));
+ action->setCheckable(true);
+ action->setChecked(false);
+ this->setAction(action);
+
+ QObject::connect(
+ context, &IAppContext::currentDocumentChanged,
+ this, &CommandToggleOriginTrihedron::onCurrentDocumentChanged
+ );
+}
+
+void CommandToggleOriginTrihedron::execute()
+{
+ GuiDocument* guiDoc = this->currentGuiDocument();
+ if (guiDoc) {
+ guiDoc->toggleOriginTrihedronVisibility();
+ guiDoc->graphicsScene()->redraw();
+ }
+}
+
+bool CommandToggleOriginTrihedron::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+void CommandToggleOriginTrihedron::onCurrentDocumentChanged()
+{
+ GuiDocument* guiDoc = this->currentGuiDocument();
+ if (guiDoc) {
+ // Sync action with current visibility status of origin trihedron
+ [[maybe_unused]] QSignalBlocker sigBlk(this->action());
+ this->action()->setChecked(guiDoc->isOriginTrihedronVisible());
+ }
+ else {
+ this->action()->setChecked(false);
+ }
+}
+
+CommandTogglePerformanceStats::CommandTogglePerformanceStats(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Show Performance Stats"));
+ action->setToolTip(Command::tr("Show/Hide rendering performance statistics"));
+ action->setCheckable(true);
+ action->setChecked(false);
+ this->setAction(action);
+
+ QObject::connect(
+ context, &IAppContext::currentDocumentChanged,
+ this, &CommandTogglePerformanceStats::onCurrentDocumentChanged
+ );
+}
+
+void CommandTogglePerformanceStats::execute()
+{
+ GuiDocument* guiDoc = this->currentGuiDocument();
+ if (guiDoc) {
+ CppUtils::toggle(guiDoc->v3dView()->ChangeRenderingParams().ToShowStats);
+ guiDoc->graphicsView().redraw();
+ }
+}
+
+bool CommandTogglePerformanceStats::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+void CommandTogglePerformanceStats::onCurrentDocumentChanged()
+{
+ GuiDocument* guiDoc = this->currentGuiDocument();
+ if (guiDoc) {
+ // Sync action with current visibility status of rendering performance stats
+ [[maybe_unused]] QSignalBlocker sigBlk(this->action());
+ this->action()->setChecked(guiDoc->v3dView()->ChangeRenderingParams().ToShowStats);
+ }
+ else {
+ this->action()->setChecked(false);
+ }
+}
+
+CommandZoomInCurrentDocument::CommandZoomInCurrentDocument(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Zoom In"));
+ action->setIcon(mayoTheme()->icon(Theme::Icon::ZoomIn));
+ action->setShortcut(Qt::CTRL + Qt::Key_Plus);
+ this->setAction(action);
+}
+
+void CommandZoomInCurrentDocument::execute()
+{
+ auto ctrl = this->context()->v3dViewController(this->currentGuiDocument());
+ if (ctrl)
+ ctrl->zoomIn();
+}
+
+bool CommandZoomInCurrentDocument::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+CommandZoomOutCurrentDocument::CommandZoomOutCurrentDocument(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Zoom Out"));
+ action->setIcon(mayoTheme()->icon(Theme::Icon::ZoomOut));
+ action->setShortcut(Qt::CTRL + Qt::Key_Minus);
+ this->setAction(action);
+}
+
+void CommandZoomOutCurrentDocument::execute()
+{
+ auto ctrl = this->context()->v3dViewController(this->currentGuiDocument());
+ if (ctrl)
+ ctrl->zoomOut();
+}
+
+bool CommandZoomOutCurrentDocument::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+CommandTurnViewCounterClockWise::CommandTurnViewCounterClockWise(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Turn Counter Clockwise"));
+ action->setIcon(mayoTheme()->icon(Theme::Icon::TurnCounterClockwise));
+ action->setShortcut(Qt::CTRL + Qt::Key_Left);
+ this->setAction(action);
+}
+
+void CommandTurnViewCounterClockWise::execute()
+{
+ const QuantityAngle increment = AppModule::get()->properties()->turnViewAngleIncrement.quantity();
+ auto ctrl = this->context()->v3dViewController(this->currentGuiDocument());
+ if (ctrl)
+ ctrl->turn(V3d_Z, -increment);
+}
+
+bool CommandTurnViewCounterClockWise::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+CommandTurnViewClockWise::CommandTurnViewClockWise(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Turn Clockwise"));
+ action->setIcon(mayoTheme()->icon(Theme::Icon::TurnClockwise));
+ action->setShortcut(Qt::CTRL + Qt::Key_Right);
+ this->setAction(action);
+}
+
+void CommandTurnViewClockWise::execute()
+{
+ const QuantityAngle increment = AppModule::get()->properties()->turnViewAngleIncrement.quantity();
+ auto ctrl = this->context()->v3dViewController(this->currentGuiDocument());
+ if (ctrl)
+ ctrl->turn(V3d_Z, increment);
+}
+
+bool CommandTurnViewClockWise::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+} // namespace Mayo
diff --git a/src/app/commands_display.h b/src/app/commands_display.h
new file mode 100644
index 00000000..14dc6d50
--- /dev/null
+++ b/src/app/commands_display.h
@@ -0,0 +1,86 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#pragma once
+
+#include "commands_api.h"
+
+class QMenu;
+
+namespace Mayo {
+
+class CommandChangeProjection : public Command {
+public:
+ CommandChangeProjection(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+
+private:
+ void onCurrentDocumentChanged();
+ QAction* m_actionOrtho = nullptr;
+ QAction* m_actionPersp = nullptr;
+};
+
+class CommandChangeDisplayMode : public Command {
+public:
+ CommandChangeDisplayMode(IAppContext* context);
+ CommandChangeDisplayMode(IAppContext* context, QMenu* containerMenu);
+ void execute() override;
+ bool getEnabledStatus() const override;
+
+private:
+ void recreateMenuDisplayMode();
+};
+
+class CommandToggleOriginTrihedron : public Command {
+public:
+ CommandToggleOriginTrihedron(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+
+private:
+ void onCurrentDocumentChanged();
+};
+
+class CommandTogglePerformanceStats : public Command {
+public:
+ CommandTogglePerformanceStats(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+
+private:
+ void onCurrentDocumentChanged();
+};
+
+class CommandZoomInCurrentDocument : public Command {
+public:
+ CommandZoomInCurrentDocument(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+class CommandZoomOutCurrentDocument : public Command {
+public:
+ CommandZoomOutCurrentDocument(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+class CommandTurnViewCounterClockWise : public Command {
+public:
+ CommandTurnViewCounterClockWise(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+class CommandTurnViewClockWise : public Command {
+public:
+ CommandTurnViewClockWise(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+} // namespace Mayo
diff --git a/src/app/commands_file.cpp b/src/app/commands_file.cpp
new file mode 100644
index 00000000..133d4d82
--- /dev/null
+++ b/src/app/commands_file.cpp
@@ -0,0 +1,577 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#include "commands_file.h"
+
+#include "../base/application.h"
+#include "../base/task_manager.h"
+#include "../gui/gui_application.h"
+#include "app_module.h"
+#include "filepath_conv.h"
+#include "qstring_conv.h"
+#include "recent_files.h"
+#include "theme.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Mayo {
+
+namespace {
+
+QString fileFilter(IO::Format format)
+{
+ if (format == IO::Format_Unknown)
+ return {};
+
+ QString filter;
+ for (std::string_view suffix : IO::formatFileSuffixes(format)) {
+ if (suffix.data() != IO::formatFileSuffixes(format).front().data())
+ filter += " ";
+
+ const QString qsuffix = to_QString(suffix);
+ filter += "*." + qsuffix;
+#ifdef Q_OS_UNIX
+ filter += " *." + qsuffix.toUpper();
+#endif
+ }
+
+ //: %1 is the format identifier and %2 is the file filters string
+ return Command::tr("%1 files(%2)")
+ .arg(to_QString(IO::formatIdentifier(format)))
+ .arg(filter);
+}
+
+IO::Format formatFromFilter(const QString& filter)
+{
+ for (IO::Format format : AppModule::get()->ioSystem()->readerFormats()) {
+ if (filter == fileFilter(format))
+ return format;
+ }
+
+ for (IO::Format format : AppModule::get()->ioSystem()->writerFormats()) {
+ if (filter == fileFilter(format))
+ return format;
+ }
+
+ return IO::Format_Unknown;
+}
+
+// TODO: move in Options
+struct ImportExportSettings {
+ FilePath openDir;
+ QString selectedFilter;
+
+ static ImportExportSettings load()
+ {
+ return {
+ AppModule::get()->properties()->lastOpenDir.value(),
+ to_QString(AppModule::get()->properties()->lastSelectedFormatFilter.value())
+ };
+ }
+
+ static void save(const ImportExportSettings& sets)
+ {
+ AppModule::get()->properties()->lastOpenDir.setValue(sets.openDir);
+ AppModule::get()->properties()->lastSelectedFormatFilter.setValue(to_stdString(sets.selectedFilter));
+ }
+};
+
+struct OpenFileNames {
+ std::vector listFilepath;
+ ImportExportSettings lastIoSettings;
+ IO::Format selectedFormat;
+
+ enum GetOption {
+ GetOne,
+ GetMany
+ };
+
+ static OpenFileNames get(
+ QWidget* parentWidget,
+ OpenFileNames::GetOption option = OpenFileNames::GetMany)
+ {
+ OpenFileNames result;
+ result.selectedFormat = IO::Format_Unknown;
+ result.lastIoSettings = ImportExportSettings::load();
+ QStringList listFormatFilter;
+ for (IO::Format format : AppModule::get()->ioSystem()->readerFormats())
+ listFormatFilter += fileFilter(format);
+
+ const QString allFilesFilter = Command::tr("All files(*.*)");
+ listFormatFilter.append(allFilesFilter);
+ const QString dlgTitle = Command::tr("Select Part File");
+ const QString dlgOpenDir = filepathTo(result.lastIoSettings.openDir);
+ const QString dlgFilter = listFormatFilter.join(QLatin1String(";;"));
+ QString* dlgPtrSelFilter = &result.lastIoSettings.selectedFilter;
+ if (option == OpenFileNames::GetOne) {
+ const QString strFilepath =
+ QFileDialog::getOpenFileName(
+ parentWidget, dlgTitle, dlgOpenDir, dlgFilter, dlgPtrSelFilter);
+ result.listFilepath.clear();
+ result.listFilepath.push_back(filepathFrom(strFilepath));
+ }
+ else {
+ const QStringList listStrFilePath =
+ QFileDialog::getOpenFileNames(
+ parentWidget, dlgTitle, dlgOpenDir, dlgFilter, dlgPtrSelFilter);
+ result.listFilepath.clear();
+ for (const QString& strFilePath : listStrFilePath)
+ result.listFilepath.push_back(filepathFrom(strFilePath));
+ }
+
+ if (!result.listFilepath.empty()) {
+ result.lastIoSettings.openDir = result.listFilepath.front();
+ result.selectedFormat =
+ result.lastIoSettings.selectedFilter != allFilesFilter ?
+ formatFromFilter(result.lastIoSettings.selectedFilter) :
+ IO::Format_Unknown;
+ ImportExportSettings::save(result.lastIoSettings);
+ }
+
+ return result;
+ }
+};
+
+QString strFilepathQuoted(const QString& filepath)
+{
+ for (QChar c : filepath) {
+ if (c.isSpace())
+ return "\"" + filepath + "\"";
+ }
+
+ return filepath;
+}
+
+} // namespace
+
+
+void FileCommandTools::closeDocument(IAppContext* context, Document::Identifier docId)
+{
+ auto app = context->guiApp()->application();
+ DocumentPtr doc = app->findDocumentByIdentifier(docId);
+ context->deleteDocumentWidget(doc);
+ app->closeDocument(doc);
+ context->updateControlsEnabledStatus();
+}
+
+void FileCommandTools::openDocumentsFromList(IAppContext* context, Span listFilePath)
+{
+ auto app = context->guiApp()->application();
+ auto appModule = AppModule::get();
+ for (const FilePath& fp : listFilePath) {
+ DocumentPtr docPtr = app->findDocumentByLocation(fp);
+ if (docPtr.IsNull()) {
+ docPtr = app->newDocument();
+ docPtr->setName(fp.filename().u8string());
+ docPtr->setFilePath(fp);
+ // Use the Document identifier instead of handle within the job function(capture)
+ // Using the handle increases the ref count and the task will be released on next
+ // task creation, so the document won't be destroyed
+ const Document::Identifier newDocId = docPtr->identifier();
+ const TaskId taskId = context->taskMgr()->newTask([=](TaskProgress* progress) {
+ QElapsedTimer chrono;
+ chrono.start();
+ const bool okImport =
+ appModule->ioSystem()->importInDocument()
+ .targetDocument(app->findDocumentByIdentifier(newDocId))
+ .withFilepath(fp)
+ .withParametersProvider(appModule)
+ .withEntityPostProcess([=](TDF_Label labelEntity, TaskProgress* progress) {
+ appModule->computeBRepMesh(labelEntity, progress);
+ })
+ .withEntityPostProcessRequiredIf(&IO::formatProvidesBRep)
+ .withEntityPostProcessInfoProgress(20, Command::textIdTr("Mesh BRep shapes"))
+ .withMessenger(appModule)
+ .withTaskProgress(progress)
+ .execute();
+ if (okImport)
+ appModule->emitInfo(fmt::format(Command::textIdTr("Import time: {}ms"), chrono.elapsed()));
+ });
+ context->taskMgr()->setTitle(taskId, fp.stem().u8string());
+ context->taskMgr()->run(taskId);
+ appModule->prependRecentFile(fp);
+ }
+ else {
+ if (listFilePath.size() == 1)
+ context->setCurrentDocument(docPtr->identifier());
+ }
+ } // endfor()
+}
+
+void FileCommandTools::openDocument(IAppContext* context, FilePath fp)
+{
+ FileCommandTools::openDocumentsFromList(context, Span(&fp, 1));
+}
+
+CommandNewDocument::CommandNewDocument(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("New"));
+ action->setToolTip(Command::tr("New Document"));
+ action->setShortcut(Qt::CTRL + Qt::Key_N);
+ this->setAction(action);
+}
+
+void CommandNewDocument::execute()
+{
+ static unsigned docSequenceId = 0;
+ auto docPtr = this->app()->newDocument(Document::Format::Binary);
+ docPtr->setName(to_stdString(Command::tr("Anonymous%1").arg(++docSequenceId)));
+}
+
+CommandOpenDocuments::CommandOpenDocuments(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Open"));
+ action->setToolTip(Command::tr("Open Documents"));
+ action->setShortcut(Qt::CTRL + Qt::Key_O);
+ this->setAction(action);
+
+ context->widgetMain()->setAcceptDrops(true);
+ context->widgetMain()->installEventFilter(this);
+}
+
+void CommandOpenDocuments::execute()
+{
+ const auto resFileNames = OpenFileNames::get(this->widgetMain());
+ if (!resFileNames.listFilepath.empty())
+ FileCommandTools::openDocumentsFromList(this->context(), resFileNames.listFilepath);
+}
+
+bool CommandOpenDocuments::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == this->widgetMain()) {
+ if (event->type() == QEvent::DragEnter) {
+ auto dragEnterEvent = static_cast(event);
+ if (dragEnterEvent->mimeData()->hasUrls())
+ dragEnterEvent->acceptProposedAction();
+
+ return true;
+ }
+ else if (event->type() == QEvent::Drop) {
+ auto dropEvent = static_cast(event);
+ const QList listUrl = dropEvent->mimeData()->urls();
+ std::vector listFilePath;
+ for (const QUrl& url : listUrl) {
+ if (url.isLocalFile())
+ listFilePath.push_back(filepathFrom(url.toLocalFile()));
+ }
+
+ dropEvent->acceptProposedAction();
+ FileCommandTools::openDocumentsFromList(this->context(), listFilePath);
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ return Command::eventFilter(watched, event);
+}
+
+CommandRecentFiles::CommandRecentFiles(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Recent files"));
+ this->setAction(action);
+}
+
+CommandRecentFiles::CommandRecentFiles(IAppContext* context, QMenu* containerMenu)
+ : CommandRecentFiles(context)
+{
+ QObject::connect(
+ containerMenu, &QMenu::aboutToShow,
+ this, &CommandRecentFiles::recreateEntries
+ );
+}
+
+void CommandRecentFiles::execute()
+{
+}
+
+void CommandRecentFiles::recreateEntries()
+{
+ QMenu* menu = this->action()->menu();
+ if (!menu)
+ menu = new QMenu(this->widgetMain());
+
+ menu->clear();
+ int idFile = 0;
+ auto appModule = AppModule::get();
+ const RecentFiles& recentFiles = appModule->properties()->recentFiles.value();
+ for (const RecentFile& recentFile : recentFiles) {
+ const QString strFilePath = filepathTo(recentFile.filepath);
+ const QString strEntryRecentFile = Command::tr("%1 | %2").arg(++idFile).arg(strFilePath);
+ menu->addAction(strEntryRecentFile, this, [=]{
+ FileCommandTools::openDocument(this->context(), recentFile.filepath);
+ });
+ }
+
+ if (!recentFiles.empty()) {
+ menu->addSeparator();
+ menu->addAction(Command::tr("Clear menu"), this, [=]{
+ menu->clear();
+ appModule->properties()->recentFiles.setValue({});
+ });
+ }
+
+ this->action()->setMenu(menu);
+}
+
+CommandImportInCurrentDocument::CommandImportInCurrentDocument(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Import"));
+ action->setToolTip(Command::tr("Import in current document"));
+ action->setIcon(mayoTheme()->icon(Theme::Icon::Import));
+ this->setAction(action);
+}
+
+void CommandImportInCurrentDocument::execute()
+{
+ const GuiDocument* guiDoc = this->currentGuiDocument();
+ if (!guiDoc)
+ return;
+
+ const auto resFileNames = OpenFileNames::get(this->widgetMain());
+ if (resFileNames.listFilepath.empty())
+ return;
+
+ auto appModule = AppModule::get();
+ const TaskId taskId = this->taskMgr()->newTask([=](TaskProgress* progress) {
+ QElapsedTimer chrono;
+ chrono.start();
+
+ const bool okImport = appModule->ioSystem()->importInDocument()
+ .targetDocument(guiDoc->document())
+ .withFilepaths(resFileNames.listFilepath)
+ .withParametersProvider(appModule)
+ .withEntityPostProcess([=](TDF_Label labelEntity, TaskProgress* progress) {
+ appModule->computeBRepMesh(labelEntity, progress);
+ })
+ .withEntityPostProcessRequiredIf(&IO::formatProvidesBRep)
+ .withEntityPostProcessInfoProgress(20, Command::textIdTr("Mesh BRep shapes"))
+ .withMessenger(appModule)
+ .withTaskProgress(progress)
+ .execute();
+ if (okImport)
+ appModule->emitInfo(fmt::format(Command::textIdTr("Import time: {}ms"), chrono.elapsed()));
+ });
+ const QString taskTitle =
+ resFileNames.listFilepath.size() > 1 ?
+ Command::tr("Import") :
+ filepathTo(resFileNames.listFilepath.front().stem());
+ this->taskMgr()->setTitle(taskId, to_stdString(taskTitle));
+ this->taskMgr()->run(taskId);
+ for (const FilePath& fp : resFileNames.listFilepath)
+ appModule->prependRecentFile(fp);
+}
+
+bool CommandImportInCurrentDocument::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+CommandExportSelectedApplicationItems::CommandExportSelectedApplicationItems(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Export selected items"));
+ action->setToolTip(Command::tr("Export selected items"));
+ action->setIcon(mayoTheme()->icon(Theme::Icon::Export));
+ this->setAction(action);
+}
+
+void CommandExportSelectedApplicationItems::execute()
+{
+ auto appModule = AppModule::get();
+ if (this->guiApp()->selectionModel()->selectedItems().empty()) {
+ appModule->emitError(Command::textIdTr("No item selected for export"));
+ return;
+ }
+
+ QStringList listWriterFileFilter;
+ for (IO::Format format : appModule->ioSystem()->writerFormats())
+ listWriterFileFilter.append(fileFilter(format));
+
+ auto lastSettings = ImportExportSettings::load();
+ const QString strFilepath =
+ QFileDialog::getSaveFileName(
+ this->widgetMain(),
+ Command::tr("Select Output File"),
+ filepathTo(lastSettings.openDir),
+ listWriterFileFilter.join(QLatin1String(";;")),
+ &lastSettings.selectedFilter);
+ if (strFilepath.isEmpty())
+ return;
+
+ lastSettings.openDir = filepathFrom(strFilepath);
+ const IO::Format format = formatFromFilter(lastSettings.selectedFilter);
+ const TaskId taskId = this->taskMgr()->newTask([=](TaskProgress* progress) {
+ QElapsedTimer chrono;
+ chrono.start();
+ const bool okExport =
+ appModule->ioSystem()->exportApplicationItems()
+ .targetFile(filepathFrom(strFilepath))
+ .targetFormat(format)
+ .withItems(this->guiApp()->selectionModel()->selectedItems())
+ .withParameters(appModule->findWriterParameters(format))
+ .withMessenger(appModule)
+ .withTaskProgress(progress)
+ .execute();
+ if (okExport)
+ appModule->emitInfo(fmt::format(Command::textIdTr("Export time: {}ms"), chrono.elapsed()));
+ });
+ this->taskMgr()->setTitle(taskId, to_stdString(QFileInfo(strFilepath).fileName()));
+ this->taskMgr()->run(taskId);
+ ImportExportSettings::save(lastSettings);
+}
+
+bool CommandExportSelectedApplicationItems::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+CommandCloseCurrentDocument::CommandCloseCurrentDocument(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Close \"%1\""));
+ action->setToolTip(action->text());
+ action->setIcon(mayoTheme()->icon(Theme::Icon::Cross));
+ action->setShortcut(Qt::CTRL + Qt::Key_W);
+ this->setAction(action);
+
+ QObject::connect(
+ context, &IAppContext::currentDocumentChanged,
+ this, &CommandCloseCurrentDocument::updateActionText
+ );
+ this->app()->signalDocumentNameChanged.connectSlot([=](const DocumentPtr& doc) {
+ if (this->currentDocument() == doc->identifier())
+ this->updateActionText(this->currentDocument());
+ });
+
+ this->updateActionText(-1);
+}
+
+void CommandCloseCurrentDocument::execute()
+{
+ FileCommandTools::closeDocument(this->context(), this->currentDocument());
+}
+
+bool CommandCloseCurrentDocument::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+void CommandCloseCurrentDocument::updateActionText(Document::Identifier docId)
+{
+ DocumentPtr docPtr = this->app()->findDocumentByIdentifier(docId);
+ const QString docName = to_QString(docPtr ? docPtr->name() : std::string{});
+ const QString textActionClose =
+ docPtr ?
+ Command::tr("Close \"%1\"").arg(strFilepathQuoted(docName)) :
+ Command::tr("Close");
+ this->action()->setText(textActionClose);
+ this->action()->setToolTip(textActionClose);
+}
+
+CommandCloseAllDocuments::CommandCloseAllDocuments(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Close all"));
+ action->setToolTip(Command::tr("Close all documents"));
+ action->setShortcut((Qt::CTRL | Qt::SHIFT) + Qt::Key_W);
+ this->setAction(action);
+}
+
+void CommandCloseAllDocuments::execute()
+{
+ while (!this->guiApp()->guiDocuments().empty())
+ FileCommandTools::closeDocument(this->context(), this->currentDocument());
+}
+
+bool CommandCloseAllDocuments::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+CommandCloseAllDocumentsExceptCurrent::CommandCloseAllDocumentsExceptCurrent(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Close all except current"));
+ action->setToolTip(Command::tr("Close all except current document"));
+ this->setAction(action);
+
+ QObject::connect(
+ context, &IAppContext::currentDocumentChanged,
+ this, &CommandCloseAllDocumentsExceptCurrent::updateActionText
+ );
+ this->app()->signalDocumentNameChanged.connectSlot([=](const DocumentPtr& doc) {
+ if (this->currentDocument() == doc->identifier())
+ this->updateActionText(this->currentDocument());
+ });
+
+ this->updateActionText(-1);
+}
+
+void CommandCloseAllDocumentsExceptCurrent::execute()
+{
+ GuiDocument* currentGuiDoc = this->currentGuiDocument();
+ std::vector vecGuiDoc;
+ for (GuiDocument* guiDoc : this->guiApp()->guiDocuments())
+ vecGuiDoc.push_back(guiDoc);
+
+ for (GuiDocument* guiDoc : vecGuiDoc) {
+ if (guiDoc != currentGuiDoc)
+ FileCommandTools::closeDocument(this->context(), guiDoc->document()->identifier());
+ }
+}
+
+bool CommandCloseAllDocumentsExceptCurrent::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+void CommandCloseAllDocumentsExceptCurrent::updateActionText(Document::Identifier docId)
+{
+ DocumentPtr docPtr = this->app()->findDocumentByIdentifier(docId);
+ const QString docName = to_QString(docPtr ? docPtr->name() : std::string{});
+ const QString textActionClose =
+ docPtr ?
+ Command::tr("Close all except \"%1\"").arg(strFilepathQuoted(docName)) :
+ Command::tr("Close all except current");
+ this->action()->setText(textActionClose);
+}
+
+CommandQuitApplication::CommandQuitApplication(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Quit"));
+ this->setAction(action);
+}
+
+void CommandQuitApplication::execute()
+{
+ QApplication::quit();
+}
+
+} // namespace Mayo
diff --git a/src/app/commands_file.h b/src/app/commands_file.h
new file mode 100644
index 00000000..bc06dcf7
--- /dev/null
+++ b/src/app/commands_file.h
@@ -0,0 +1,90 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#pragma once
+
+#include "../base/filepath.h"
+#include "../base/span.h"
+#include "commands_api.h"
+
+namespace Mayo {
+
+class FileCommandTools {
+public:
+ static void closeDocument(IAppContext* context, Document::Identifier docId);
+ static void openDocumentsFromList(IAppContext* context, Span listFilePath);
+ static void openDocument(IAppContext* context, FilePath fp);
+};
+
+class CommandNewDocument : public Command {
+public:
+ CommandNewDocument(IAppContext* context);
+ void execute() override;
+};
+
+class CommandOpenDocuments : public Command {
+public:
+ CommandOpenDocuments(IAppContext* context);
+ void execute() override;
+ bool eventFilter(QObject* watched, QEvent* event) override;
+};
+
+class CommandRecentFiles : public Command {
+public:
+ CommandRecentFiles(IAppContext* context);
+ CommandRecentFiles(IAppContext* context, QMenu* containerMenu);
+ void execute() override;
+ void recreateEntries();
+};
+
+class CommandImportInCurrentDocument : public Command {
+public:
+ CommandImportInCurrentDocument(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+class CommandExportSelectedApplicationItems : public Command {
+public:
+ CommandExportSelectedApplicationItems(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+class CommandCloseCurrentDocument : public Command {
+public:
+ CommandCloseCurrentDocument(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+
+private:
+ void updateActionText(Document::Identifier docId);
+};
+
+class CommandCloseAllDocuments : public Command {
+public:
+ CommandCloseAllDocuments(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+class CommandCloseAllDocumentsExceptCurrent : public Command {
+public:
+ CommandCloseAllDocumentsExceptCurrent(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+
+private:
+ void updateActionText(Document::Identifier docId);
+};
+
+class CommandQuitApplication : public Command {
+public:
+ CommandQuitApplication(IAppContext* context);
+ void execute() override;
+};
+
+} // namespace Mayo
diff --git a/src/app/commands_help.cpp b/src/app/commands_help.cpp
new file mode 100644
index 00000000..b7fe8f3f
--- /dev/null
+++ b/src/app/commands_help.cpp
@@ -0,0 +1,45 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#include "commands_help.h"
+
+#include "dialog_about.h"
+#include "qtwidgets_utils.h"
+
+#include
+#include
+#include
+
+namespace Mayo {
+
+CommandReportBug::CommandReportBug(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Report Bug"));
+ this->setAction(action);
+}
+
+void CommandReportBug::execute()
+{
+ QDesktopServices::openUrl(QUrl(QStringLiteral("https://github.com/fougue/mayo/issues")));
+}
+
+CommandAbout::CommandAbout(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("About %1").arg(QApplication::applicationName()));
+ this->setAction(action);
+}
+
+void CommandAbout::execute()
+{
+ auto dlg = new DialogAbout(this->widgetMain());
+ QtWidgetsUtils::asyncDialogExec(dlg);
+}
+
+} // namespace Mayo
diff --git a/src/app/commands_help.h b/src/app/commands_help.h
new file mode 100644
index 00000000..ca880669
--- /dev/null
+++ b/src/app/commands_help.h
@@ -0,0 +1,25 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#pragma once
+
+#include "commands_api.h"
+
+namespace Mayo {
+
+class CommandReportBug : public Command {
+public:
+ CommandReportBug(IAppContext* context);
+ void execute() override;
+};
+
+class CommandAbout : public Command {
+public:
+ CommandAbout(IAppContext* context);
+ void execute() override;
+};
+
+} // namespace Mayo
diff --git a/src/app/commands_tools.cpp b/src/app/commands_tools.cpp
new file mode 100644
index 00000000..a8d52bb9
--- /dev/null
+++ b/src/app/commands_tools.cpp
@@ -0,0 +1,99 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#include "commands_tools.h"
+
+#include "../base/application.h"
+#include "../base/application_item_selection_model.h"
+#include "../gui/gui_application.h"
+#include "../gui/gui_document.h"
+#include "app_module.h"
+#include "dialog_inspect_xde.h"
+#include "dialog_options.h"
+#include "dialog_save_image_view.h"
+#include "qtwidgets_utils.h"
+#include "theme.h"
+
+#include
+
+namespace Mayo {
+
+CommandSaveViewImage::CommandSaveViewImage(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Save View to Image"));
+ action->setToolTip(Command::tr("Save View to Image"));
+ action->setIcon(mayoTheme()->icon(Theme::Icon::Camera));
+ this->setAction(action);
+}
+
+void CommandSaveViewImage::execute()
+{
+ auto guiDoc = this->currentGuiDocument();
+ auto dlg = new DialogSaveImageView(guiDoc->v3dView(), this->widgetMain());
+ QtWidgetsUtils::asyncDialogExec(dlg);
+}
+
+bool CommandSaveViewImage::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0;
+}
+
+CommandInspectXde::CommandInspectXde(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Inspect XDE"));
+ action->setToolTip(Command::tr("Inspect XDE"));
+ this->setAction(action);
+}
+
+void CommandInspectXde::execute()
+{
+ const Span spanAppItem = this->guiApp()->selectionModel()->selectedItems();
+ DocumentPtr doc;
+ for (const ApplicationItem& appItem : spanAppItem) {
+ if (appItem.document()->isXCafDocument()) {
+ doc = appItem.document();
+ break;
+ }
+ }
+
+ if (doc) {
+ auto dlg = new DialogInspectXde(this->widgetMain());
+ dlg->load(doc);
+ QtWidgetsUtils::asyncDialogExec(dlg);
+ }
+}
+
+bool CommandInspectXde::getEnabledStatus() const
+{
+ Span spanSelectedAppItem = this->guiApp()->selectionModel()->selectedItems();
+ const ApplicationItem firstAppItem =
+ !spanSelectedAppItem.empty() ? spanSelectedAppItem.front() : ApplicationItem();
+ return spanSelectedAppItem.size() == 1
+ && firstAppItem.isValid()
+ && firstAppItem.document()->isXCafDocument();
+}
+
+CommandEditOptions::CommandEditOptions(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Options"));
+ action->setToolTip(Command::tr("Options"));
+ this->setAction(action);
+}
+
+void CommandEditOptions::execute()
+{
+ auto dlg = new DialogOptions(AppModule::get()->settings(), this->widgetMain());
+ QtWidgetsUtils::asyncDialogExec(dlg);
+}
+
+} // namespace Mayo {
+
diff --git a/src/app/commands_tools.h b/src/app/commands_tools.h
new file mode 100644
index 00000000..5e3e803e
--- /dev/null
+++ b/src/app/commands_tools.h
@@ -0,0 +1,33 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#pragma once
+
+#include "commands_api.h"
+
+namespace Mayo {
+
+class CommandSaveViewImage : public Command {
+public:
+ CommandSaveViewImage(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+class CommandInspectXde : public Command {
+public:
+ CommandInspectXde(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+class CommandEditOptions : public Command {
+public:
+ CommandEditOptions(IAppContext* context);
+ void execute() override;
+};
+
+} // namespace Mayo
diff --git a/src/app/commands_window.cpp b/src/app/commands_window.cpp
new file mode 100644
index 00000000..971497d2
--- /dev/null
+++ b/src/app/commands_window.cpp
@@ -0,0 +1,142 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#include "commands_window.h"
+
+#include "../base/application.h"
+#include "theme.h"
+
+#include
+#include
+
+namespace Mayo {
+
+CommandMainWidgetToggleFullscreen::CommandMainWidgetToggleFullscreen(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Fullscreen"));
+ action->setToolTip(Command::tr("Switch Fullscreen/Normal"));
+ action->setShortcut(Qt::Key_F11);
+ action->setCheckable(true);
+ action->setChecked(context->widgetMain()->isFullScreen());
+ this->setAction(action);
+}
+
+void CommandMainWidgetToggleFullscreen::execute()
+{
+ auto widget = this->widgetMain();
+ if (widget->isFullScreen()) {
+ if (m_previousWindowState.testFlag(Qt::WindowMaximized))
+ widget->showMaximized();
+ else
+ widget->showNormal();
+ }
+ else {
+ m_previousWindowState = widget->windowState();
+ widget->showFullScreen();
+ }
+}
+
+CommandLeftSidebarWidgetToggle::CommandLeftSidebarWidgetToggle(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setToolTip(Command::tr("Show/Hide Left Sidebar"));
+ action->setShortcut(Qt::ALT + Qt::Key_0);
+ action->setCheckable(true);
+ action->setChecked(context->widgetLeftSidebar()->isVisible());
+ this->setAction(action);
+ this->updateAction();
+ context->widgetLeftSidebar()->installEventFilter(this);
+}
+
+void CommandLeftSidebarWidgetToggle::execute()
+{
+ const bool isVisible = this->context()->widgetLeftSidebar()->isVisible();
+ this->context()->widgetLeftSidebar()->setVisible(!isVisible);
+}
+
+bool CommandLeftSidebarWidgetToggle::getEnabledStatus() const
+{
+ return this->context()->modeWidgetMain() != IAppContext::ModeWidgetMain::Home;
+}
+
+bool CommandLeftSidebarWidgetToggle::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == this->context()->widgetLeftSidebar()) {
+ if (event->type() == QEvent::Show || event->type() == QEvent::Hide) {
+ this->updateAction();
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ return Command::eventFilter(watched, event);
+}
+
+void CommandLeftSidebarWidgetToggle::updateAction()
+{
+ if (this->context()->widgetLeftSidebar()->isVisible()) {
+ this->action()->setText(Command::tr("Hide Left Sidebar"));
+ this->action()->setIcon(mayoTheme()->icon(Theme::Icon::BackSquare));
+ }
+ else {
+ this->action()->setText(Command::tr("Show Left Sidebar"));
+ this->action()->setIcon(mayoTheme()->icon(Theme::Icon::LeftSidebar));
+ }
+
+ this->action()->setToolTip(this->action()->text());
+}
+
+CommandPreviousDocument::CommandPreviousDocument(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Previous Document"));
+ action->setToolTip(Command::tr("Previous Document"));
+ action->setIcon(mayoTheme()->icon(Theme::Icon::Back));
+ action->setShortcut(Qt::ALT + Qt::Key_Left);
+ this->setAction(action);
+}
+
+void CommandPreviousDocument::execute()
+{
+ const int prevDocIndex = this->currentDocumentIndex() - 1;
+ this->context()->setCurrentDocument(this->context()->findDocumentFromIndex(prevDocIndex));
+}
+
+bool CommandPreviousDocument::getEnabledStatus() const
+{
+ return this->app()->documentCount() != 0 && this->currentDocumentIndex() > 0;
+}
+
+CommandNextDocument::CommandNextDocument(IAppContext* context)
+ : Command(context)
+{
+ auto action = new QAction(this);
+ action->setText(Command::tr("Next Document"));
+ action->setToolTip(Command::tr("Next Document"));
+ action->setIcon(mayoTheme()->icon(Theme::Icon::Next));
+ action->setShortcut(Qt::ALT + Qt::Key_Right);
+ this->setAction(action);
+}
+
+void CommandNextDocument::execute()
+{
+ const int nextDocIndex = this->currentDocumentIndex() + 1;
+ this->context()->setCurrentDocument(this->context()->findDocumentFromIndex(nextDocIndex));
+}
+
+bool CommandNextDocument::getEnabledStatus() const
+{
+ const int appDocumentCount = this->app()->documentCount();
+ return appDocumentCount != 0 && this->currentDocumentIndex() < appDocumentCount - 1;
+}
+
+} // namespace Mayo
diff --git a/src/app/commands_window.h b/src/app/commands_window.h
new file mode 100644
index 00000000..008ffbc8
--- /dev/null
+++ b/src/app/commands_window.h
@@ -0,0 +1,48 @@
+/****************************************************************************
+** Copyright (c) 2022, Fougue Ltd.
+** All rights reserved.
+** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
+****************************************************************************/
+
+#pragma once
+
+#include "commands_api.h"
+
+namespace Mayo {
+
+class CommandMainWidgetToggleFullscreen : public Command {
+public:
+ CommandMainWidgetToggleFullscreen(IAppContext* context);
+ void execute() override;
+
+private:
+ Qt::WindowStates m_previousWindowState = Qt::WindowNoState;
+};
+
+class CommandLeftSidebarWidgetToggle : public Command {
+public:
+ CommandLeftSidebarWidgetToggle(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+private:
+ void updateAction();
+};
+
+class CommandPreviousDocument : public Command {
+public:
+ CommandPreviousDocument(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+class CommandNextDocument : public Command {
+public:
+ CommandNextDocument(IAppContext* context);
+ void execute() override;
+ bool getEnabledStatus() const override;
+};
+
+} // namespace Mayo
diff --git a/src/app/dialog_inspect_xde.cpp b/src/app/dialog_inspect_xde.cpp
index 015aa886..90ea81b6 100644
--- a/src/app/dialog_inspect_xde.cpp
+++ b/src/app/dialog_inspect_xde.cpp
@@ -12,12 +12,12 @@
#include "../base/meta_enum.h"
#include "../base/settings.h"
#include "../base/tkernel_utils.h"
-#include "../gui/qtgui_utils.h"
#include "app_module.h"
#include "qmeta_tdf_label.h"
#include "qstring_conv.h"
#include "qstring_utils.h"
-#include "widgets_utils.h"
+#include "qtgui_utils.h"
+#include "qtwidgets_utils.h"
#include "ui_dialog_inspect_xde.h"
#include
@@ -55,7 +55,12 @@
# include
#endif
+#include
+#include
+
+#include
#include
+#include
namespace Mayo {
@@ -283,14 +288,73 @@ static void loadLabelMaterialProperties(
}
#if OCC_VERSION_HEX >= OCC_VERSION_CHECK(7, 4, 0)
-static QTreeWidgetItem* createPropertyTreeItem(
- const QString& text, const opencascade::handle& imgTexture)
+
+// Provides a QTreeWidgetItem specialized to display an image file with a tooltip
+// QTreeWidgetItem::setToolTip() could be used but it forces all image files to be loaded on
+// tree item construction
+// This helper allows "lazy" loading of the image files
+class ImageFileTreeWidgetItem : public QTreeWidgetItem {
+public:
+ void setImageFilePath(int col, const QString& strFilePath)
+ {
+ const ItemData item{strFilePath, {}};
+ m_mapColumnItemData.insert({ col, item });
+ }
+
+ QVariant data(int column, int role) const override
+ {
+ if (role != Qt::ToolTipRole)
+ return QTreeWidgetItem::data(column, role);
+
+ auto itItem = m_mapColumnItemData.find(column);
+ ItemData* ptrItem = itItem != m_mapColumnItemData.end() ? &itItem->second : nullptr;
+ if (!ptrItem)
+ return {};
+
+ if (ptrItem->strToolTip.isEmpty()) {
+ const QPixmap pixmap(ptrItem->strFilePath);
+ if (!pixmap.isNull()) {
+ QBuffer bufferPixmap;
+ const QPixmap pixmapClamped = pixmap.scaledToWidth(std::min(pixmap.width(), 400));
+ pixmapClamped.save(&bufferPixmap, "PNG");
+ const auto imageSize = QFileInfo(ptrItem->strFilePath).size();
+ const QString strImageSize = QStringUtils::bytesText(imageSize, appDefaultTextOptions().locale);
+ ptrItem->strToolTip =
+ QString("%4
")
+ .arg(QString::fromLatin1(bufferPixmap.data().toBase64()))
+ .arg(pixmapClamped.width())
+ .arg(pixmapClamped.height())
+ .arg(DialogInspectXde::tr("File Size: %1
Dimensions: %2x%3 Depth: %4")
+ .arg(strImageSize).arg(pixmap.width()).arg(pixmap.height()).arg(pixmap.depth()))
+ ;
+ }
+ else {
+ ptrItem->strToolTip = DialogInspectXde::tr("Error when loading texture file(invalid path?)");
+ }
+ }
+
+ return ptrItem->strToolTip;
+ }
+
+private:
+ struct ItemData {
+ QString strFilePath;
+ QString strToolTip;
+ };
+
+ mutable std::unordered_map m_mapColumnItemData;
+};
+
+static QTreeWidgetItem* createPropertyTreeItem(const QString& text, const Handle(Image_Texture)& imgTexture)
{
if (imgTexture.IsNull())
return static_cast(nullptr);
- auto item = createPropertyTreeItem(text);
- item->setText(1, to_QString(imgTexture->FilePath()));
+ const QString strTextureFilePath = to_QString(imgTexture->FilePath());
+ auto item = new ImageFileTreeWidgetItem;
+ item->setText(0, text);
+ item->setText(1, strTextureFilePath);
+ item->setImageFilePath(1, strTextureFilePath);
return item;
}
#endif
@@ -655,7 +719,7 @@ void DialogInspectXde::load(const Handle_TDocStd_Document& doc)
{
m_doc = doc;
if (!XCAFDoc_DocumentTool::IsXCAFDocument(doc)) {
- WidgetsUtils::asyncMsgBoxCritical(
+ QtWidgetsUtils::asyncMsgBoxCritical(
this, tr("Error"), tr("This document is not suitable for XDE"));
return;
}
diff --git a/src/app/dialog_options.cpp b/src/app/dialog_options.cpp
index 587d4e04..da01a316 100644
--- a/src/app/dialog_options.cpp
+++ b/src/app/dialog_options.cpp
@@ -10,13 +10,13 @@
#include "../base/settings.h"
#include "../base/property_builtins.h"
#include "../base/property_enumeration.h"
-#include "../gui/qtgui_utils.h"
#include "app_module.h"
#include "item_view_buttons.h"
#include "qsettings_storage.h"
#include "qstring_conv.h"
+#include "qtgui_utils.h"
+#include "qtwidgets_utils.h"
#include "theme.h"
-#include "widgets_utils.h"
#include "ui_dialog_options.h"
#include
@@ -147,7 +147,8 @@ DialogOptions::DialogOptions(Settings* settings, QWidget* parent)
treeViewBtns->addButton(
idBtnRestore,
mayoTheme()->icon(Theme::Icon::Reload),
- tr("Restore default values"));
+ tr("Restore default values")
+ );
treeViewBtns->setButtonDetection(idBtnRestore, -1, {});
treeViewBtns->setButtonDisplayColumn(idBtnRestore, 0);
treeViewBtns->setButtonDisplayModes(idBtnRestore, ItemViewButtons::DisplayWhenItemSelected);
@@ -204,15 +205,14 @@ DialogOptions::DialogOptions(Settings* settings, QWidget* parent)
}
// Enable/disable editor widget when the corresponding setting status is changed
- QObject::connect(settings, &Settings::enabled, this, [=](const Property* setting, bool on) {
+ settings->signalEnabled.connectSlot([=](const Property* setting, bool on) {
QWidget* editor = CppUtils::findValue(setting, m_mapSettingEditor);
if (editor)
editor->setEnabled(on);
});
// Backup initial value of changed settings, so they can be restored on dialog cancellation
- auto connSettingsAboutToChange =
- QObject::connect(m_settings, &Settings::aboutToChange, this, [=](Property* property) {
+ m_connSettingsAboutToChange = m_settings->signalAboutToChange.connectSlot([=](Property* property) {
if (m_mapSettingInitialValue.find(property) == m_mapSettingInitialValue.cend()) {
const Settings::Variant propertyValue = m_settings->propertyValueConversion().toVariant(*property);
m_mapSettingInitialValue.insert({ property, propertyValue});
@@ -220,8 +220,7 @@ DialogOptions::DialogOptions(Settings* settings, QWidget* parent)
});
// Synchronize editor widget when value of the corresponding property is changed
- auto connSettingsChanged =
- QObject::connect(m_settings, &Settings::changed, this, [=](const Property* property) {
+ m_connSettingsChanged = m_settings->signalChanged.connectSlot([=](const Property* property) {
auto itFound = m_mapSettingEditor.find(property);
if (itFound != m_mapSettingEditor.cend())
this->syncEditor(itFound->second);
@@ -229,13 +228,12 @@ DialogOptions::DialogOptions(Settings* settings, QWidget* parent)
// When a setting is clicked in the "right-side" view then scroll to and select corresponding
// tree item in the "left-side" view
- QObject::connect(m_ui->listWidget_Settings, &QListWidget::currentRowChanged, [=](int listRow) {
+ QObject::connect(m_ui->listWidget_Settings, &QListWidget::currentRowChanged, this, [=](int listRow) {
auto listItem = m_ui->listWidget_Settings->item(listRow);
const QVariant variantNodeId = listItem ? listItem->data(ItemSettingNodeId_Role) : QVariant();
const Qt::MatchFlags matchFlags = Qt::MatchRecursive | Qt::MatchExactly;
const QModelIndex indexFirst = treeModel->index(0, 0);
- const QModelIndexList indexList = treeModel->match(
- indexFirst, ItemSettingNodeId_Role, variantNodeId, 1, matchFlags);
+ const QModelIndexList indexList = treeModel->match(indexFirst, ItemSettingNodeId_Role, variantNodeId, 1, matchFlags);
if (!indexList.isEmpty()) {
m_ui->treeView_GroupSections->scrollTo(indexList.front(), QAbstractItemView::PositionAtTop);
QSignalBlocker _(m_ui->treeView_GroupSections);
@@ -259,16 +257,11 @@ DialogOptions::DialogOptions(Settings* settings, QWidget* parent)
});
// Action for "Cancel" button : restore changed properties to their initial values
- QObject::connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, [=]{
- QObject::disconnect(connSettingsAboutToChange);
- QObject::disconnect(connSettingsChanged);
- for (const auto& [prop, propInitialValue] : m_mapSettingInitialValue)
- m_settings->propertyValueConversion().fromVariant(prop, propInitialValue);
- });
+ QObject::connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &DialogOptions::cancelChanges);
// Action for "Restore defaults" button
auto btnRestoreDefaults = m_ui->buttonBox->button(QDialogButtonBox::RestoreDefaults);
- QObject::connect(btnRestoreDefaults, &QPushButton::clicked, m_settings, &Settings::resetAll);
+ QObject::connect(btnRestoreDefaults, &QPushButton::clicked, this, [=]{ m_settings->resetAll(); });
// Actions for "Exchange" button
auto btnExchange = m_ui->buttonBox->addButton(tr("Exchange"), QDialogButtonBox::ActionRole);
@@ -282,10 +275,12 @@ DialogOptions::DialogOptions(Settings* settings, QWidget* parent)
DialogOptions::~DialogOptions()
{
+ m_connSettingsAboutToChange.disconnect();
+ m_connSettingsChanged.disconnect();
delete m_ui;
}
-void DialogOptions::setPropertyEditorFactory(std::unique_ptr editorFactory)
+void DialogOptions::setPropertyEditorFactory(std::unique_ptr editorFactory)
{
m_editorFactory = std::move(editorFactory);
}
@@ -359,12 +354,12 @@ void DialogOptions::loadFromFile()
const QFileInfo fi(filepath);
if (!fi.exists()) {
- WidgetsUtils::asyncMsgBoxCritical(this, tr("Error"), tr("'%1' doesn't exist").arg(filepath));
+ QtWidgetsUtils::asyncMsgBoxCritical(this, tr("Error"), tr("'%1' doesn't exist").arg(filepath));
return;
}
if (!fi.isReadable()) {
- WidgetsUtils::asyncMsgBoxCritical(this, tr("Error"), tr("'%1' is not readable").arg(filepath));
+ QtWidgetsUtils::asyncMsgBoxCritical(this, tr("Error"), tr("'%1' is not readable").arg(filepath));
return;
}
@@ -384,7 +379,18 @@ void DialogOptions::saveAs()
m_settings->saveAs(&fileSettings, &AppModule::excludeSettingPredicate);
fileSettings.sync();
if (fileSettings.get().status() != QSettings::NoError)
- WidgetsUtils::asyncMsgBoxCritical(this, tr("Error"), tr("Error when writing to'%1'").arg(filepath));
+ QtWidgetsUtils::asyncMsgBoxCritical(this, tr("Error"), tr("Error when writing to'%1'").arg(filepath));
+}
+
+void DialogOptions::cancelChanges()
+{
+ m_connSettingsAboutToChange.block(true);
+ m_connSettingsChanged.block(true);
+ for (const auto& [prop, propInitialValue] : m_mapSettingInitialValue)
+ m_settings->propertyValueConversion().fromVariant(prop, propInitialValue);
+
+ m_connSettingsAboutToChange.block(false);
+ m_connSettingsChanged.block(false);
}
void DialogOptions::handleTreeViewButtonClick_restoreDefaults(const QModelIndex& index)
@@ -405,7 +411,7 @@ void DialogOptions::handleTreeViewButtonClick_restoreDefaults(const QModelIndex&
menu->addAction(tr("Restore values for the whole group"), this, [=]{
m_settings->resetGroup(groupIndex);
});
- WidgetsUtils::asyncMenuExec(menu);
+ QtWidgetsUtils::asyncMenuExec(menu);
}
else {
m_settings->resetGroup(groupIndex);
diff --git a/src/app/dialog_options.h b/src/app/dialog_options.h
index e82185cf..1025ac54 100644
--- a/src/app/dialog_options.h
+++ b/src/app/dialog_options.h
@@ -22,8 +22,8 @@ class DialogOptions : public QDialog {
DialogOptions(Settings* settings, QWidget* parent = nullptr);
~DialogOptions();
- PropertyEditorFactory* editorFactory() const { return m_editorFactory.get(); }
- void setPropertyEditorFactory(std::unique_ptr editorFactory);
+ IPropertyEditorFactory* editorFactory() const { return m_editorFactory.get(); }
+ void setPropertyEditorFactory(std::unique_ptr editorFactory);
private:
QWidget* createEditor(Property* property, QWidget* parentWidget) const;
@@ -31,14 +31,17 @@ class DialogOptions : public QDialog {
void loadFromFile();
void saveAs();
+ void cancelChanges();
void handleTreeViewButtonClick_restoreDefaults(const QModelIndex& index);
class Ui_DialogOptions* m_ui = nullptr;
- std::unique_ptr m_editorFactory;
+ std::unique_ptr m_editorFactory;
std::unordered_map m_mapSettingEditor;
std::unordered_map m_mapSettingInitialValue;
Settings* m_settings = nullptr;
+ SignalConnectionHandle m_connSettingsAboutToChange;
+ SignalConnectionHandle m_connSettingsChanged;
};
} // namespace Mayo
diff --git a/src/app/dialog_save_image_view.cpp b/src/app/dialog_save_image_view.cpp
index c7d01a1d..960229ee 100644
--- a/src/app/dialog_save_image_view.cpp
+++ b/src/app/dialog_save_image_view.cpp
@@ -6,7 +6,7 @@
#include "dialog_save_image_view.h"
#include "ui_dialog_save_image_view.h"
-#include "widgets_utils.h"
+#include "qtwidgets_utils.h"
#include "../graphics/graphics_utils.h"
#include
@@ -33,7 +33,7 @@ static QImage qtImageTemp(const Image_PixMap& occImg)
} // namespace Internal
-DialogSaveImageView::DialogSaveImageView(const Handle_V3d_View& view, QWidget *parent)
+DialogSaveImageView::DialogSaveImageView(const Handle_V3d_View& view, QWidget* parent)
: QDialog(parent),
m_ui(new Ui_DialogSaveImageView),
m_view(view)
@@ -97,7 +97,7 @@ void DialogSaveImageView::saveFile()
saveOk = img.save(fileName, format);
}
if (!saveOk) {
- WidgetsUtils::asyncMsgBoxCritical(
+ QtWidgetsUtils::asyncMsgBoxCritical(
this, tr("Error"), tr("Failed to save image '%1'").arg(fileName));
}
}
diff --git a/src/app/dialog_save_image_view.h b/src/app/dialog_save_image_view.h
index f368d279..66162971 100644
--- a/src/app/dialog_save_image_view.h
+++ b/src/app/dialog_save_image_view.h
@@ -16,7 +16,7 @@ namespace Mayo {
class DialogSaveImageView : public QDialog {
Q_OBJECT
public:
- DialogSaveImageView(const Handle_V3d_View& view, QWidget *parent = nullptr);
+ DialogSaveImageView(const Handle_V3d_View& view, QWidget* parent = nullptr);
~DialogSaveImageView();
private:
diff --git a/src/app/dialog_task_manager.cpp b/src/app/dialog_task_manager.cpp
index 9ae45309..fc195275 100644
--- a/src/app/dialog_task_manager.cpp
+++ b/src/app/dialog_task_manager.cpp
@@ -116,10 +116,10 @@ DialogTaskManager::DialogTaskManager(TaskManager* taskMgr, QWidget* parent)
this->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
this->setWindowModality(Qt::WindowModal);
- QObject::connect(taskMgr, &TaskManager::started, this, &DialogTaskManager::onTaskStarted);
- QObject::connect(taskMgr, &TaskManager::ended, this, &DialogTaskManager::onTaskEnded);
- QObject::connect(taskMgr, &TaskManager::progressChanged, this, &DialogTaskManager::onTaskProgress);
- QObject::connect(taskMgr, &TaskManager::progressStep, this, &DialogTaskManager::onTaskProgressStep);
+ taskMgr->signalStarted.connectSlot(&DialogTaskManager::onTaskStarted, this);
+ taskMgr->signalEnded.connectSlot(&DialogTaskManager::onTaskEnded, this);
+ taskMgr->signalProgressChanged.connectSlot(&DialogTaskManager::onTaskProgress, this);
+ taskMgr->signalProgressStep.connectSlot(&DialogTaskManager::onTaskProgressStep, this);
}
DialogTaskManager::~DialogTaskManager()
@@ -185,7 +185,7 @@ void DialogTaskManager::onTaskProgressStep(TaskId taskId, std::string_view name)
const QString taskTitle = to_QString(m_taskMgr->title(taskId));
QString text = taskTitle;
if (!name.empty()) {
- const QLocale& locale = AppModule::get()->locale();
+ const QLocale& locale = AppModule::get()->qtLocale();
if (!text.isEmpty())
QStringUtils::append(&text, tr(" / "), locale);
diff --git a/src/app/document_property_group.cpp b/src/app/document_property_group.cpp
index 3ed202c3..24b987b7 100644
--- a/src/app/document_property_group.cpp
+++ b/src/app/document_property_group.cpp
@@ -23,12 +23,12 @@ DocumentPropertyGroup::DocumentPropertyGroup(const DocumentPtr& doc)
const auto fileSize = filepathFileSize(doc->filePath());
auto appModule = AppModule::get();
- const QString strFileSize = QStringUtils::bytesText(fileSize, appModule->locale());
- this->strFileSize.setValue(to_stdString(strFileSize));
+ const QString qstrFileSize = QStringUtils::bytesText(fileSize, appModule->qtLocale());
+ this->strFileSize.setValue(to_stdString(qstrFileSize));
const QFileInfo fileInfo = filepathTo(doc->filePath());
- const QString strCreated = appModule->locale().toString(fileInfo.birthTime(), QLocale::ShortFormat);
- const QString strModified = appModule->locale().toString(fileInfo.lastModified(), QLocale::ShortFormat);
+ const QString strCreated = appModule->qtLocale().toString(fileInfo.birthTime(), QLocale::ShortFormat);
+ const QString strModified = appModule->qtLocale().toString(fileInfo.lastModified(), QLocale::ShortFormat);
this->strCreatedDateTime.setValue(to_stdString(strCreated));
this->strModifiedDateTime.setValue(to_stdString(strModified));
diff --git a/src/app/document_property_group.h b/src/app/document_property_group.h
index 6d8cfdec..d6a35eff 100644
--- a/src/app/document_property_group.h
+++ b/src/app/document_property_group.h
@@ -11,6 +11,8 @@
namespace Mayo {
+// Provides relevant properties for a Document object
+// TODO Connect to Document "changed" signals to update the properties
class DocumentPropertyGroup : public PropertyGroup {
MAYO_DECLARE_TEXT_ID_FUNCTIONS(Mayo::DocumentPropertyGroup)
public:
diff --git a/src/app/document_tree_node_properties_providers.cpp b/src/app/document_tree_node_properties_providers.cpp
index 4a277d45..94db4dcc 100644
--- a/src/app/document_tree_node_properties_providers.cpp
+++ b/src/app/document_tree_node_properties_providers.cpp
@@ -7,15 +7,23 @@
#include "document_tree_node_properties_providers.h"
#include "../base/caf_utils.h"
-#include "../base/data_triangulation.h"
+#include "../base/label_data.h"
+#include "../base/triangulation_annex_data.h"
#include "../base/document.h"
#include "../base/document_tree_node.h"
+#include "../base/mesh_access.h"
#include "../base/mesh_utils.h"
#include "../base/meta_enum.h"
+#include "../base/point_cloud_data.h"
#include "../base/xcaf.h"
+#include "../graphics/graphics_mesh_object_driver.h"
+#include "../graphics/graphics_point_cloud_object_driver.h"
+#include "../graphics/graphics_shape_object_driver.h"
#include "qstring_conv.h"
+#include
#include
+#include
namespace Mayo {
@@ -194,7 +202,7 @@ class XCaf_DocumentTreeNodePropertiesProvider::Properties : public PropertyGroup
bool XCaf_DocumentTreeNodePropertiesProvider::supports(const DocumentTreeNode& treeNode) const
{
- return XCaf::isShape(treeNode.label());
+ return GraphicsShapeObjectDriver::shapeSupportStatus(treeNode.label()) == GraphicsObjectDriver::Support::Complete;
}
std::unique_ptr
@@ -211,15 +219,15 @@ class Mesh_DocumentTreeNodePropertiesProvider::Properties : public PropertyGroup
public:
Properties(const DocumentTreeNode& treeNode)
{
- auto attrTriangulation = CafUtils::findAttribute(treeNode.label());
- Handle_Poly_Triangulation polyTri;
- if (!attrTriangulation.IsNull())
- polyTri = attrTriangulation->Get();
-
- m_propertyNodeCount.setValue(!polyTri.IsNull() ? polyTri->NbNodes() : 0);
- m_propertyTriangleCount.setValue(!polyTri.IsNull() ? polyTri->NbTriangles() : 0);
- m_propertyArea.setQuantity(MeshUtils::triangulationArea(polyTri) * Quantity_SquareMillimeter);
- m_propertyVolume.setQuantity(MeshUtils::triangulationVolume(polyTri) * Quantity_CubicMillimeter);
+ Handle_Poly_Triangulation mesh;
+ IMeshAccess_visitMeshes(treeNode, [&](const IMeshAccess& access) {
+ mesh = access.triangulation();
+ });
+
+ m_propertyNodeCount.setValue(!mesh.IsNull() ? mesh->NbNodes() : 0);
+ m_propertyTriangleCount.setValue(!mesh.IsNull() ? mesh->NbTriangles() : 0);
+ m_propertyArea.setQuantity(MeshUtils::triangulationArea(mesh) * Quantity_SquareMillimeter);
+ m_propertyVolume.setQuantity(MeshUtils::triangulationVolume(mesh) * Quantity_CubicMillimeter);
for (Property* property : this->properties())
property->setUserReadOnly(true);
}
@@ -232,7 +240,7 @@ class Mesh_DocumentTreeNodePropertiesProvider::Properties : public PropertyGroup
bool Mesh_DocumentTreeNodePropertiesProvider::supports(const DocumentTreeNode& treeNode) const
{
- return CafUtils::hasAttribute(treeNode.label());
+ return GraphicsMeshObjectDriver::meshSupportStatus(treeNode.label()) == GraphicsObjectDriver::Support::Complete;
}
std::unique_ptr
@@ -244,4 +252,48 @@ Mesh_DocumentTreeNodePropertiesProvider::properties(const DocumentTreeNode& tree
return std::make_unique(treeNode);
}
+class PointCloud_DocumentTreeNodePropertiesProvider::Properties : public PropertyGroupSignals {
+ MAYO_DECLARE_TEXT_ID_FUNCTIONS(Mayo::PointCloud_DocumentTreeNodeProperties)
+public:
+ Properties(const DocumentTreeNode& treeNode)
+ {
+ auto attrPointCloudData = CafUtils::findAttribute(treeNode.label());
+
+ const bool hasAttrData = !attrPointCloudData.IsNull() && !attrPointCloudData->points().IsNull();
+ m_propertyPointCount.setValue(hasAttrData ? attrPointCloudData->points()->VertexNumber() : 0);
+ m_propertyHasColors.setValue(hasAttrData ? attrPointCloudData->points()->HasVertexColors() : false);
+ if (hasAttrData) {
+ Bnd_Box bndBox;
+ const int pntCount = attrPointCloudData->points()->VertexNumber();
+ for (int i = 1; i <= pntCount; ++i)
+ bndBox.Add(attrPointCloudData->points()->Vertice(i));
+
+ m_propertyCornerMin.setValue(bndBox.CornerMin());
+ m_propertyCornerMax.setValue(bndBox.CornerMax());
+ }
+
+ for (Property* property : this->properties())
+ property->setUserReadOnly(true);
+ }
+
+ PropertyInt m_propertyPointCount{ this, textId("PointCount") };
+ PropertyBool m_propertyHasColors{ this, textId("HasColors") };
+ PropertyOccPnt m_propertyCornerMin{ this, textId("CornerMin") };
+ PropertyOccPnt m_propertyCornerMax{ this, textId("CornerMax") };
+};
+
+bool PointCloud_DocumentTreeNodePropertiesProvider::supports(const DocumentTreeNode& treeNode) const
+{
+ return GraphicsPointCloudObjectDriver::pointCloudSupportStatus(treeNode.label()) == GraphicsObjectDriver::Support::Complete;
+}
+
+std::unique_ptr
+PointCloud_DocumentTreeNodePropertiesProvider::properties(const DocumentTreeNode& treeNode) const
+{
+ if (!treeNode.isValid())
+ return {};
+
+ return std::make_unique(treeNode);
+}
+
} // namespace Mayo
diff --git a/src/app/document_tree_node_properties_providers.h b/src/app/document_tree_node_properties_providers.h
index 29c3d0ef..90fd1dde 100644
--- a/src/app/document_tree_node_properties_providers.h
+++ b/src/app/document_tree_node_properties_providers.h
@@ -13,6 +13,7 @@
namespace Mayo {
+// Provides relevant properties for tree node pointing to XCAF data
class XCaf_DocumentTreeNodePropertiesProvider : public DocumentTreeNodePropertiesProvider {
public:
bool supports(const DocumentTreeNode& treeNode) const override;
@@ -22,6 +23,7 @@ class XCaf_DocumentTreeNodePropertiesProvider : public DocumentTreeNodePropertie
class Properties;
};
+// Provides relevant properties for tree node pointing to mesh data
class Mesh_DocumentTreeNodePropertiesProvider : public DocumentTreeNodePropertiesProvider {
public:
bool supports(const DocumentTreeNode& treeNode) const override;
@@ -31,4 +33,13 @@ class Mesh_DocumentTreeNodePropertiesProvider : public DocumentTreeNodePropertie
class Properties;
};
+class PointCloud_DocumentTreeNodePropertiesProvider : public DocumentTreeNodePropertiesProvider {
+public:
+ bool supports(const DocumentTreeNode& treeNode) const override;
+ std::unique_ptr properties(const DocumentTreeNode& treeNode) const override;
+
+private:
+ class Properties;
+};
+
} // namespace Mayo
diff --git a/src/app/filepath_conv.h b/src/app/filepath_conv.h
index 7743c071..4e178d8d 100644
--- a/src/app/filepath_conv.h
+++ b/src/app/filepath_conv.h
@@ -14,7 +14,7 @@
namespace Mayo {
// Returns a FilePath object constructed from input
-inline FilePath filepathFrom(const QByteArray& bytes) { return std::filesystem::u8path(bytes.constData()); }
+inline FilePath filepathFrom(const QByteArray& bytes) { return filepathFrom(std::string_view{ bytes.constData() }); }
inline FilePath filepathFrom(const QString& str) { return reinterpret_cast(str.utf16()); }
inline FilePath filepathFrom(const QFileInfo& fi) { return filepathFrom(fi.filePath()); }
diff --git a/src/app/grid_helper.cpp b/src/app/grid_helper.cpp
index 40bf4673..b3605bf2 100644
--- a/src/app/grid_helper.cpp
+++ b/src/app/grid_helper.cpp
@@ -32,25 +32,32 @@ void ProxyModel::setSourceModel(QAbstractItemModel* newModel)
});
QObject::connect(
newModel, &QAbstractItemModel::modelAboutToBeReset,
- this, &ProxyModel::beginResetModel);
+ this, &ProxyModel::beginResetModel
+ );
QObject::connect(
newModel, &QAbstractItemModel::modelReset,
- this, &ProxyModel::endResetModel);
+ this, &ProxyModel::endResetModel
+ );
QObject::connect(
newModel, &QAbstractItemModel::rowsAboutToBeInserted,
- this, &ProxyModel::beginResetModel);
+ this, &ProxyModel::beginResetModel
+ );
QObject::connect(
newModel, &QAbstractItemModel::rowsInserted,
- this, &ProxyModel::endResetModel);
+ this, &ProxyModel::endResetModel
+ );
QObject::connect(
newModel, &QAbstractItemModel::rowsAboutToBeRemoved,
- this, &ProxyModel::beginResetModel);
+ this, &ProxyModel::beginResetModel
+ );
QObject::connect(
newModel, &QAbstractItemModel::rowsRemoved,
- this, &ProxyModel::endResetModel);
+ this, &ProxyModel::endResetModel
+ );
QObject::connect(
newModel, &QAbstractItemModel::dataChanged,
- this, &ProxyModel::onDataChanged);
+ this, &ProxyModel::onDataChanged
+ );
}
}
diff --git a/src/app/grid_helper.h b/src/app/grid_helper.h
index 87ce4fcf..c276477f 100644
--- a/src/app/grid_helper.h
+++ b/src/app/grid_helper.h
@@ -10,8 +10,11 @@
#include
namespace Mayo {
+
+// Provides helper tools for "grid" view
namespace GridHelper {
+// Provides a proxy model to layout items along a grid(this setColumnCount())
class ProxyModel : public QAbstractItemModel {
public:
void setSourceModel(QAbstractItemModel* newModel);
@@ -32,12 +35,14 @@ class ProxyModel : public QAbstractItemModel {
private:
void onDataChanged(
- const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles);
+ const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles
+ );
QAbstractItemModel* m_sourceModel = nullptr;
int m_columnCount = 1;
};
+// Provides Qt view with grid layout
class View : public QTableView {
public:
View(QWidget* parent);
diff --git a/src/gui/gui_document_list_model.cpp b/src/app/gui_document_list_model.cpp
similarity index 82%
rename from src/gui/gui_document_list_model.cpp
rename to src/app/gui_document_list_model.cpp
index 677d7c33..5bbd27c3 100644
--- a/src/gui/gui_document_list_model.cpp
+++ b/src/app/gui_document_list_model.cpp
@@ -8,8 +8,8 @@
#include "../base/application.h"
#include "../base/document.h"
-#include "gui_application.h"
-#include "gui_document.h"
+#include "../gui/gui_application.h"
+#include "../gui/gui_document.h"
namespace Mayo {
@@ -19,15 +19,10 @@ GuiDocumentListModel::GuiDocumentListModel(const GuiApplication* guiApp, QObject
for (const GuiDocument* doc : guiApp->guiDocuments())
this->appendGuiDocument(doc);
- QObject::connect(
- guiApp, &GuiApplication::guiDocumentAdded,
- this, &GuiDocumentListModel::appendGuiDocument);
- QObject::connect(
- guiApp, &GuiApplication::guiDocumentErased,
- this, &GuiDocumentListModel::removeGuiDocument);
- QObject::connect(
- guiApp->application().get(), &Application::documentNameChanged,
- this, &GuiDocumentListModel::onDocumentNameChanged);
+ auto app = guiApp->application();
+ app->signalDocumentNameChanged.connectSlot(&GuiDocumentListModel::onDocumentNameChanged, this);
+ guiApp->signalGuiDocumentAdded.connectSlot(&GuiDocumentListModel::appendGuiDocument, this);
+ guiApp->signalGuiDocumentErased.connectSlot(&GuiDocumentListModel::removeGuiDocument, this);
}
QVariant GuiDocumentListModel::data(const QModelIndex& index, int role) const
diff --git a/src/gui/gui_document_list_model.h b/src/app/gui_document_list_model.h
similarity index 86%
rename from src/gui/gui_document_list_model.h
rename to src/app/gui_document_list_model.h
index 86d95437..06a5a569 100644
--- a/src/gui/gui_document_list_model.h
+++ b/src/app/gui_document_list_model.h
@@ -15,6 +15,8 @@ namespace Mayo {
class GuiApplication;
class GuiDocument;
+// Provides a Qt item model of the documents owned by a GuiApplication object
+// Contents is automatically updated in reaction of Application and GuiApplication signals
class GuiDocumentListModel : public QAbstractListModel {
public:
GuiDocumentListModel(const GuiApplication* guiApp, QObject* parent = nullptr);
diff --git a/src/app/item_view_buttons.cpp b/src/app/item_view_buttons.cpp
index 28065cfe..c2c54374 100644
--- a/src/app/item_view_buttons.cpp
+++ b/src/app/item_view_buttons.cpp
@@ -20,8 +20,8 @@ namespace Mayo {
namespace {
-template
-void checkedAssign(VALUE_TYPE CLASS::*attrMember, CLASS* object, CALL_VALUE_TYPE value)
+template
+void checkedAssign(ValueType ClassType::*attrMember, ClassType* object, CallValueType value)
{
if (object && attrMember)
object->*attrMember = value;
@@ -174,7 +174,7 @@ void ItemViewButtons::Private::resetButtonUnderMouseState()
*
* \param btnId Identifier of the button clicked (this is the id that was
* passed to addButton())
- * \param index Index of the item model where the button click occured
+ * \param index Index of the item model where the button click occurred
*/
ItemViewButtons::ItemViewButtons(QAbstractItemView* view, QObject* parent)
diff --git a/src/app/list_helper.cpp b/src/app/list_helper.cpp
index 1f595a81..c68ca10e 100644
--- a/src/app/list_helper.cpp
+++ b/src/app/list_helper.cpp
@@ -6,7 +6,7 @@
#include "list_helper.h"
-#include "../gui/qtgui_utils.h"
+#include "qtgui_utils.h"
#include
#include
#include
@@ -97,12 +97,11 @@ void Model::setStorage(std::unique_ptr ptr)
}
ItemDelegate::ItemDelegate(QObject* parent)
- : QStyledItemDelegate(parent)
+ : QStyledItemDelegate(parent),
+ m_frameColor(220, 220, 220),
+ m_pixmapColor(qApp->palette().color(QPalette::Button)),
+ m_textColor(qApp->palette().color(QPalette::WindowText))
{
- m_frameColor = QColor(220, 220, 220);
- m_pixmapColor = qApp->palette().color(QPalette::Button);
- m_textColor = qApp->palette().color(QPalette::WindowText);
-
m_itemAnimation.setDuration(250);
m_itemAnimation.setEasingCurve(QEasingCurve::OutQuad);
m_itemAnimation.setLoopCount(1);
@@ -110,7 +109,8 @@ ItemDelegate::ItemDelegate(QObject* parent)
m_itemAnimation.setEndValue(m_itemSize.height());
QObject::connect(
&m_itemAnimation, &QVariantAnimation::valueChanged,
- this, &ItemDelegate::drawItem);
+ this, &ItemDelegate::drawItem
+ );
}
void ItemDelegate::paint(
diff --git a/src/app/list_helper.h b/src/app/list_helper.h
index 02caafd6..81c02013 100644
--- a/src/app/list_helper.h
+++ b/src/app/list_helper.h
@@ -30,12 +30,12 @@ struct ModelStorage {
virtual const ModelItem* at(int i) const = 0;
};
-template struct DefaultModelStorage : public ModelStorage {
- static_assert(!std::is_pointer- ::value);
- static_assert(std::is_base_of::value);
+template struct DefaultModelStorage : public ModelStorage {
+ static_assert(!std::is_pointer::value);
+ static_assert(std::is_base_of::value);
int count() const override { return int(m_items.size()); }
- const ITEM* at(int i) const override { return &m_items.at(i); }
- std::vector
- m_items;
+ const ItemType* at(int i) const override { return &m_items.at(i); }
+ std::vector m_items;
};
class Model : public QAbstractListModel {
@@ -105,7 +105,7 @@ class ItemDelegate : public QStyledItemDelegate {
mutable QModelIndex m_previousIndex;
mutable QVariantAnimation m_itemAnimation;
mutable QRect m_area;
- mutable QAbstractItemView* m_widget;
+ mutable QAbstractItemView* m_widget = nullptr;
mutable QPixmap m_blurredPixmap;
};
diff --git a/src/app/main.cpp b/src/app/main.cpp
index a6da0314..76e4b510 100644
--- a/src/app/main.cpp
+++ b/src/app/main.cpp
@@ -12,12 +12,14 @@
#include "../io_gmio/io_gmio.h"
#include "../io_image/io_image.h"
#include "../io_occ/io_occ.h"
+#include "../io_off/io_off_reader.h"
+#include "../io_off/io_off_writer.h"
#include "../io_ply/io_ply_reader.h"
#include "../io_ply/io_ply_writer.h"
#include "../graphics/graphics_mesh_object_driver.h"
+#include "../graphics/graphics_point_cloud_object_driver.h"
#include "../graphics/graphics_shape_object_driver.h"
#include "../gui/gui_application.h"
-#include "../gui/qtgui_utils.h"
#include "app_module.h"
#include "cli_export.h"
#include "console.h"
@@ -26,6 +28,7 @@
#include "mainwindow.h"
#include "qsettings_storage.h"
#include "qstring_conv.h"
+#include "qtgui_utils.h"
#include "theme.h"
#include "version.h"
#include "widget_model_tree.h"
@@ -62,6 +65,7 @@ namespace Mayo {
// Declared in graphics/graphics_create_driver.cpp
void setFunctionCreateGraphicsDriver(std::function fn);
+// Provides an i18n context for the current file(main.cpp)
class Main {
MAYO_DECLARE_TEXT_ID_FUNCTIONS(Mayo::Main)
Q_DECLARE_TR_FUNCTIONS(Mayo::Main)
@@ -69,6 +73,7 @@ class Main {
namespace {
+// Stores arguments(options) passed at command line
struct CommandLineArguments {
QString themeName;
FilePath filepathSettings;
@@ -79,6 +84,7 @@ struct CommandLineArguments {
bool cliProgressReport = true;
};
+// Provides customization of Qt message handler
class LogMessageHandler {
public:
static LogMessageHandler& instance()
@@ -87,15 +93,20 @@ class LogMessageHandler {
return object;
}
+ // Corresponds to CommandLineArguments::includeDebugLogs
void enableDebugLogs(bool on)
{
m_enableDebugLogs = on;
}
+ // Corresponds to CommandLineArguments::filepathLog
void setOutputFilePath(const FilePath& fp)
{
m_outputFilePath = fp;
- m_outputFile.open(fp, std::ios::out | std::ios::app);
+ if (!fp.empty())
+ m_outputFile.open(fp, std::ios::out | std::ios::app);
+ else
+ m_outputFile.close();
}
std::ostream& outputStream(QtMsgType type)
@@ -109,6 +120,7 @@ class LogMessageHandler {
return std::cerr;
}
+ // Function called for Qt message handling
static void qtHandler(QtMsgType type, const QMessageLogContext& /*context*/, const QString& msg)
{
const std::string localMsg = consoleToPrintable(msg);
@@ -142,8 +154,27 @@ class LogMessageHandler {
bool m_enableDebugLogs = true;
};
+// Provides handling of signal/slot thread mismatch with the help of Qt
+// There will be a single QObject created per thread, so it can be used to enqueue slot functions
+class QtSignalThreadHelper : public ISignalThreadHelper {
+public:
+ std::any getCurrentThreadContext() override
+ {
+ // Note: thread_local implies "static"
+ // See https://en.cppreference.com/w/cpp/language/storage_duration
+ thread_local QObject obj;
+ return &obj;
+ }
+
+ void execInThread(const std::any& context, const std::function