mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-21 07:54:13 +02:00
Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8edda68e96 | |||
| ad9a47602d | |||
| dd08b3daa5 | |||
| 2bdad43371 | |||
| c05033d44c | |||
| 15c893de89 | |||
| 3e7dd15392 | |||
| 061f07ef9e | |||
| cb0d8c8f63 | |||
| f9d063bd38 | |||
| fab5571ea2 | |||
| a0acf878a2 | |||
| 21588a9c10 | |||
| fcbea2dafd | |||
| 1272b9db06 | |||
| 6e8d821853 | |||
| e64c0618c8 | |||
| 3af551583c | |||
| b274170cff | |||
| bd7d60021c | |||
| 12f443dd89 | |||
| 9afba7091c | |||
| 3d5799773e | |||
| 9c3ab588a4 | |||
| 3a26cc4b86 | |||
| 88f5af08d7 | |||
| 29e4817254 | |||
| c8b5901296 | |||
| f8cb997f3b | |||
| 1e8f5d8a72 | |||
| ab8b85127f | |||
| 52d5c7572c | |||
| 30661c2e2d | |||
| d74317c87a | |||
| 256993e25b | |||
| b18904415e | |||
| 18477c22b5 | |||
| 128d9e8dff | |||
| 6eb5384048 | |||
| 4e9463ee8a | |||
| baa3a16614 | |||
| 32502bef2c | |||
| c13971c0fe | |||
| 4e8893929f | |||
| 59c51869b3 | |||
| f6f68871d1 | |||
| 20a0a888c7 | |||
| d2e0532b12 | |||
| a8df5e3df9 | |||
| 73250bd746 | |||
| 956836458a | |||
| 2c88f78f69 | |||
| f85ee19fc0 | |||
| 2f415117f7 | |||
| 7c0e867226 | |||
| 0b7197118a | |||
| 77f8e26ffc | |||
| 16dbc5c404 | |||
| 73f7ea8f50 | |||
| 99e9ce5aca | |||
| bc06f7d444 | |||
| 2620d34b59 | |||
| 4b675dda1b | |||
| e982778526 | |||
| 10e5cd00f4 | |||
| 6bc53277ec | |||
| 7d4cbc30f1 | |||
| 2fcf3540dd | |||
| a45a984b32 | |||
| 1f647fa229 | |||
| c4d2f0c4fb | |||
| ac47f52eb0 | |||
| d2e75b4195 | |||
| bbc995c91e | |||
| 2322e6fd12 | |||
| b5722c3f2a | |||
| 2926ca7306 | |||
| c7893c8229 | |||
| 412bc7f71f | |||
| c147e6562d | |||
| bee25a4ca9 | |||
| 6d09041dce | |||
| 1a12d440b2 | |||
| 85f46f2b48 | |||
| 9637420756 | |||
| 6c68c461b1 | |||
| c830101ba6 | |||
| a1c0907811 | |||
| ffd829bb69 | |||
| 957bbe5edb | |||
| ce0ba4a681 | |||
| 8295083f05 | |||
| a2441a6f81 | |||
| c1f9af8544 | |||
| 033c2f93a8 | |||
| 83623a0fa6 | |||
| c85f80bbcf | |||
| c6995a0e7e | |||
| f18eda845b | |||
| 73e9473db4 | |||
| 7307128fc3 | |||
| 799ff5573f | |||
| 6f2c66afef | |||
| c39c7414cf | |||
| c33f250910 | |||
| c265f0206c | |||
| cfa1e3ed27 | |||
| 36dd1624d2 | |||
| a583b3c43c | |||
| 1738c3ad6c | |||
| 82026d8f7c | |||
| a494a8fd6c | |||
| 95d7b74e9e | |||
| 68e507a522 | |||
| 6657b3de2b | |||
| 4c038a868b | |||
| dab4c6c424 | |||
| 6208c7e5df | |||
| 31feb7b7b0 | |||
| 622791f994 | |||
| a48124a27a | |||
| e19d60ae55 | |||
| 030e6ebf00 | |||
| b855d760a8 | |||
| 5b8d05fc1e | |||
| b034b1a634 | |||
| 44ed01ff5d | |||
| 6061c63809 | |||
| 884ec4bbb9 | |||
| 9285d12333 | |||
| fb0649ceee | |||
| 6f9f5f84c2 | |||
| c26cf34397 | |||
| 7afc1eb56b | |||
| 0c2027fd77 | |||
| f9cbf2365d | |||
| 52992b31eb | |||
| d7c75ea5a5 | |||
| b9153269a4 | |||
| eab9603d8a | |||
| e7cbb8f50e | |||
| e3d11a4992 |
@@ -217,10 +217,71 @@ jobs:
|
||||
cp -r "$GITHUB_WORKSPACE/examples" "$FILES/examples" || true
|
||||
cp -r "$GITHUB_WORKSPACE/fonts" "$FILES/fonts" || true
|
||||
|
||||
cp -r "$GITHUB_WORKSPACE/lang" "$FILES/lang" || true
|
||||
find "$GITHUB_WORKSPACE/build" -name "*.qm" -exec cp {} "$FILES/lang/" \; 2>/dev/null || true
|
||||
echo "=== .qm files in files/lang/ ==="
|
||||
ls "$FILES/lang/"*.qm 2>/dev/null | wc -l || echo "0 .qm files"
|
||||
# --- Translations ---
|
||||
# Since PR #751 the .qm files are no longer tracked in git: lrelease
|
||||
# generates them in build/lang/ (OUTPUT_LOCATION "lang", relative to
|
||||
# the build dir). Expected set = the .ts listed in TS_FILES
|
||||
# (cmake/qet_compilation_vars.cmake): a missing .qm fails the job
|
||||
# instead of silently shipping an untranslated build; a .ts present
|
||||
# in lang/ but not listed in TS_FILES only raises a warning.
|
||||
# find/grep only, no shell glob: $GITHUB_WORKSPACE is a Windows path
|
||||
# (D:\a\...) and its backslashes break glob patterns.
|
||||
QM_SRC="$GITHUB_WORKSPACE/build/lang"
|
||||
mkdir -p "$FILES/lang"
|
||||
TS_LISTED=$(grep -o 'lang/qet_[A-Za-z_]*\.ts' "$GITHUB_WORKSPACE/cmake/qet_compilation_vars.cmake" \
|
||||
| sed -e 's#^lang/##' -e 's#\.ts$##' | LC_ALL=C sort -u || true)
|
||||
[ -n "$TS_LISTED" ] || { echo "ERROR: cannot read TS_FILES from cmake/qet_compilation_vars.cmake"; exit 1; }
|
||||
TS_PRESENT=$(find "$GITHUB_WORKSPACE/lang" -maxdepth 1 -name 'qet_*.ts' -exec basename {} .ts \; | LC_ALL=C sort)
|
||||
UNLISTED=$(LC_ALL=C comm -13 <(echo "$TS_LISTED") <(echo "$TS_PRESENT"))
|
||||
if [ -n "$UNLISTED" ]; then
|
||||
echo "::warning::.ts files not in TS_FILES, no .qm built:" $UNLISTED
|
||||
fi
|
||||
find "$QM_SRC" -maxdepth 1 -name 'qet_*.qm' -exec cp {} "$FILES/lang/" \; 2>/dev/null || true
|
||||
QM_PRESENT=$(find "$FILES/lang" -maxdepth 1 -name 'qet_*.qm' -exec basename {} .qm \; | LC_ALL=C sort)
|
||||
MISSING=$(LC_ALL=C comm -23 <(echo "$TS_LISTED") <(echo "$QM_PRESENT"))
|
||||
QM_COUNT=$(printf '%s\n' "$QM_PRESENT" | grep -c . || true)
|
||||
TS_COUNT=$(printf '%s\n' "$TS_LISTED" | grep -c .)
|
||||
echo "=== $QM_COUNT .qm files copied to files/lang/ (expected: $TS_COUNT) ==="
|
||||
if [ -n "$MISSING" ]; then
|
||||
echo "ERROR: missing translations:" $MISSING
|
||||
find "$GITHUB_WORKSPACE/build" -name '*.qm' || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Qt's own translations (OK/Cancel buttons, standard dialogs...):
|
||||
# they come from qtbase_XX.qm, not from QET's .ts, and windeployqt
|
||||
# runs with --no-translations. QETApp::setLanguage() falls back to
|
||||
# lang/qt_XX.qm, so copy each qtbase_XX.qm there under that name
|
||||
# (qtbase_XX.qm is standalone, unlike Qt's qt_XX.qm meta catalogs).
|
||||
QT_TR_DIR=/ucrt64/share/qt6/translations
|
||||
if [ ! -d "$QT_TR_DIR" ]; then
|
||||
QT_TR_DIR=$(cygpath -u "$(/ucrt64/bin/qtpaths6 --query QT_INSTALL_TRANSLATIONS 2>/dev/null)" 2>/dev/null || true)
|
||||
fi
|
||||
find "$QT_TR_DIR" -maxdepth 1 -name 'qtbase_*.qm' 2>/dev/null | while read -r f; do
|
||||
l=$(basename "$f" .qm)
|
||||
cp "$f" "$FILES/lang/qt_${l#qtbase_}.qm"
|
||||
done
|
||||
# QET languages Qt only ships with a region (pt -> pt_PT, zh -> zh_CN):
|
||||
# QTranslator only shortens codes (fr_FR -> fr), it never extends them.
|
||||
for q in $TS_LISTED; do
|
||||
l=${q#qet_}
|
||||
if [ ! -e "$FILES/lang/qt_$l.qm" ]; then
|
||||
if [ -e "$QT_TR_DIR/qtbase_${l}_${l^^}.qm" ]; then
|
||||
alt="$QT_TR_DIR/qtbase_${l}_${l^^}.qm"
|
||||
else
|
||||
alt=$(find "$QT_TR_DIR" -maxdepth 1 -name "qtbase_${l}_*.qm" 2>/dev/null | LC_ALL=C sort | head -1 || true)
|
||||
fi
|
||||
if [ -n "$alt" ]; then
|
||||
cp "$alt" "$FILES/lang/qt_$l.qm"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
QT_QM_COUNT=$(find "$FILES/lang" -maxdepth 1 -name 'qt_*.qm' | wc -l)
|
||||
echo "=== $QT_QM_COUNT Qt translation files (qt_*.qm) copied from $QT_TR_DIR ==="
|
||||
if [ "$QT_QM_COUNT" -eq 0 ]; then
|
||||
echo "ERROR: no qtbase_*.qm found in '$QT_TR_DIR' (mingw-w64-ucrt-x86_64-qt6-translations installed?)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for f in LICENSE ChangeLog CREDIT README ELEMENTS.LICENSE; do
|
||||
cp "$GITHUB_WORKSPACE/$f" "$FILES/$f" 2>/dev/null || true
|
||||
|
||||
+2
-1
@@ -6,4 +6,5 @@
|
||||
doc/*
|
||||
!doc/QElectroTech.qch
|
||||
QElectroTech.tag
|
||||
!doc/doc-utils
|
||||
!doc/doc-utils
|
||||
lang/*.qm
|
||||
|
||||
+20
-4
@@ -19,7 +19,7 @@ include(cmake/hoto_update_cmake_message.cmake)
|
||||
cmake_minimum_required(VERSION 3.5...4.2)
|
||||
|
||||
project(qelectrotech
|
||||
VERSION 0.100.1
|
||||
VERSION 0.200.1
|
||||
DESCRIPTION "QET is a CAD/CAE editor focusing on schematics drawing features."
|
||||
HOMEPAGE_URL "https://qelectrotech.org/"
|
||||
LANGUAGES C CXX)
|
||||
@@ -36,7 +36,7 @@ set(QET_DIR ${PROJECT_SOURCE_DIR})
|
||||
# includes below, so every subdirectory and every FetchContent dependency
|
||||
# sees a consistent, already-defined value.
|
||||
if(NOT DEFINED QT_VERSION_MAJOR)
|
||||
set(QT_VERSION_MAJOR 5)
|
||||
set(QT_VERSION_MAJOR 6)
|
||||
endif()
|
||||
|
||||
# Some third-party CMake projects we pull in via FetchContent (e.g.
|
||||
@@ -134,6 +134,7 @@ include(cmake/fetch_kdeaddons.cmake)
|
||||
option(PACKAGE_TESTS "Build the tests" ON)
|
||||
if(PACKAGE_TESTS)
|
||||
message("Add sub directory tests")
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
@@ -163,7 +164,22 @@ else()
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE TRUE)
|
||||
# CFBundleIdentifier must not be empty. CMake's default Info.plist
|
||||
# template fills it from MACOSX_BUNDLE_GUI_IDENTIFIER; with that unset
|
||||
# the bundle ships an empty identifier, LaunchServices never registers
|
||||
# the .app, and AppKit's open/save panel service (which is keyed on the
|
||||
# client's bundle id) silently presents nothing -- every
|
||||
# QFileDialog::getOpenFileName()/getSaveFileName() call returns an empty
|
||||
# string without a panel ever appearing, so File > Open and File > Save
|
||||
# as do nothing at all.
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER "org.qelectrotech.QElectroTech"
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "QElectroTech"
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
|
||||
MACOSX_BUNDLE_COPYRIGHT "Copyright 2006-2026 The QElectroTech Team"
|
||||
)
|
||||
endif()
|
||||
|
||||
# The default build only compiles the tracked .ts files to .qm (lrelease).
|
||||
@@ -174,7 +190,7 @@ endif()
|
||||
# fail with "Premature end of document".
|
||||
set_source_files_properties(
|
||||
${TS_FILES}
|
||||
PROPERTIES OUTPUT_LOCATION "${QET_DIR}/lang"
|
||||
PROPERTIES OUTPUT_LOCATION "lang"
|
||||
)
|
||||
if(QT_VERSION_MAJOR EQUAL 6)
|
||||
if(Qt6_VERSION VERSION_LESS "6.2")
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = QElectroTech
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = " 0.100.0-dev"
|
||||
PROJECT_NUMBER = " 0.200.1"
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -48,8 +48,8 @@ Here are the technical choices made for the software development:
|
||||
* Coding language: [C++](https://en.wikipedia.org/wiki/C%2B%2B)
|
||||
* GUI translations: [Qt Linguist](http://doc.qt.io/qt-5/qtlinguist-index.html)
|
||||
* Version control: [GIT](https://github.com/qelectrotech/qelectrotech-source-mirror.git)
|
||||
* Doxygen documentation :[Doxygen](https://qelectrotech.github.io/qelectrotech-source-mirror/)
|
||||
* QtCreator qch doxygen :[QElectroTech.qch](https://github.com/qelectrotech/qelectrotech-source-mirror/blob/master/doc/QElectroTech.qch)
|
||||
* Doxygen documentation :[Doxygen](https://download.qelectrotech.org/qet/doxygen/html/)
|
||||
* QtCreator qch doxygen :[QElectroTech.qch](https://download.qelectrotech.org/qet/doxygen/QElectroTech.qch)
|
||||
* File format for projects, elements and titleblocks: [XML](http://www.w3schools.com/xml/xml_whatis.asp)
|
||||
* Main development platform: [GNU/Linux](http://getgnulinux.org/en/linux/)
|
||||
* Targeted platforms: Windows, GNU/Linux, Mac OS X, BSDs
|
||||
|
||||
@@ -85,6 +85,11 @@ parts:
|
||||
- qt6-tools-dev
|
||||
- qt6-base-private-dev
|
||||
- pkgconf
|
||||
# Qt6 PrintSupport records Cups::Cups as a third-party dependency
|
||||
# (qprint_p.h includes <cups/ppd.h>), so find_package(Qt6 PrintSupport)
|
||||
# runs FindCups at configure time and fails without the CUPS headers.
|
||||
# Build-time only: nothing from it is staged into the snap.
|
||||
- libcups2-dev
|
||||
override-build: |
|
||||
displayed_version=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g')
|
||||
snap_version="${displayed_version}-g$(git rev-parse --short=8 HEAD)"
|
||||
|
||||
@@ -26,7 +26,7 @@ if(BUILD_PUGIXML)
|
||||
pugixml
|
||||
GIT_REPOSITORY https://github.com/zeux/pugixml.git
|
||||
GIT_TAG v1.15)
|
||||
|
||||
set(PUGIXML_INSTALL OFF CACHE INTERNAL "")
|
||||
FetchContent_MakeAvailable(pugixml)
|
||||
else()
|
||||
find_package(pugixml REQUIRED)
|
||||
|
||||
@@ -174,7 +174,11 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/conductornumexport.cpp
|
||||
${QET_DIR}/sources/wiringlistexport.h
|
||||
${QET_DIR}/sources/wiringlistexport.cpp
|
||||
${QET_DIR}/sources/ui/wiringlistdialog.h
|
||||
${QET_DIR}/sources/ui/wiringlistdialog.cpp
|
||||
${QET_DIR}/sources/conductornumexport.h
|
||||
${QET_DIR}/sources/bomexport.cpp
|
||||
${QET_DIR}/sources/bomexport.h
|
||||
${QET_DIR}/sources/conductorprofile.cpp
|
||||
${QET_DIR}/sources/conductorprofile.h
|
||||
${QET_DIR}/sources/conductorproperties.cpp
|
||||
@@ -473,6 +477,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/project/projectusagetracker.cpp
|
||||
${QET_DIR}/sources/project/projectusagetracker.h
|
||||
|
||||
${QET_DIR}/sources/properties/deviceinformation.cpp
|
||||
${QET_DIR}/sources/properties/deviceinformation.h
|
||||
${QET_DIR}/sources/properties/elementdata.cpp
|
||||
${QET_DIR}/sources/properties/elementdata.h
|
||||
${QET_DIR}/sources/properties/propertiesinterface.cpp
|
||||
@@ -856,6 +862,7 @@ set(TS_FILES
|
||||
${QET_DIR}/lang/qet_hu.ts
|
||||
${QET_DIR}/lang/qet_it.ts
|
||||
${QET_DIR}/lang/qet_ja.ts
|
||||
${QET_DIR}/lang/qet_ko.ts
|
||||
${QET_DIR}/lang/qet_mn.ts
|
||||
${QET_DIR}/lang/qet_nb.ts
|
||||
${QET_DIR}/lang/qet_nl.ts
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Smart Device Information and BOM CSV
|
||||
|
||||
## Architecture
|
||||
|
||||
- A `.qet` project is XML. Placed component properties are held in
|
||||
`ElementData::m_informations`, a `DiagramContext`, and are serialized by the
|
||||
existing `Element::fromXml()` and `Element::toXml()` path.
|
||||
- `DeviceInformation` is a typed view over that existing context. It does not
|
||||
introduce a second data store. Updates made by the properties editor continue
|
||||
to use `ChangeElementInformationCommand`, including undo/redo and dirty state.
|
||||
- The project database copies element contexts into `element_info` and exposes
|
||||
reporting fields through `element_nomenclature_view`.
|
||||
- GUI and CLI BOM exports both use `BomExport::defaultQuery()` against that
|
||||
view. The existing `BOMExportDialog` remains the only GUI export entry point;
|
||||
users can still customize columns, filters, and grouped/ungrouped output.
|
||||
|
||||
## Device Properties
|
||||
|
||||
| Device field | XML/context key | Default BOM |
|
||||
| --- | --- | --- |
|
||||
| Device tag | `label` | Yes |
|
||||
| Manufacturer | `manufacturer` | Yes |
|
||||
| Part number | `manufacturer_reference` | Yes |
|
||||
| Model | `model` | Yes |
|
||||
| Description | `description` | Yes |
|
||||
| Category | `category` | Yes |
|
||||
| Quantity | `quantity` | Yes |
|
||||
| Voltage rating | `voltage_rating` | Yes |
|
||||
| Current rating | `current_rating` | Yes |
|
||||
| Page | `folio` from the diagram | Yes |
|
||||
| Notes | `notes` | Yes |
|
||||
|
||||
The five new context keys are `model`, `category`, `voltage_rating`,
|
||||
`current_rating`, and `notes`. They are available in the existing information
|
||||
editor for Simple and Master elements, in symbol definitions, in the SQL
|
||||
nomenclature view, and in the export column picker.
|
||||
|
||||
No XML version bump or migration is required. Missing keys read as empty
|
||||
strings, empty properties are omitted by the existing writer, and unknown
|
||||
context keys remain preserved. Quantity and ratings stay free text so existing
|
||||
unit conventions and decimal quantities are not changed implicitly.
|
||||
|
||||
## BOM Policy
|
||||
|
||||
The default BOM query exports one row per Simple or Master element. Slave
|
||||
contacts, terminal-strip elements, reports, thumbnails, and conductor
|
||||
definitions are excluded. A Master device is counted once and PLC I/O entries
|
||||
are not expanded. The existing `exclude_from_bom` property is honored by the
|
||||
SQL view, including common true representations (`true`, `1`, `yes`, `on`).
|
||||
|
||||
The default columns are:
|
||||
|
||||
`Device tag, Designation, Manufacturer, Part number, Model, Description,
|
||||
Category, Quantity, Voltage rating, Current rating, Page, Notes`.
|
||||
|
||||
The existing **Format as bill of materials** checkbox provides grouped output;
|
||||
it is off by default so the initial export remains ungrouped. Users may change
|
||||
the query and column order through the existing dialog. The command-line
|
||||
`--export-bom` operation uses the same default query and column order.
|
||||
|
||||
CSV output is UTF-8 with a BOM, semicolon-delimited, and quotes every field.
|
||||
Embedded quotes are doubled and embedded newlines are retained. `QSaveFile`
|
||||
provides atomic replacement; query, open, write, and commit failures are
|
||||
reported without deleting an existing destination first.
|
||||
|
||||
## Verification
|
||||
|
||||
The smart-device test is registered explicitly in
|
||||
`tests/qttest/CMakeLists.txt` and uses only Qt Test and Qt SQL. It is therefore
|
||||
part of the normal cross-platform CMake/CTest flow:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DPACKAGE_TESTS=ON
|
||||
cmake --build build
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
`tst_smart_device` checks the metadata column contract, Simple/Master filtering,
|
||||
stable page ordering, UTF-8 with Latin and non-Latin samples, CSV escaping, and
|
||||
atomic write success/failure. Existing project XML save/load tests cover the
|
||||
generic `DiagramContext` persistence path used by the new keys.
|
||||
|
||||
Manual checks:
|
||||
|
||||
1. Open an existing project and edit a Simple or Master element's information.
|
||||
2. Set all device fields, apply, undo, redo, save, close, and reopen.
|
||||
3. Open Project > Export as CSV and verify the device columns are preselected.
|
||||
4. Export with grouping both disabled and enabled.
|
||||
5. Verify leading-zero part numbers, non-Latin text, quotes, and newlines in a
|
||||
UTF-8 CSV reader.
|
||||
|
||||
Cable management, terminal-strip planning, automatic wire numbering, PLC I/O
|
||||
expansion, panel layout, and manufacturer catalog integration are out of scope.
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -42,7 +42,7 @@
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>qelectrotech</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.qelectrotech</string>
|
||||
<string>org.qelectrotech.QElectroTech</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Copyright 2019 The QElectroTech Team
|
||||
# This file is part of QElectroTech.
|
||||
|
||||
# QElectroTech is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# QElectroTech is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
# Need homebrew and coreutils installed see <http://brew.sh>.
|
||||
|
||||
|
||||
#Force MacOSX12.3.sdk
|
||||
#see: https://www.downtowndougbrown.com/2023/08/how-to-create-a-qt-5-arm-intel-universal-binary-for-mac/
|
||||
|
||||
export DEVELOPER_DIR=/Applications/Xcode_14.01.app/Contents/Developer
|
||||
|
||||
# configuration
|
||||
APPNAME='qelectrotech'
|
||||
BUNDLE=$APPNAME.app
|
||||
APPBIN="$BUNDLE/Contents/MacOS/$APPNAME"
|
||||
|
||||
# Emplacement du script
|
||||
current_dir=$(dirname "$0")
|
||||
|
||||
# On se remet au depart
|
||||
cd "${current_dir}/../"
|
||||
|
||||
# Emplacement courant
|
||||
current_dir=$(PWD)
|
||||
|
||||
|
||||
### get system configuration ########################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "This script prepares a Qt application bundle for deployment."
|
||||
echo "Please see the \"Deploying an Application on Qt/Mac\""
|
||||
echo "page in the Qt documentation for more information."
|
||||
echo
|
||||
echo "This script :"
|
||||
echo "\t - up date the svn depot"
|
||||
echo "\t - built the application bundle,"
|
||||
echo "\t - copy over required Qt frameworks,"
|
||||
echo "\t - copy additional files: translations, titleblocks and elements,"
|
||||
echo "\t - create image disk."
|
||||
echo
|
||||
echo "Enjoy ;-)"
|
||||
echo
|
||||
|
||||
# as long as we can find qmake, we don't need QTDIR
|
||||
FWPATH=`qmake -query QT_INSTALL_LIBS`
|
||||
if [ ! -d $FWPATH/QtGui.framework ] ; then
|
||||
echo "ERROR: cannot find the Qt frameworks. Make sure Qt is installed"
|
||||
echo "and qmake is in your environment path."
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
### GIT ####################################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Run GIT:"
|
||||
|
||||
# recupere le numero de la dernière revision
|
||||
# Si il y a ':' garde la 2e partie
|
||||
# Remplace les lettres par 'rien'
|
||||
#revAv=$(svnversion | cut -d : -f 2 | tr -d '[:alpha:]')
|
||||
|
||||
# Fait une mise à jour
|
||||
git submodule init
|
||||
git submodule update
|
||||
git pull --recurse-submodules
|
||||
git pull
|
||||
#git checkout foliolist_position
|
||||
|
||||
# recupere le numero de la nouvelle revision
|
||||
|
||||
GITCOMMIT=$(git rev-parse --short HEAD)
|
||||
A=$(git rev-list HEAD --count)
|
||||
HEAD=$(($A+473))
|
||||
|
||||
|
||||
VERSION=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g')
|
||||
#VERSION=$(cat sources/qetversion.cpp | grep "return QVersionNumber{ 0, "| head -n 1| cut -c25-35| sed -e 's/,/./g' -e 's/ //g') #Find major, minor, and micro version numbers in sources/qetversion.cp
|
||||
|
||||
# Dmg de la dernière revision déjà créé
|
||||
if [ -e "build-aux/mac-osx/${APPNAME} $VERSION r$HEAD.dmg" ] ; then
|
||||
echo "There are not new updates, make disk image can"
|
||||
echo "take a lot of time (5 min). Can you continu?"
|
||||
echo "[y/n]"
|
||||
read userinput
|
||||
if [ "$userinput" == "n" ] ; then
|
||||
echo
|
||||
echo "Process is stopped."
|
||||
echo
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
### make install ####################################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Run make install:"
|
||||
|
||||
# pour effacer l’ancienne compilation
|
||||
if [ -d $BUNDLE ] ; then
|
||||
echo "Removing hold bundle..."
|
||||
rm -rf $BUNDLE
|
||||
fi
|
||||
if [ -e Makefile ] ; then
|
||||
echo "Removing hold Makefile..."
|
||||
make clean
|
||||
fi
|
||||
|
||||
# genere le Makefile
|
||||
echo "Generating new makefile..."
|
||||
qmake -spec macx-clang
|
||||
|
||||
# compilation
|
||||
if [ -e Makefile.Release ] ; then
|
||||
START_TIME=$SECONDS
|
||||
|
||||
# arret du script si erreur de compilation
|
||||
testSuccessBuild () {
|
||||
if [ $? -ne 0 ]; then
|
||||
cleanVerionTag
|
||||
ELAPSED_TIME=$(($SECONDS - $START_TIME))
|
||||
echo
|
||||
echo "make failed - $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# utilise tout les coeurs pour une compilation plus rapide
|
||||
coeur=$(sysctl hw.ncpu | awk '{print $2}')
|
||||
if [ $? -ne 0 ]; then
|
||||
make -f Makefile.Release
|
||||
testSuccessBuild
|
||||
else
|
||||
make -j$(($coeur + 1)) -f Makefile.Release
|
||||
testSuccessBuild
|
||||
fi
|
||||
|
||||
cleanVerionTag
|
||||
ELAPSED_TIME=$(($SECONDS - $START_TIME))
|
||||
echo
|
||||
echo "The time of compilation is $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
|
||||
else
|
||||
echo "ERROR: Makefile not found. This script requires the macx-clang makespec"
|
||||
exit
|
||||
fi
|
||||
|
||||
cp -R ${current_dir}/misc/Info.plist qelectrotech.app/Contents/
|
||||
cp -R ${current_dir}/ico/mac_icon/*.icns qelectrotech.app/Contents/Resources/
|
||||
# On rajoute le numero de version pour "cmd + i"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION r$HEAD" "qelectrotech.app/Contents/Info.plist" # Version number
|
||||
|
||||
### copy over frameworks ############################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Copy Qt libraries and private frameworks:"
|
||||
|
||||
echo "Processing Mac deployment tool..."
|
||||
if [ ! -d $BUNDLE ] ; then
|
||||
echo "ERROR: cannot find application bundle \"$BUNDLE\" in current directory"
|
||||
exit
|
||||
fi
|
||||
#~/Qt/5.5/clang_64/bin/macdeployqt $BUNDLE
|
||||
macdeployqt $BUNDLE
|
||||
|
||||
### add file missing #######################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Copy file missing:"
|
||||
|
||||
# Dossier à ajouter
|
||||
QET_ELMT_DIR="${current_dir}/elements/"
|
||||
QET_TBT_DIR="${current_dir}/titleblocks/"
|
||||
QET_LANG_DIR="${current_dir}/lang/"
|
||||
QET_FONTS_DIR="${current_dir}/fonts/"
|
||||
QET_LICENSES_DIR="${current_dir}/licenses/"
|
||||
|
||||
# Add new folder for Qt dialog translation see
|
||||
## see <https://download.tuxfamily.org/qet/Qt_lang/>.
|
||||
|
||||
LANG_DIR="${current_dir}/lang1/"
|
||||
|
||||
if [ -d "${QET_ELMT_DIR}" ]; then
|
||||
echo "Copying add elements in the bundle..."
|
||||
#mkdir $BUNDLE/Contents/Resources/elements
|
||||
cp -R ${QET_ELMT_DIR} $BUNDLE/Contents/Resources/elements
|
||||
fi
|
||||
|
||||
if [ -d "${QET_TBT_DIR}" ]; then
|
||||
echo "Copying titleblocks in the bundle..."
|
||||
#mkdir $BUNDLE/Contents/Resources/titleblocks
|
||||
cp -R ${QET_TBT_DIR} $BUNDLE/Contents/Resources/titleblocks
|
||||
fi
|
||||
|
||||
if [ -d "${QET_LANG_DIR}" ]; then
|
||||
echo "Copying translations in the bundle... "
|
||||
mkdir $BUNDLE/Contents/Resources/lang
|
||||
cp ${current_dir}/lang/*.qm $BUNDLE/Contents/Resources/lang
|
||||
fi
|
||||
|
||||
if [ -d "${LANG_DIR}" ]; then
|
||||
echo "Copying translations in the bundle... "
|
||||
cp ${current_dir}/lang1/*.qm $BUNDLE/Contents/Resources/lang
|
||||
|
||||
fi
|
||||
|
||||
if [ -d "${QET_FONTS_DIR}" ]; then
|
||||
echo "Copying fonts in the bundle... "
|
||||
mkdir $BUNDLE/Contents/Resources/fonts
|
||||
cp ${current_dir}/fonts/*.ttf $BUNDLE/Contents/Resources/fonts
|
||||
|
||||
fi
|
||||
|
||||
if [ -d "${QET_LICENSES_DIR}" ]; then
|
||||
echo "Copying licenses in the bundle..."
|
||||
mkdir $BUNDLE/Contents/Resources/licenses
|
||||
cp -R -L ${QET_LICENSES_DIR} $BUNDLE/Contents/Resources/licenses
|
||||
fi
|
||||
### create disk image ###############################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Create disk image:"
|
||||
|
||||
# Direction
|
||||
EX_Dir="examples"
|
||||
DOC_Dir="docs/Documentation.webloc"
|
||||
imagedir="/tmp/$APPNAME.$$"
|
||||
|
||||
# Remove any previous disk folder or DMG
|
||||
echo 'Preparing (removing hold files)... '
|
||||
if [ -e "/Volumes/${APPNAME}" ]; then
|
||||
hdiutil detach -quiet "/Volumes/${APPNAME}"
|
||||
fi
|
||||
if [ -e "${APPNAME} $VERSION r$HEAD.dmg" ] ; then
|
||||
rm -f "${APPNAME} $VERSION r$HEAD.dmg"
|
||||
fi
|
||||
if [ -e "build-aux/mac-osx/${APPNAME} $VERSION r$HEAD.dmg" ] ; then
|
||||
rm -f "build-aux/mac-osx/${APPNAME} $VERSION r$HEAD.dmg"
|
||||
fi
|
||||
if [ -e $imagedir ] ; then
|
||||
rm -rf $imagedir
|
||||
fi
|
||||
|
||||
# Create disk folder
|
||||
echo 'Copying application bundle and examples... '
|
||||
mkdir $imagedir
|
||||
cp -Rf $BUNDLE $imagedir
|
||||
cp -Rf $EX_Dir $imagedir
|
||||
if [ -e $DOC_Dir ] ; then
|
||||
cp $DOC_Dir $imagedir
|
||||
else
|
||||
echo "Documentation in ${DOC_Dir} not find."
|
||||
fi
|
||||
ln -s /Applications $imagedir/Applications
|
||||
|
||||
# Elimine les accents, symbole ...
|
||||
strip "$imagedir/$APPBIN"
|
||||
|
||||
# Creating a disk image from a folder
|
||||
echo 'Creating disk image... '
|
||||
hdiutil create -quiet -ov -srcfolder $imagedir -format UDBZ -volname "${APPNAME}" "${APPNAME} $VERSION r$HEAD.dmg"
|
||||
hdiutil internet-enable -yes -quiet "${APPNAME} $VERSION r$HEAD.dmg"
|
||||
|
||||
# Clean up disk folder
|
||||
echo 'Cleaning up... '
|
||||
cp -Rf "${APPNAME} $VERSION r$HEAD.dmg" "build-aux/mac-osx/${APPNAME}-$VERSION-r$HEAD-intel_X86_64.dmg"
|
||||
rm -f "${APPNAME} $VERSION r$HEAD.dmg"
|
||||
rm -rf $imagedir
|
||||
rm -rf $BUNDLE
|
||||
|
||||
|
||||
### The end, process is done ##########################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "The process of creating deployable application bundle is done."
|
||||
echo The disque image is in the folder \'build-aux/mac-osx\'.
|
||||
# Affiche les mise à jour depuis l'ancienne revision
|
||||
#if [ ! $(($HEAD - $revAv)) -eq 0 ] ; then
|
||||
# echo
|
||||
# echo "There are new updates. This numero of revision is $HEAD."
|
||||
# svn log -l $(($HEAD - $revAv))
|
||||
#else
|
||||
# echo
|
||||
# echo "There are not new updates. This numero of revision is $HEAD."
|
||||
# fi
|
||||
# echo
|
||||
|
||||
# La version en local n'est pas conforme à la dernière version svn
|
||||
# svnversion | grep -q '[MS:]' ; if [ $? -eq 0 ] ; then
|
||||
# echo Please note that the latest \local version is $(svnversion).
|
||||
# echo This is not the same version as the deposit.
|
||||
# echo You can use \'svn diff\' to see the differences.
|
||||
# echo And use \'svn revert \<fichier\>\' to delete the difference.
|
||||
# echo To go back, you can use svn update -r 360
|
||||
# echo to go to revision number 360.
|
||||
# echo
|
||||
#fi
|
||||
|
||||
|
||||
|
||||
#rsync to TF DMG builds
|
||||
echo -e "\033[1;31mWould you like to upload MacOS packages "${APPNAME}"-"$VERSION"_"r$HEAD-intel.dmg", n/Y?.\033[m"
|
||||
read a
|
||||
if [[ $a == "Y" || $a == "y" ]]; then
|
||||
cp -Rf "build-aux/mac-osx/${APPNAME}-$VERSION-r$HEAD-intel_X86_64.dmg" /Users/laurent/MAC_OS_X/
|
||||
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w --progress --exclude='.DS_Store' /Users/laurent/MAC_OS_X/ server:download.qelectrotech.org/qet/builds/MAC_OS_X/intel_X86_64/
|
||||
if [ $? != 0 ]; then
|
||||
{
|
||||
echo "RSYNC ERROR: problem syncing ${APPNAME}-$VERSION-r$HEAD-intel_X86_64.dmg"
|
||||
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w --progress --exclude='.DS_Store' /Users/laurent/MAC_OS_X/ server:download.qelectrotech.org/qet/builds/MAC_OS_X/intel_X86_64/
|
||||
|
||||
} fi
|
||||
|
||||
else
|
||||
echo -e "\033[1;33mExit.\033[m"
|
||||
|
||||
fi
|
||||
@@ -1,509 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Copyright 2023 The QElectroTech Team
|
||||
# This file is part of QElectroTech.
|
||||
|
||||
# QElectroTech is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# QElectroTech is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
# Need homebrew and coreutils installed see <http://brew.sh>.
|
||||
|
||||
#Force MacOSX12.3.sdk
|
||||
#see: https://www.downtowndougbrown.com/2023/08/how-to-create-a-qt-5-arm-intel-universal-binary-for-mac/
|
||||
|
||||
export DEVELOPER_DIR=/Applications/Xcode_14.01.app/Contents/Developer
|
||||
|
||||
# configuration
|
||||
APPNAME='qelectrotech'
|
||||
BUNDLE=$APPNAME.app
|
||||
APPBIN="$BUNDLE/Contents/MacOS/$APPNAME"
|
||||
IDENTITY="Developer ID Application: Laurent TRINQUES (Y73WZ6WZ5X)"
|
||||
|
||||
# Temp paths
|
||||
RW_DMG="/tmp/qet_rw.dmg"
|
||||
MOUNT_POINT="/tmp/qet_dmg_mount"
|
||||
STAGING="/tmp/qet_dmg_staging"
|
||||
|
||||
# Script location
|
||||
current_dir=$(dirname "$0")
|
||||
|
||||
# Go back to repo root
|
||||
cd "${current_dir}/../"
|
||||
|
||||
# Current directory
|
||||
current_dir=$(PWD)
|
||||
|
||||
|
||||
### get system configuration ########################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "This script prepares a Qt application bundle for deployment."
|
||||
echo "Please see the \"Deploying an Application on Qt/Mac\""
|
||||
echo "page in the Qt documentation for more information."
|
||||
echo
|
||||
echo "This script :"
|
||||
echo "\t - update the git depot"
|
||||
echo "\t - build the application bundle,"
|
||||
echo "\t - copy over required Qt frameworks,"
|
||||
echo "\t - copy additional files: translations, titleblocks and elements,"
|
||||
echo "\t - notarize the .app, then create a signed DMG."
|
||||
echo
|
||||
echo "Enjoy ;-)"
|
||||
echo
|
||||
|
||||
# as long as we can find qmake, we don't need QTDIR
|
||||
FWPATH=`qmake -query QT_INSTALL_LIBS`
|
||||
if [ ! -d $FWPATH/QtGui.framework ] ; then
|
||||
echo "ERROR: cannot find the Qt frameworks. Make sure Qt is installed"
|
||||
echo "and qmake is in your environment path."
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
### GIT ####################################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Run GIT:"
|
||||
|
||||
git submodule init
|
||||
git submodule update
|
||||
git pull --recurse-submodules
|
||||
git pull
|
||||
|
||||
# Get revision number and version
|
||||
GITCOMMIT=$(git rev-parse --short HEAD)
|
||||
A=$(git rev-list HEAD --count)
|
||||
HEAD=$(($A+473))
|
||||
|
||||
VERSION=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g')
|
||||
|
||||
DMG_NAME="${APPNAME}-$VERSION-r$HEAD-arm64.dmg"
|
||||
DMG_PATH="build-aux/mac-osx/$DMG_NAME"
|
||||
|
||||
# Check if already built
|
||||
if [ -e "$DMG_PATH" ] ; then
|
||||
echo "There are not new updates, make disk image can"
|
||||
echo "take a lot of time (5 min). Can you continu?"
|
||||
echo "[y/n]"
|
||||
read userinput
|
||||
if [ "$userinput" == "n" ] ; then
|
||||
echo
|
||||
echo "Process is stopped."
|
||||
echo
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
### make install ####################################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Run make install:"
|
||||
|
||||
# Remove old bundle
|
||||
if [ -d $BUNDLE ] ; then
|
||||
echo "Removing old bundle..."
|
||||
rm -rf $BUNDLE
|
||||
fi
|
||||
if [ -e Makefile ] ; then
|
||||
echo "Removing old Makefile..."
|
||||
rm .qmake.stash
|
||||
make clean
|
||||
fi
|
||||
|
||||
# Generate Makefile
|
||||
echo "Generating new makefile..."
|
||||
qmake -spec macx-clang
|
||||
|
||||
# Compile
|
||||
if [ -e Makefile.Release ] ; then
|
||||
START_TIME=$SECONDS
|
||||
|
||||
testSuccessBuild () {
|
||||
if [ $? -ne 0 ]; then
|
||||
cleanVerionTag
|
||||
ELAPSED_TIME=$(($SECONDS - $START_TIME))
|
||||
echo
|
||||
echo "make failed - $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
coeur=$(sysctl hw.ncpu | awk '{print $2}')
|
||||
if [ $? -ne 0 ]; then
|
||||
make -f Makefile.Release
|
||||
testSuccessBuild
|
||||
else
|
||||
make -j$(($coeur + 1)) -f Makefile.Release
|
||||
testSuccessBuild
|
||||
fi
|
||||
|
||||
ELAPSED_TIME=$(($SECONDS - $START_TIME))
|
||||
echo
|
||||
echo "The time of compilation is $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
|
||||
else
|
||||
echo "ERROR: Makefile not found. This script requires the macx-clang makespec"
|
||||
exit
|
||||
fi
|
||||
|
||||
cp -R ${current_dir}/misc/Info.plist qelectrotech.app/Contents/
|
||||
cp -R ${current_dir}/ico/mac_icon/*.icns qelectrotech.app/Contents/Resources/
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION r$HEAD" "qelectrotech.app/Contents/Info.plist"
|
||||
|
||||
|
||||
### copy over frameworks ############################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Copy Qt libraries and private frameworks:"
|
||||
|
||||
echo "Processing Mac deployment tool..."
|
||||
if [ ! -d $BUNDLE ] ; then
|
||||
echo "ERROR: cannot find application bundle \"$BUNDLE\" in current directory"
|
||||
exit
|
||||
fi
|
||||
macdeployqt $BUNDLE
|
||||
|
||||
### add missing files ###############################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Copy missing files:"
|
||||
|
||||
QET_ELMT_DIR="${current_dir}/elements/"
|
||||
QET_TBT_DIR="${current_dir}/titleblocks/"
|
||||
QET_LANG_DIR="${current_dir}/lang/"
|
||||
QET_EXAMPLES_DIR="${current_dir}/examples/"
|
||||
QET_FONTS_DIR="${current_dir}/fonts/"
|
||||
QET_LICENSES_DIR="${current_dir}/licenses/"
|
||||
LANG_DIR="${current_dir}/lang1/"
|
||||
|
||||
if [ -d "${QET_ELMT_DIR}" ]; then
|
||||
echo "Copying elements in the bundle..."
|
||||
cp -R ${QET_ELMT_DIR} $BUNDLE/Contents/Resources/elements
|
||||
fi
|
||||
|
||||
if [ -d "${QET_TBT_DIR}" ]; then
|
||||
echo "Copying titleblocks in the bundle..."
|
||||
cp -R ${QET_TBT_DIR} $BUNDLE/Contents/Resources/titleblocks
|
||||
fi
|
||||
|
||||
if [ -d "${QET_LANG_DIR}" ]; then
|
||||
echo "Copying translations in the bundle..."
|
||||
mkdir $BUNDLE/Contents/Resources/lang
|
||||
cp ${current_dir}/lang/*.qm $BUNDLE/Contents/Resources/lang
|
||||
fi
|
||||
|
||||
if [ -d "${LANG_DIR}" ]; then
|
||||
echo "Copying extra translations in the bundle..."
|
||||
cp ${current_dir}/lang1/*.qm $BUNDLE/Contents/Resources/lang
|
||||
fi
|
||||
|
||||
if [ -d "${QET_EXAMPLES_DIR}" ]; then
|
||||
echo "Copying examples in the bundle..."
|
||||
mkdir $BUNDLE/Contents/Resources/examples
|
||||
cp ${current_dir}/examples/*.qet $BUNDLE/Contents/Resources/examples
|
||||
fi
|
||||
|
||||
if [ -d "${QET_FONTS_DIR}" ]; then
|
||||
echo "Copying fonts in the bundle..."
|
||||
mkdir $BUNDLE/Contents/Resources/fonts
|
||||
cp ${current_dir}/fonts/*.ttf $BUNDLE/Contents/Resources/fonts
|
||||
fi
|
||||
|
||||
if [ -d "${QET_LICENSES_DIR}" ]; then
|
||||
echo "Copying licenses in the bundle..."
|
||||
mkdir $BUNDLE/Contents/Resources/licenses
|
||||
cp -R -L ${QET_LICENSES_DIR} $BUNDLE/Contents/Resources/licenses
|
||||
fi
|
||||
|
||||
### Sign the bundle #################################################
|
||||
# Sign in correct order: all dylibs first (including flat libs copied
|
||||
# by macdeployqt from Homebrew), then frameworks, plugins, bundle last.
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Code signing (dylibs -> frameworks -> plugins -> bundle):"
|
||||
|
||||
# 1. Sign all flat .dylib files in Frameworks/
|
||||
echo "-- Signing dylibs in Frameworks/..."
|
||||
find "$BUNDLE/Contents/Frameworks" -name "*.dylib" | while read lib; do
|
||||
echo " $(basename $lib)"
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
|
||||
done
|
||||
|
||||
# 2. Sign .framework bundles
|
||||
echo "-- Signing .framework bundles..."
|
||||
find "$BUNDLE/Contents/Frameworks" -maxdepth 1 -name "*.framework" | while read fw; do
|
||||
echo " $(basename $fw)"
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$fw"
|
||||
done
|
||||
|
||||
# 3. Sign plugins
|
||||
echo "-- Signing plugins..."
|
||||
find "$BUNDLE/Contents/PlugIns" \( -name "*.dylib" -o -name "*.so" \) | while read lib; do
|
||||
echo " $(basename $lib)"
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
|
||||
done
|
||||
|
||||
# 4. Sign any dylibs in MacOS/
|
||||
echo "-- Signing dylibs in MacOS/..."
|
||||
find "$BUNDLE/Contents/MacOS" -name "*.dylib" | while read lib; do
|
||||
echo " $(basename $lib)"
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
|
||||
done
|
||||
|
||||
# 5. Sign the main executable explicitly
|
||||
echo "-- Signing main executable..."
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
|
||||
"$BUNDLE/Contents/MacOS/$APPNAME"
|
||||
|
||||
# 6. Sign the bundle itself last
|
||||
echo "-- Signing bundle..."
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$BUNDLE"
|
||||
|
||||
# 7. Verify
|
||||
echo
|
||||
echo "Verifying bundle signature..."
|
||||
codesign --verify --deep --strict --verbose=2 "$BUNDLE"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: bundle signature verification failed, aborting."
|
||||
exit 1
|
||||
fi
|
||||
spctl -a -vv "$BUNDLE"
|
||||
echo "Bundle signature OK."
|
||||
|
||||
### Notarize the .app (via temporary ZIP) ###########################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Create temporary ZIP for notarization:"
|
||||
|
||||
NOTARIZE_ZIP="/tmp/${APPNAME}-$VERSION-r$HEAD-arm64-notarize.zip"
|
||||
/usr/bin/ditto -c -k --keepParent "$BUNDLE" "$NOTARIZE_ZIP"
|
||||
|
||||
echo -e "\033[1;31mWould you like to notarize the .app \"${APPNAME}-${VERSION}-r${HEAD}\", n/Y?\033[m"
|
||||
read a
|
||||
if [[ $a == "Y" || $a == "y" ]]; then
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Notarizing .app:"
|
||||
xcrun notarytool submit "$NOTARIZE_ZIP" --keychain-profile "org.qelectrotech" --wait
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: notarization failed. Check the log with:"
|
||||
echo " xcrun notarytool log <submission-id> --keychain-profile org.qelectrotech"
|
||||
rm -f "$NOTARIZE_ZIP"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "\033[1;33mExit.\033[m"
|
||||
fi
|
||||
|
||||
echo "Cleaning up temporary notarization ZIP..."
|
||||
rm -f "$NOTARIZE_ZIP"
|
||||
|
||||
### Staple the .app #################################################
|
||||
|
||||
echo -e "\033[1;31mWould you like to staple the .app \"${APPNAME}-${VERSION}-r${HEAD}\", n/Y?\033[m"
|
||||
read a
|
||||
if [[ $a == "Y" || $a == "y" ]]; then
|
||||
xcrun stapler staple -v "$BUNDLE"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: stapling .app failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "Verifying staple on .app..."
|
||||
xcrun stapler validate -v "$BUNDLE"
|
||||
spctl -a -vv "$BUNDLE"
|
||||
echo ".app stapled OK."
|
||||
else
|
||||
echo -e "\033[1;33mExit.\033[m"
|
||||
fi
|
||||
|
||||
### Create staging folder with Applications symlink #################
|
||||
# The staging folder contains the .app and a symlink to /Applications
|
||||
# so the user can drag-and-drop to install directly from the DMG.
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Preparing DMG staging folder:"
|
||||
|
||||
rm -rf "$STAGING"
|
||||
mkdir -p "$STAGING"
|
||||
cp -R "$BUNDLE" "$STAGING/"
|
||||
ln -s /Applications "$STAGING/Applications"
|
||||
echo "Staging folder ready: $STAGING"
|
||||
|
||||
### Create writable DMG (UDRW) ######################################
|
||||
# We use a writable DMG first so we can re-sign the .app inside
|
||||
# after hdiutil copies it (hdiutil can invalidate Sealed Resources
|
||||
# during the copy, so we must re-sign inside the mounted volume).
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Create writable DMG (UDRW) and re-sign .app inside:"
|
||||
|
||||
rm -f "$RW_DMG"
|
||||
hdiutil create \
|
||||
-volname "QElectroTech $VERSION" \
|
||||
-srcfolder "$STAGING" \
|
||||
-ov \
|
||||
-format UDRW \
|
||||
-fs HFS+ \
|
||||
"$RW_DMG"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: hdiutil failed to create writable DMG."
|
||||
rm -rf "$STAGING"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Mount the writable DMG
|
||||
rm -rf "$MOUNT_POINT"
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
hdiutil attach "$RW_DMG" -mountpoint "$MOUNT_POINT" -nobrowse -noverify
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: failed to mount writable DMG."
|
||||
rm -f "$RW_DMG"
|
||||
rm -rf "$STAGING"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Re-sign all binaries inside the mounted DMG
|
||||
echo "-- Re-signing dylibs inside DMG..."
|
||||
find "$MOUNT_POINT/$BUNDLE/Contents/Frameworks" -name "*.dylib" | while read lib; do
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
|
||||
done
|
||||
|
||||
find "$MOUNT_POINT/$BUNDLE/Contents/Frameworks" -maxdepth 1 -name "*.framework" | while read fw; do
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$fw"
|
||||
done
|
||||
|
||||
find "$MOUNT_POINT/$BUNDLE/Contents/PlugIns" \( -name "*.dylib" -o -name "*.so" \) | while read lib; do
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
|
||||
done
|
||||
|
||||
echo "-- Re-signing main executable inside DMG..."
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
|
||||
"$MOUNT_POINT/$BUNDLE/Contents/MacOS/$APPNAME"
|
||||
|
||||
echo "-- Re-signing bundle inside DMG..."
|
||||
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
|
||||
"$MOUNT_POINT/$BUNDLE"
|
||||
|
||||
# Verify signature inside the mounted DMG
|
||||
echo "Verifying bundle signature inside DMG..."
|
||||
codesign --verify --deep --strict --verbose=2 "$MOUNT_POINT/$BUNDLE"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: bundle signature invalid inside DMG, aborting."
|
||||
hdiutil detach "$MOUNT_POINT"
|
||||
rm -f "$RW_DMG"
|
||||
rm -rf "$STAGING" "$MOUNT_POINT"
|
||||
exit 1
|
||||
fi
|
||||
echo "Bundle signature inside DMG OK."
|
||||
|
||||
# Detach the writable DMG
|
||||
hdiutil detach "$MOUNT_POINT"
|
||||
|
||||
### Convert UDRW to final compressed UDZO ###########################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Convert to final compressed DMG (UDZO):"
|
||||
|
||||
mkdir -p "build-aux/mac-osx"
|
||||
rm -f "$DMG_PATH"
|
||||
|
||||
hdiutil convert "$RW_DMG" \
|
||||
-format UDZO \
|
||||
-o "$DMG_PATH"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: hdiutil convert failed."
|
||||
rm -f "$RW_DMG"
|
||||
rm -rf "$STAGING" "$MOUNT_POINT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$RW_DMG"
|
||||
rm -rf "$STAGING" "$MOUNT_POINT"
|
||||
|
||||
### Sign the final DMG ##############################################
|
||||
|
||||
echo "Signing final DMG..."
|
||||
codesign --sign "$IDENTITY" --timestamp "$DMG_PATH"
|
||||
|
||||
### Notarize and staple the final DMG ###############################
|
||||
|
||||
echo -e "\033[1;31mWould you like to notarize the DMG \"${DMG_NAME}\", n/Y?\033[m"
|
||||
read a
|
||||
if [[ $a == "Y" || $a == "y" ]]; then
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Notarizing DMG:"
|
||||
xcrun notarytool submit "$DMG_PATH" --keychain-profile "org.qelectrotech" --wait
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: DMG notarization failed. Check the log with:"
|
||||
echo " xcrun notarytool log <submission-id> --keychain-profile org.qelectrotech"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Stapling DMG..."
|
||||
xcrun stapler staple "$DMG_PATH"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: stapling DMG failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "DMG notarized and stapled OK."
|
||||
|
||||
echo "Verifying final DMG..."
|
||||
spctl -a -vv "$DMG_PATH"
|
||||
else
|
||||
echo -e "\033[1;33mExit.\033[m"
|
||||
fi
|
||||
|
||||
### Clean up bundle #################################################
|
||||
|
||||
echo "Cleaning up bundle..."
|
||||
rm -rf "$BUNDLE"
|
||||
|
||||
### The end #########################################################
|
||||
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "The process is done."
|
||||
echo "DMG is in the folder 'build-aux/mac-osx'."
|
||||
|
||||
### Upload via rsync ################################################
|
||||
|
||||
echo -e "\033[1;31mWould you like to upload MacOS package \"${DMG_NAME}\", n/Y?\033[m"
|
||||
read a
|
||||
if [[ $a == "Y" || $a == "y" ]]; then
|
||||
cp -Rf "$DMG_PATH" /Users/laurent/MAC_OS_X/
|
||||
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w \
|
||||
--progress --exclude='.DS_Store' \
|
||||
/Users/laurent/MAC_OS_X/ \
|
||||
server:download.qelectrotech.org/qet/builds/MAC_OS_X/arm64/
|
||||
if [ $? != 0 ]; then
|
||||
echo "RSYNC ERROR: problem syncing ${DMG_NAME}, retrying..."
|
||||
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w \
|
||||
--progress --exclude='.DS_Store' \
|
||||
/Users/laurent/MAC_OS_X/ \
|
||||
server:download.qelectrotech.org/qet/builds/MAC_OS_X/arm64/
|
||||
fi
|
||||
else
|
||||
echo -e "\033[1;33mExit.\033[m"
|
||||
fi
|
||||
@@ -126,7 +126,8 @@ ELAPSED_TIME=$(($SECONDS - $START_TIME))
|
||||
echo
|
||||
echo "The time of compilation is $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
|
||||
|
||||
# TODO: confirmer le chemin exact de sortie du .app selon CMakeLists.txt
|
||||
# Le .app sort a la racine de $BUILD_DIR : add_executable(... MACOSX_BUNDLE)
|
||||
# sans RUNTIME_OUTPUT_DIRECTORY dans CMakeLists.txt.
|
||||
echo "Copying built bundle into place..."
|
||||
cp -R "$BUILD_DIR/qelectrotech.app" "./$BUNDLE"
|
||||
|
||||
@@ -147,6 +148,98 @@ fi
|
||||
|
||||
macdeployqt $BUNDLE
|
||||
|
||||
### fix Homebrew dependencies macdeployqt could not handle ##########
|
||||
# Recent Homebrew bottles (brotli, webp, sharpyuv...) reference their own
|
||||
# dependencies as @rpath/libX.dylib. macdeployqt only resolves @rpath in
|
||||
# Contents/lib and in Qt's lib dir (-libpath does not help): it prints
|
||||
# "Cannot resolve rpath" and leaves some references untouched, either
|
||||
# absolute /opt/homebrew paths or @rpath libs missing from the bundle.
|
||||
# Fix both here: every /opt/homebrew reference is rewritten to
|
||||
# @rpath/libX.dylib (install ids too), and every @rpath/libX.dylib is
|
||||
# copied into Contents/Frameworks, where the executable's LC_RPATH
|
||||
# (@executable_path/../Frameworks) lets dyld find it. Everything is signed
|
||||
# below with the rest of Frameworks/.
|
||||
echo
|
||||
echo "______________________________________________________________"
|
||||
echo "Fix Homebrew dependencies left by macdeployqt:"
|
||||
|
||||
FW="$BUNDLE/Contents/Frameworks"
|
||||
chmod -R u+w "$BUNDLE/Contents/MacOS" "$FW" "$BUNDLE/Contents/PlugIns" 2>/dev/null
|
||||
|
||||
# Every Mach-O candidate of the bundle (otool silently ignores the others)
|
||||
list_macho() {
|
||||
find "$BUNDLE/Contents/MacOS" "$FW" "$BUNDLE/Contents/PlugIns" -type f \
|
||||
\( -name '*.dylib' -o -perm -u+x \) 2>/dev/null
|
||||
}
|
||||
|
||||
# Dependencies of one binary, without its own install id (dylibs only)
|
||||
list_deps() {
|
||||
_id=$(otool -D "$1" 2>/dev/null | sed -n 2p)
|
||||
otool -L "$1" 2>/dev/null | awk 'NR>1 { print $1 }' | while read _dep; do
|
||||
[ "$_dep" = "$_id" ] || echo "$_dep"
|
||||
done
|
||||
}
|
||||
|
||||
# The @rpath/libX.dylib names referenced anywhere in the bundle
|
||||
list_rpath_libs() {
|
||||
list_macho | while read bin; do
|
||||
list_deps "$bin" | sed -n 's#^@rpath/\([^/]*\.dylib\)$#\1#p'
|
||||
done | LC_ALL=C sort -u
|
||||
}
|
||||
|
||||
if ! otool -l "$BUNDLE/Contents/MacOS/$APPNAME" | grep -q "@executable_path/../Frameworks" ; then
|
||||
install_name_tool -add_rpath "@executable_path/../Frameworks" "$BUNDLE/Contents/MacOS/$APPNAME"
|
||||
echo " Added LC_RPATH @executable_path/../Frameworks to $APPNAME"
|
||||
fi
|
||||
|
||||
# 3 passes, since each copied library can bring its own dependencies:
|
||||
# a. rewrite absolute /opt/homebrew references (install ids included),
|
||||
# copying the referenced library into Frameworks/ if needed
|
||||
# b. copy the @rpath/libX.dylib still missing from Frameworks/
|
||||
for PASS in 1 2 3; do
|
||||
list_macho | while read bin; do
|
||||
_id=$(otool -D "$bin" 2>/dev/null | sed -n 2p)
|
||||
case "$_id" in
|
||||
/opt/homebrew/*)
|
||||
install_name_tool -id "@rpath/$(basename "$_id")" "$bin" 2>/dev/null
|
||||
echo " Fixed id (pass $PASS): $(basename "$bin")"
|
||||
;;
|
||||
esac
|
||||
list_deps "$bin" | grep '^/opt/homebrew/' | while read ref; do
|
||||
name=$(basename "$ref")
|
||||
if [ ! -e "$FW/$name" ]; then
|
||||
cp -L "$ref" "$FW/$name" && chmod u+w "$FW/$name"
|
||||
echo " Copied (pass $PASS): $name"
|
||||
fi
|
||||
install_name_tool -change "$ref" "@rpath/$name" "$bin" 2>/dev/null
|
||||
echo " Fixed ref (pass $PASS): $(basename "$bin") -> @rpath/$name"
|
||||
done
|
||||
done
|
||||
list_rpath_libs | while read lib; do
|
||||
if [ ! -e "$FW/$lib" ] && [ -e "/opt/homebrew/lib/$lib" ]; then
|
||||
cp -L "/opt/homebrew/lib/$lib" "$FW/$lib" && chmod u+w "$FW/$lib"
|
||||
echo " Copied (pass $PASS): $lib"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# 3. Checks
|
||||
UNRESOLVED=$(list_rpath_libs | while read lib; do [ -e "$FW/$lib" ] || echo "$lib"; done)
|
||||
if [ -n "$UNRESOLVED" ]; then
|
||||
echo "ERROR: @rpath libraries still missing from Frameworks/:" $UNRESOLVED
|
||||
exit 1
|
||||
fi
|
||||
HOMEBREW_REFS=$(list_macho | while read bin; do
|
||||
otool -L "$bin" 2>/dev/null | awk 'NR>1 { print $1 }' | grep '^/opt/homebrew/' \
|
||||
| sed "s#^# $(basename "$bin") -> #"
|
||||
done)
|
||||
if [ -n "$HOMEBREW_REFS" ]; then
|
||||
echo "ERROR: bundle still references Homebrew paths:"
|
||||
echo "$HOMEBREW_REFS"
|
||||
exit 1
|
||||
fi
|
||||
echo "All dependencies resolved inside the bundle."
|
||||
|
||||
### install Info.plist and app icon #################################
|
||||
# NOTE: this must run AFTER macdeployqt, not before. macdeployqt
|
||||
# rewrites/regenerates parts of Contents/Resources, and files copied
|
||||
@@ -175,7 +268,6 @@ QET_LANG_DIR="${current_dir}/lang/"
|
||||
QET_EXAMPLES_DIR="${current_dir}/examples/"
|
||||
QET_FONTS_DIR="${current_dir}/fonts/"
|
||||
QET_LICENSES_DIR="${current_dir}/licenses/"
|
||||
LANG_DIR="${current_dir}/lang1/"
|
||||
|
||||
if [ -d "${QET_ELMT_DIR}" ]; then
|
||||
cp -R ${QET_ELMT_DIR} $BUNDLE/Contents/Resources/elements
|
||||
@@ -183,12 +275,85 @@ fi
|
||||
if [ -d "${QET_TBT_DIR}" ]; then
|
||||
cp -R ${QET_TBT_DIR} $BUNDLE/Contents/Resources/titleblocks
|
||||
fi
|
||||
if [ -d "${QET_LANG_DIR}" ]; then
|
||||
mkdir $BUNDLE/Contents/Resources/lang
|
||||
cp ${current_dir}/lang/*.qm $BUNDLE/Contents/Resources/lang
|
||||
# Traductions : depuis la PR #751, les .qm ne sont plus versionnes ; lrelease
|
||||
# les genere dans $BUILD_DIR/lang/. Jeu attendu = les .ts listes dans TS_FILES
|
||||
# (cmake/qet_compilation_vars.cmake) : un .qm manquant arrete le script, un
|
||||
# .ts present dans lang/ mais absent de TS_FILES donne seulement un WARNING.
|
||||
# Fichiers temporaires plutot que <(...) : /bin/sh de macOS (bash 3.2 en mode
|
||||
# POSIX) n'a pas la substitution de processus.
|
||||
QM_SRC="${current_dir}/${BUILD_DIR}/lang"
|
||||
QM_TMP=$(mktemp -d /tmp/qet_qm.XXXXXX)
|
||||
grep -o 'lang/qet_[A-Za-z_]*\.ts' "${current_dir}/cmake/qet_compilation_vars.cmake" \
|
||||
| sed -e 's#^lang/##' -e 's#\.ts$##' | LC_ALL=C sort -u > "$QM_TMP/listed"
|
||||
if [ ! -s "$QM_TMP/listed" ]; then
|
||||
echo "ERROR: cannot read TS_FILES from cmake/qet_compilation_vars.cmake"
|
||||
rm -rf "$QM_TMP"
|
||||
exit 1
|
||||
fi
|
||||
if [ -d "${LANG_DIR}" ]; then
|
||||
cp ${current_dir}/lang1/*.qm $BUNDLE/Contents/Resources/lang
|
||||
find "${QET_LANG_DIR}" -maxdepth 1 -name 'qet_*.ts' -exec basename {} .ts \; | LC_ALL=C sort > "$QM_TMP/present"
|
||||
UNLISTED=$(LC_ALL=C comm -13 "$QM_TMP/listed" "$QM_TMP/present")
|
||||
if [ -n "$UNLISTED" ]; then
|
||||
echo "WARNING: .ts files not in TS_FILES, no .qm built:" $UNLISTED
|
||||
fi
|
||||
mkdir -p $BUNDLE/Contents/Resources/lang
|
||||
find "${QM_SRC}" -maxdepth 1 -name 'qet_*.qm' -exec cp {} $BUNDLE/Contents/Resources/lang/ \; 2>/dev/null
|
||||
find $BUNDLE/Contents/Resources/lang -maxdepth 1 -name 'qet_*.qm' -exec basename {} .qm \; | LC_ALL=C sort > "$QM_TMP/built"
|
||||
MISSING=$(LC_ALL=C comm -23 "$QM_TMP/listed" "$QM_TMP/built")
|
||||
echo "$(wc -l < "$QM_TMP/built" | tr -d ' ') .qm files copied to Contents/Resources/lang (expected: $(wc -l < "$QM_TMP/listed" | tr -d ' '))"
|
||||
|
||||
# Traductions de Qt lui-meme (boutons OK/Annuler, dialogues standard...) :
|
||||
# elles viennent de qtbase_XX.qm, pas des .ts de QET, et macdeployqt ne les
|
||||
# deploie pas. QETApp::setLanguage() charge "qt_XX" depuis le chemin de
|
||||
# traductions de Qt (absent du bundle), puis depuis le dossier lang/ de QET :
|
||||
# on y depose donc chaque qtbase_XX.qm sous le nom qt_XX.qm. qtbase_XX.qm est
|
||||
# autonome, contrairement aux qt_XX.qm de Qt qui dependent de tous les modules.
|
||||
# Premier dossier contenant des qtbase_*.qm : celui annonce par qtpaths, puis
|
||||
# la formule Homebrew separee qttranslations, puis les autres emplacements
|
||||
# Homebrew possibles. Homebrew fait de ces dossiers des liens symboliques
|
||||
# (-> Cellar/qttranslations/...) : find doit donc les suivre (-L), sinon il
|
||||
# ne voit que le lien lui-meme et ne trouve aucun fichier dedans.
|
||||
QT_TR_DIR=""
|
||||
for d in "$("$QT_PREFIX/bin/qtpaths" --query QT_INSTALL_TRANSLATIONS 2>/dev/null)" \
|
||||
"$(brew --prefix qttranslations 2>/dev/null)/share/qt/translations" \
|
||||
"$QT_PREFIX/share/qt/translations" \
|
||||
/opt/homebrew/share/qt/translations \
|
||||
/opt/homebrew/opt/*/share/qt/translations ; do
|
||||
if ls "$d"/qtbase_*.qm >/dev/null 2>&1 ; then
|
||||
QT_TR_DIR="$d"
|
||||
break
|
||||
fi
|
||||
done
|
||||
LANG_DST="$BUNDLE/Contents/Resources/lang"
|
||||
find -L "$QT_TR_DIR" -maxdepth 1 -name 'qtbase_*.qm' 2>/dev/null | while read f; do
|
||||
l=$(basename "$f" .qm | sed 's/^qtbase_//')
|
||||
cp "$f" "$LANG_DST/qt_$l.qm"
|
||||
done
|
||||
# Langues QET sans equivalent Qt exact (pt -> pt_PT, zh -> zh_CN...) :
|
||||
# QTranslator ne sait que raccourcir le code (fr_FR -> fr), pas l'allonger.
|
||||
# On prefere la variante "principale" (pt_PT), sinon la premiere trouvee.
|
||||
sed 's/^qet_//' "$QM_TMP/listed" | while read l; do
|
||||
if [ ! -e "$LANG_DST/qt_$l.qm" ]; then
|
||||
main="$QT_TR_DIR/qtbase_${l}_$(echo "$l" | tr 'a-z' 'A-Z').qm"
|
||||
if [ -e "$main" ]; then
|
||||
alt="$main"
|
||||
else
|
||||
alt=$(find -L "$QT_TR_DIR" -maxdepth 1 -name "qtbase_${l}_*.qm" 2>/dev/null | LC_ALL=C sort | head -1)
|
||||
fi
|
||||
[ -n "$alt" ] && cp "$alt" "$LANG_DST/qt_$l.qm"
|
||||
fi
|
||||
done
|
||||
QT_QM_COUNT=$(find "$LANG_DST" -maxdepth 1 -name 'qt_*.qm' | wc -l | tr -d ' ')
|
||||
echo "${QT_QM_COUNT} Qt translation files (qt_*.qm) copied from ${QT_TR_DIR:-<not found>}"
|
||||
if [ "${QT_QM_COUNT}" -eq 0 ]; then
|
||||
echo "ERROR: no qtbase_*.qm found (Qt translations not installed?)."
|
||||
echo " Check with: find /opt/homebrew -name 'qtbase_fr.qm'"
|
||||
rm -rf "$QM_TMP"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$QM_TMP"
|
||||
if [ -n "$MISSING" ]; then
|
||||
echo "ERROR: missing translations:" $MISSING
|
||||
exit 1
|
||||
fi
|
||||
if [ -d "${QET_EXAMPLES_DIR}" ]; then
|
||||
mkdir $BUNDLE/Contents/Resources/examples
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetxml.h"
|
||||
#include "../qetproject.h"
|
||||
#include <QDir>
|
||||
#include <QDomDocument>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
#include <utility>
|
||||
@@ -703,6 +705,74 @@ namespace autonum
|
||||
return formula;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief prefixFromLabelFile
|
||||
Look up a prefix for @a path (path[dirLevel] outermost, path[1] the
|
||||
deepest directory; path[0], the element's own file name, is never
|
||||
matched) in the qet_labels.xml at @a filepath.
|
||||
|
||||
Descends through nested \<category name="..."\> elements matching
|
||||
path[dirLevel], path[dirLevel-1], ..., path[1] in turn, considering
|
||||
only *direct* children at each step -- unlike a flat token scan,
|
||||
this cannot be fooled by a same-named category living elsewhere in
|
||||
the document at the wrong nesting depth (bugtracker #671 item 5).
|
||||
|
||||
At each matched level, that category's own \<prefix\> child -- even
|
||||
an empty one -- overrides whatever a shallower ancestor already
|
||||
provided, so an explicit empty \<prefix/\> cancels inheritance
|
||||
rather than silently falling back to it (the behaviour requested in
|
||||
PR #686 review). A category with no \<prefix\> child at all leaves
|
||||
the inherited value untouched, which is how a directory with no
|
||||
prefix of its own comes to inherit its parent's, as the file's own
|
||||
header comment documents.
|
||||
|
||||
@return the prefix that applies, or a null QString if the file
|
||||
cannot be read, is not well-formed, or does not describe this
|
||||
path at all (as opposed to describing it with no prefix
|
||||
anywhere along it, which is a non-null empty string).
|
||||
*/
|
||||
static QString prefixFromLabelFile(const QString &filepath, const QStringList &path, int dirLevel)
|
||||
{
|
||||
QFile file(filepath);
|
||||
if (!file.open(QFile::ReadOnly | QFile::Text))
|
||||
return QString();
|
||||
|
||||
QDomDocument document;
|
||||
if (!document.setContent(&file))
|
||||
return QString();
|
||||
|
||||
QDomElement node = document.documentElement();
|
||||
if (node.isNull())
|
||||
return QString();
|
||||
|
||||
QString prefix;
|
||||
for (int i = dirLevel ; i >= 1 ; --i) {
|
||||
QDomElement child = node.firstChildElement(QStringLiteral("category"));
|
||||
while (!child.isNull()
|
||||
&& child.attribute(QStringLiteral("name")) != path[i]) {
|
||||
child = child.nextSiblingElement(QStringLiteral("category"));
|
||||
}
|
||||
if (child.isNull())
|
||||
return QString();
|
||||
node = child;
|
||||
|
||||
const QDomElement own = node.firstChildElement(QStringLiteral("prefix"));
|
||||
if (!own.isNull()) {
|
||||
//readElementText()'s null-vs-empty distinction that PR
|
||||
//#686 needed for the old QXmlStreamReader-based lookup
|
||||
//has a QDomElement equivalent: text() on an empty
|
||||
//element can itself come back null depending on how the
|
||||
//XML was written, so the same explicit fallback applies
|
||||
//-- an empty QString here means "found, deliberately
|
||||
//blank", not "not found".
|
||||
prefix = own.text();
|
||||
if (prefix.isNull())
|
||||
prefix = QString("");
|
||||
}
|
||||
}
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief elementPrefixForLocation
|
||||
@param location
|
||||
@@ -716,114 +786,85 @@ namespace autonum
|
||||
if (!location.isProject())
|
||||
return QString();
|
||||
|
||||
QXmlStreamReader rxml;
|
||||
QString path[10];
|
||||
int i = -1;
|
||||
//Directory names from the element up to (not including) the
|
||||
//collection root, outermost last -- path[dirLevel] is the
|
||||
//top-level category, path[1] the element's immediate parent
|
||||
//directory, path[0] the element's own file name (never matched
|
||||
//against a category: the search stops descending once it has
|
||||
//matched path[1], the deepest real directory). An unbounded
|
||||
//QStringList rather than a fixed-size array, because a custom
|
||||
//collection can nest deeper than the shipped one -- see
|
||||
//bugtracker #671 item 3.
|
||||
QStringList path;
|
||||
ElementsLocation current_location = location;
|
||||
int dirLevel = -1;
|
||||
|
||||
//Add location name to path array
|
||||
while((current_location.parent() != current_location) && (current_location.parent().fileName() != "import"))
|
||||
while ((current_location.parent() != current_location)
|
||||
&& (current_location.parent().fileName() != "import"))
|
||||
{
|
||||
i++;
|
||||
path[i]=current_location.fileName();
|
||||
path << current_location.fileName();
|
||||
current_location = current_location.parent();
|
||||
dirLevel++;
|
||||
}
|
||||
//User Element without folder treatment
|
||||
if (i == -1)
|
||||
{
|
||||
i = 0;
|
||||
path[i]=current_location.fileName();
|
||||
//User element without folder treatment
|
||||
if (path.isEmpty()) {
|
||||
path << current_location.fileName();
|
||||
current_location = current_location.parent();
|
||||
dirLevel = 0;
|
||||
}
|
||||
const int dirLevel = path.size() - 1;
|
||||
//Name of the top-level tree the element's path was found
|
||||
//under, e.g. "10_electric" -- or, for a custom/company
|
||||
//collection not organised that way, whatever its top-level
|
||||
//folder happens to be called.
|
||||
const QString collection_root = current_location.fileName();
|
||||
|
||||
//Every top-level common-collection tree (10_electric,
|
||||
//20_logic, 30_hydraulic, ...) may carry its own
|
||||
//qet_labels.xml, with categories relative to that tree, the
|
||||
//same way 10_electric/qet_labels.xml already does -- not just
|
||||
//10_electric, which is all the hardcoded check this replaces
|
||||
//used to allow (bugtracker #671 item 2). commonElementsDir()
|
||||
//-- unlike customElementsDir(), which normalises this itself
|
||||
//-- returns whatever path the user configured verbatim, with
|
||||
//no guaranteed trailing separator; concatenating a suffix onto
|
||||
//it directly used to silently mangle the path (and so the
|
||||
//prefix lookup) for any install relocated to a directory
|
||||
//without a trailing slash (#671 item 1). QDir::filePath()
|
||||
//joins correctly either way.
|
||||
{
|
||||
const QString common_file = QDir(QETApp::commonElementsDir())
|
||||
.filePath(collection_root + QStringLiteral("/qet_labels.xml"));
|
||||
const QString prefix = prefixFromLabelFile(common_file, path, dirLevel);
|
||||
if (!prefix.isNull()) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Custom labels if qet_labels.xml exits in customElementsDir
|
||||
if (current_location.fileName() != "10_electric"){
|
||||
QString custom_labels = "qet_labels.xml";
|
||||
QString customfilepath = QETApp::customElementsDir().append(custom_labels);
|
||||
|
||||
QFile file(customfilepath);
|
||||
file.isReadable();
|
||||
if (!file.open(QFile::ReadOnly | QFile::Text))
|
||||
return QString();
|
||||
rxml.setDevice(&file);
|
||||
rxml.readNext();
|
||||
/* Which collection an element actually came from is not
|
||||
* recoverable post-import (addElement() strips the protocol),
|
||||
* so custom and company labels files are tried against two
|
||||
* possible layouts: with the collection's top-level tree name
|
||||
* folded into the path (a custom/company file organised as a
|
||||
* mirror of the common collection, tree name included) and
|
||||
* without it (a file scoped to just this one tree, matching
|
||||
* how the common collection's own files are written). Custom
|
||||
* is tried before company, so a user override wins over a
|
||||
* shared one.
|
||||
*/
|
||||
QStringList path_from_root = path;
|
||||
path_from_root << collection_root;
|
||||
|
||||
while(!rxml.atEnd())
|
||||
{
|
||||
if (rxml.attributes().value("name").toString() == path[i])
|
||||
{
|
||||
rxml.readNext();
|
||||
i=i-1;
|
||||
//reached element directory
|
||||
if (i==0)
|
||||
{
|
||||
for (int j=i; j<= dirLevel; j = j +1)
|
||||
{
|
||||
//if there is a prefix available apply prefix
|
||||
if(rxml.name().toString()=="prefix")
|
||||
{
|
||||
return rxml.readElementText();
|
||||
}
|
||||
//if there isn't a prefix available, find parent prefix in parent folder
|
||||
else
|
||||
{
|
||||
while (rxml.readNextStartElement() && rxml.name().toString()!="prefix")
|
||||
{
|
||||
rxml.skipCurrentElement();
|
||||
rxml.readNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
const QStringList candidate_dirs = {
|
||||
QETApp::customElementsDir(),
|
||||
QETApp::companyElementsDir()
|
||||
};
|
||||
for (const QString &dir : candidate_dirs) {
|
||||
const QString candidate =
|
||||
QDir(dir).filePath(QStringLiteral("qet_labels.xml"));
|
||||
for (const QStringList &segments : {path_from_root, path}) {
|
||||
const QString prefix = prefixFromLabelFile(
|
||||
candidate, segments, segments.size() - 1);
|
||||
if (!prefix.isNull()) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
rxml.readNext();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QString qet_labels = "10_electric/qet_labels.xml";
|
||||
QString filepath = QETApp::commonElementsDir().append(qet_labels);
|
||||
QFile file(filepath);
|
||||
file.isReadable();
|
||||
if (!file.open(QFile::ReadOnly | QFile::Text))
|
||||
return QString();
|
||||
|
||||
rxml.setDevice(&file);
|
||||
rxml.readNext();
|
||||
|
||||
while(!rxml.atEnd())
|
||||
{
|
||||
if (rxml.attributes().value("name").toString() == path[i])
|
||||
{
|
||||
rxml.readNext();
|
||||
i=i-1;
|
||||
//reached element directory
|
||||
if (i==0)
|
||||
{
|
||||
for (int j=i; j<= dirLevel; j = j +1)
|
||||
{
|
||||
//if there is a prefix available apply prefix
|
||||
if(rxml.name().toString()=="prefix")
|
||||
{
|
||||
return rxml.readElementText();
|
||||
}
|
||||
//if there isn't a prefix available, find parent prefix in parent folder
|
||||
else
|
||||
{
|
||||
while (rxml.readNextStartElement() && rxml.name().toString()!="prefix")
|
||||
{
|
||||
rxml.skipCurrentElement();
|
||||
rxml.readNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rxml.readNext();
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "bomexport.h"
|
||||
|
||||
#include <QSaveFile>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlRecord>
|
||||
#include <QVariant>
|
||||
|
||||
namespace {
|
||||
QString csvField(QString value)
|
||||
{
|
||||
value.replace(QLatin1Char('"'), QStringLiteral("\"\""));
|
||||
return QLatin1Char('"') + value + QLatin1Char('"');
|
||||
}
|
||||
|
||||
QByteArray csvRecord(const QStringList &values)
|
||||
{
|
||||
QStringList escaped;
|
||||
for (const auto &value : values) {
|
||||
escaped.append(csvField(value));
|
||||
}
|
||||
return (escaped.join(QLatin1Char(';')) + QLatin1Char('\n')).toUtf8();
|
||||
}
|
||||
}
|
||||
|
||||
QStringList BomExport::defaultColumns()
|
||||
{
|
||||
return {
|
||||
QStringLiteral("label"),
|
||||
QStringLiteral("designation"),
|
||||
QStringLiteral("manufacturer"),
|
||||
QStringLiteral("manufacturer_reference"),
|
||||
QStringLiteral("model"),
|
||||
QStringLiteral("description"),
|
||||
QStringLiteral("category"),
|
||||
QStringLiteral("quantity"),
|
||||
QStringLiteral("voltage_rating"),
|
||||
QStringLiteral("current_rating"),
|
||||
QStringLiteral("folio"),
|
||||
QStringLiteral("notes")
|
||||
};
|
||||
}
|
||||
|
||||
QString BomExport::defaultQuery()
|
||||
{
|
||||
return QStringLiteral("SELECT %1 FROM element_nomenclature_view "
|
||||
"WHERE ( element_type = 'simple' OR element_type = 'master') "
|
||||
"ORDER BY diagram_position, position, label")
|
||||
.arg(defaultColumns().join(QStringLiteral(", ")));
|
||||
}
|
||||
|
||||
QByteArray BomExport::toCsv(QSqlQuery &query, const QStringList &headers,
|
||||
bool include_headers, int *row_count)
|
||||
{
|
||||
QByteArray csv("\xEF\xBB\xBF");
|
||||
if (include_headers) {
|
||||
csv += csvRecord(headers);
|
||||
}
|
||||
int rows = 0;
|
||||
while (query.next())
|
||||
{
|
||||
QStringList values;
|
||||
for (int i = 0; i < query.record().count(); ++i) {
|
||||
values.append(query.value(i).toString());
|
||||
}
|
||||
csv += csvRecord(values);
|
||||
++rows;
|
||||
}
|
||||
if (row_count) {
|
||||
*row_count = rows;
|
||||
}
|
||||
return csv;
|
||||
}
|
||||
|
||||
bool BomExport::writeCsv(const QString &file_path, const QByteArray &csv,
|
||||
QString *error)
|
||||
{
|
||||
if (error) {
|
||||
error->clear();
|
||||
}
|
||||
QSaveFile file(file_path);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(csv) != csv.size())
|
||||
{
|
||||
if (error) {
|
||||
*error = file.errorString();
|
||||
}
|
||||
file.cancelWriting();
|
||||
return false;
|
||||
}
|
||||
if (!file.commit())
|
||||
{
|
||||
if (error) {
|
||||
*error = file.errorString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef BOMEXPORT_H
|
||||
#define BOMEXPORT_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
class QSqlQuery;
|
||||
|
||||
namespace BomExport
|
||||
{
|
||||
QStringList defaultColumns();
|
||||
QString defaultQuery();
|
||||
QByteArray toCsv(QSqlQuery &query, const QStringList &headers,
|
||||
bool include_headers = true, int *row_count = nullptr);
|
||||
bool writeCsv(const QString &file_path, const QByteArray &csv,
|
||||
QString *error = nullptr);
|
||||
}
|
||||
|
||||
#endif // BOMEXPORT_H
|
||||
+89
-41
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cli_export.h"
|
||||
#include "bomexport.h"
|
||||
|
||||
#include "bordertitleblock.h"
|
||||
#include "conductornumexport.h"
|
||||
@@ -39,6 +40,7 @@
|
||||
#include <QDomDocument>
|
||||
#include <QDate>
|
||||
#include <QFile>
|
||||
#include <QSaveFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
@@ -70,6 +72,7 @@ const QHash<QString, QString> &exportFlags()
|
||||
{"--export-cables", "cables"},
|
||||
{"--export-wires", "wires"},
|
||||
{"--export-bom", "bom"},
|
||||
{"--export-wiring", "wiring"},
|
||||
{"--export-nets", "nets"},
|
||||
{"--export-links", "links"},
|
||||
{"--info", "info"},
|
||||
@@ -302,61 +305,40 @@ int exportCsv(QETProject &project, const QString &format, const QString &output)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Quote a field for CSV output (RFC-4180 style, ';' delimiter).
|
||||
QString csvField(const QString &value)
|
||||
{
|
||||
if (value.contains(';') || value.contains('"')
|
||||
|| value.contains('\n') || value.contains('\r')) {
|
||||
QString v = value;
|
||||
v.replace('"', "\"\"");
|
||||
return '"' % v % '"';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Bill of materials: one row per element, key component-data fields.
|
||||
/// Pulls from QET's own project database (the same source as the GUI BOM
|
||||
/// export), so the output matches what the editor produces.
|
||||
/// Bill of materials from the same project database and default query as the
|
||||
/// GUI nomenclature export.
|
||||
int exportBom(QETProject &project, const QString &output)
|
||||
{
|
||||
// The project database is built lazily; force a (re)build before querying.
|
||||
project.dataBase()->updateDB();
|
||||
|
||||
static const QStringList columns {
|
||||
"label", "designation", "manufacturer", "manufacturer_reference",
|
||||
"quantity", "location", "function", "title", "folio"
|
||||
};
|
||||
|
||||
QSqlQuery query = project.dataBase()->newQuery(
|
||||
"SELECT " % columns.join(", ") %
|
||||
" FROM element_nomenclature_view ORDER BY label");
|
||||
QSqlQuery query = project.dataBase()->newQuery(BomExport::defaultQuery());
|
||||
if (!query.exec()) {
|
||||
err << "BOM query failed: " << query.lastError().text() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
QString csv = columns.join(";") % "\n";
|
||||
int rows = 0;
|
||||
while (query.next()) {
|
||||
QStringList values;
|
||||
for (int i = 0; i < columns.size(); ++i)
|
||||
values << csvField(query.value(i).toString());
|
||||
csv += values.join(";") % "\n";
|
||||
++rows;
|
||||
}
|
||||
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
const auto csv = BomExport::toCsv(
|
||||
query, BomExport::defaultColumns(), true, &rows);
|
||||
QString error;
|
||||
if (!BomExport::writeCsv(output, csv, &error)) {
|
||||
err << "Cannot write '" << output << "': " << error << "\n";
|
||||
return 1;
|
||||
}
|
||||
QTextStream fout(&file);
|
||||
fout << csv;
|
||||
file.close();
|
||||
out << "Exported " << rows << " component(s) -> " << output << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString csvField(const QString &value)
|
||||
{
|
||||
if (value.contains(QLatin1Char(';')) || value.contains(QLatin1Char('"'))
|
||||
|| value.contains(QLatin1Char('\n')) || value.contains(QLatin1Char('\r')))
|
||||
{
|
||||
QString escaped = value;
|
||||
escaped.replace(QLatin1Char('"'), QStringLiteral("\"\""));
|
||||
return QLatin1Char('"') % escaped % QLatin1Char('"');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Count terminals on @p element that no conductor connects to.
|
||||
int freeTerminals(Element *element)
|
||||
{
|
||||
@@ -543,6 +525,70 @@ QHash<Element *, int> folioIndex(QETProject &project)
|
||||
return folio;
|
||||
}
|
||||
|
||||
/// From-to wiring list: one row per conductor, each endpoint resolved to its
|
||||
/// element label and terminal name.
|
||||
///
|
||||
/// Reads wiring_list_view out of the project database. --export-cables produces
|
||||
/// the same logical list from the document XML instead, and the two are meant
|
||||
/// to agree: running both and diffing them is a direct check that the database
|
||||
/// still describes the project, which is otherwise only observable through the
|
||||
/// GUI.
|
||||
int exportWiring(QETProject &project, const QString &output)
|
||||
{
|
||||
// The project database is built lazily; force a (re)build before querying.
|
||||
project.dataBase()->updateDB();
|
||||
|
||||
static const QStringList columns {
|
||||
"wire_number", "from_element_label", "from_terminal",
|
||||
"to_element_label", "to_terminal", "diagram_position", "conductor_uuid"
|
||||
};
|
||||
|
||||
QSqlQuery query = project.dataBase()->newQuery(
|
||||
"SELECT " % columns.join(", ") %
|
||||
" FROM wiring_list_view"
|
||||
//Wire numbers are text, so a plain sort puts "10" before "9".
|
||||
//Numeric ones first, ordered by value; anything non-numeric after,
|
||||
//ordered as text. The trailing wire_number keeps ties stable.
|
||||
" ORDER BY diagram_position,"
|
||||
" CASE WHEN wire_number GLOB '[0-9]*' THEN 0 ELSE 1 END,"
|
||||
" CAST(wire_number AS INTEGER),"
|
||||
" wire_number");
|
||||
if (!query.exec()) {
|
||||
err << "Wiring list query failed: " << query.lastError().text() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
QString csv = columns.join(";") % "\n";
|
||||
int rows = 0;
|
||||
while (query.next()) {
|
||||
QStringList values;
|
||||
for (int i = 0; i < columns.size(); ++i)
|
||||
values << csvField(query.value(i).toString());
|
||||
csv += values.join(";") % "\n";
|
||||
++rows;
|
||||
}
|
||||
|
||||
//Written through QSaveFile so a failure part-way leaves the previous
|
||||
//file intact rather than a truncated one, and with a UTF-8 byte order
|
||||
//mark: without it Excel opens a .csv as the local 8-bit codepage and
|
||||
//mangles any accented element label. Qt writes UTF-8 by default, so
|
||||
//the bytes were already right -- the mark is what tells Excel so.
|
||||
QSaveFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
}
|
||||
static const char utf8_bom[] = "\xEF\xBB\xBF";
|
||||
file.write(utf8_bom, 3);
|
||||
file.write(csv.toUtf8());
|
||||
if (!file.commit()) {
|
||||
err << "Cannot write '" << output << "': " << file.errorString() << "\n";
|
||||
return 1;
|
||||
}
|
||||
out << "Exported " << rows << " conductor(s) -> " << output << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Electrical nets: groups of terminals joined into one potential.
|
||||
/// Walks QET's own potential graph, so each net is a connected component
|
||||
/// of terminals across all folios. The ground truth for connectivity.
|
||||
@@ -847,6 +893,8 @@ int run(const QStringList &args)
|
||||
return exportCsv(project, format, output);
|
||||
if (format == "bom")
|
||||
return exportBom(project, output);
|
||||
if (format == "wiring")
|
||||
return exportWiring(project, output);
|
||||
if (format == "nets")
|
||||
return exportNets(project, output);
|
||||
if (format == "links")
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace CLIExport {
|
||||
qelectrotech --export-cables <project.qet> <output.csv>
|
||||
qelectrotech --export-wires <project.qet> <output.csv>
|
||||
qelectrotech --export-bom <project.qet> <output.csv>
|
||||
qelectrotech --export-wiring <project.qet> <output.csv>
|
||||
qelectrotech --export-nets <project.qet> <output.json>
|
||||
qelectrotech --export-links <project.qet> <output.csv>
|
||||
qelectrotech --info <project.qet> [output.json]
|
||||
@@ -64,6 +65,11 @@ namespace CLIExport {
|
||||
cables: wiring list (one row per conductor) as CSV.
|
||||
wires: list of distinct wire numbers as CSV.
|
||||
bom: bill of materials (one row per element) as CSV.
|
||||
wiring: from-to wiring list (one row per conductor) as CSV, read from
|
||||
the project database. Same logical list as `cables`, which
|
||||
reads the document XML instead; the two are meant to agree,
|
||||
so diffing them checks that the database still describes the
|
||||
project.
|
||||
nets: electrical nets (connected-terminal groups) as JSON.
|
||||
links: element cross-references (coil/contact) as CSV, with
|
||||
unresolved links flagged.
|
||||
|
||||
@@ -124,15 +124,15 @@ void ConductorNumExport::fillHash()
|
||||
continue;
|
||||
}
|
||||
|
||||
//We must define if the connected terminal is a folio report, if it is the case
|
||||
//we don't add the num to the hash because the terminal doesn't represent a real terminal.
|
||||
if(!(c->terminal1->parentElement()->linkType() & Element::AllReport))
|
||||
{
|
||||
int value = m_hash.value(num, 0);
|
||||
++value;
|
||||
m_hash.insert(num, value);
|
||||
}
|
||||
if(!(c->terminal2->parentElement()->linkType() & Element::AllReport))
|
||||
//We must define if the connected terminals are folio reports, if
|
||||
//both are the case we don't add the num to the hash because
|
||||
//neither terminal represents a real terminal. Otherwise the
|
||||
//conductor counts once, regardless of how many of its two ends
|
||||
//are real -- counting per terminal instead of per conductor
|
||||
//doubled every ordinary two-real-terminal conductor's tally.
|
||||
bool terminal1_is_report = c->terminal1->parentElement()->linkType() & Element::AllReport;
|
||||
bool terminal2_is_report = c->terminal2->parentElement()->linkType() & Element::AllReport;
|
||||
if (!(terminal1_is_report && terminal2_is_report))
|
||||
{
|
||||
int value = m_hash.value(num, 0);
|
||||
++value;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CONTACTUSAGE_H
|
||||
#define CONTACTUSAGE_H
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
/**
|
||||
@brief The ContactUsage struct
|
||||
How many slave contacts a master element currently uses, broken down
|
||||
by contact type.
|
||||
|
||||
Header-only and free of any graphics dependency so that the counting
|
||||
rules can be unit tested on their own. MasterElement::contactUsage()
|
||||
is the thin wrapper that feeds it the linked elements.
|
||||
|
||||
This counts contacts, which is what tells you how many contacts an
|
||||
auxiliary block must provide. It is deliberately not the count that
|
||||
MasterElement::isFull() uses: a master's max_slaves is a number of
|
||||
slots, and a slave fills exactly one slot however many contacts it
|
||||
carries.
|
||||
|
||||
Two rules are easy to get wrong, and both live here so that every
|
||||
caller gets them right:
|
||||
- a slave stands for as many contacts as its "number" kind
|
||||
information says, so a 4 pole contact counts as 4, not as 1
|
||||
- a changeover contact is counted once, as sw. CrossRefItem's
|
||||
NOElements() and NCElements() both return it, so adding those two
|
||||
lists together would count it twice.
|
||||
*/
|
||||
struct ContactUsage
|
||||
{
|
||||
/**
|
||||
Contact types a slave can declare. Mirrors
|
||||
ElementData::SlaveState, which is not used directly so that this
|
||||
header stays free of the element data dependencies and can be
|
||||
unit tested on its own. MasterElement::contactUsage() maps
|
||||
between the two.
|
||||
*/
|
||||
enum Type
|
||||
{
|
||||
NO, ///< Normally open
|
||||
NC, ///< Normally closed
|
||||
SW, ///< Changeover
|
||||
Other ///< Neither of the above
|
||||
};
|
||||
|
||||
int no = 0; ///< Normally open
|
||||
int nc = 0; ///< Normally closed
|
||||
int sw = 0; ///< Changeover
|
||||
int other = 0; ///< Neither of the above
|
||||
|
||||
int total() const { return no + nc + sw + other; }
|
||||
|
||||
/**
|
||||
Add one slave element to the tally.
|
||||
@param type the contact type the slave declares
|
||||
@param contacts how many contacts it stands for. Values below 1
|
||||
are treated as 1: an element which declares no
|
||||
contact count is still one contact.
|
||||
*/
|
||||
void addSlave(Type type, int contacts)
|
||||
{
|
||||
const int n = std::max(1, contacts);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case NO: no += n; break;
|
||||
case NC: nc += n; break;
|
||||
case SW: sw += n; break;
|
||||
case Other: other += n; break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // CONTACTUSAGE_H
|
||||
@@ -53,6 +53,7 @@ projectDataBase::projectDataBase(QETProject *project, QObject *parent) :
|
||||
});
|
||||
connect(m_project, &QETProject::projectDiagramsOrderChanged, [this]()
|
||||
{
|
||||
m_content_changed = true;
|
||||
for (auto diagram : m_project->diagrams())
|
||||
{
|
||||
m_diagram_order_changed.bindValue(":pos", m_project->folioIndex(diagram)+1);
|
||||
@@ -85,14 +86,42 @@ projectDataBase::~projectDataBase()
|
||||
*/
|
||||
void projectDataBase::updateDB()
|
||||
{
|
||||
//A bulk operation is in progress and updates the database itself once
|
||||
//it is done : rebuilding now would only be thrown away by that final
|
||||
//rebuild. @see setUpdateBlocked().
|
||||
if (m_update_blocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Nothing in the project has changed since the last rebuild, so
|
||||
//repopulating would insert exactly the rows that are already there.
|
||||
//The signal is still emitted : callers and models rely on it to
|
||||
//refresh, and what they read back is unchanged either way.
|
||||
if (!m_content_changed)
|
||||
{
|
||||
emit dataBaseUpdated();
|
||||
return;
|
||||
}
|
||||
|
||||
populateDiagramTable();
|
||||
populateDiagramInfoTable();
|
||||
populateElementTable();
|
||||
populateElementInfoTable();
|
||||
populateConductorTable();
|
||||
m_content_changed = false;
|
||||
|
||||
emit dataBaseUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::setUpdateBlocked
|
||||
@param blocked : whether updateDB() should skip the full rebuild
|
||||
*/
|
||||
void projectDataBase::setUpdateBlocked(bool blocked)
|
||||
{
|
||||
m_update_blocked = blocked;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::project
|
||||
@return the project of this database
|
||||
@@ -111,35 +140,65 @@ QSqlQuery projectDataBase::newQuery(const QString &query) {
|
||||
return QSqlQuery(query, m_data_base);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::excludedConductorCount
|
||||
@return how many conductors of the project are absent from the conductor
|
||||
table because an endpoint has no parent element to key on.
|
||||
|
||||
Counted from the live scene rather than from the database, precisely
|
||||
because the database is where these conductors are *not*.
|
||||
|
||||
This used to count conductors whose terminals had no uuid, which was most
|
||||
of them on most projects. Terminal::stableUuid() now derives an identity
|
||||
from the terminal's geometry when the definition provides no uuid, so that
|
||||
is no longer a reason to exclude anything, and this counts only the case
|
||||
that remains genuinely unkeyable.
|
||||
|
||||
This is what lets a caller tell the user "N wires are missing and here
|
||||
is why", instead of silently presenting a short list as if it were
|
||||
complete.
|
||||
*/
|
||||
int projectDataBase::excludedConductorCount() const
|
||||
{
|
||||
if (!m_project) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (auto *diagram : m_project->diagrams())
|
||||
{
|
||||
const auto conductor_list = diagram->conductors();
|
||||
for (auto *conductor : conductor_list)
|
||||
{
|
||||
//Must match addConductor()'s guard exactly, or this reports
|
||||
//wires as missing that the list is in fact showing.
|
||||
if (!conductor->terminal1->parentElement()
|
||||
|| !conductor->terminal2->parentElement()) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::addElement
|
||||
@param element
|
||||
*/
|
||||
void projectDataBase::addElement(Element *element)
|
||||
{
|
||||
m_content_changed = true;
|
||||
if (!element || !element->diagram()) {
|
||||
qDebug() << "projectDataBase::addElement: null element or diagram";
|
||||
return;
|
||||
}
|
||||
|
||||
m_insert_elements_query.bindValue(":uuid", element->uuid().toString());
|
||||
m_insert_elements_query.bindValue(":diagram_uuid", element->diagram()->uuid().toString());
|
||||
m_insert_elements_query.bindValue(":pos", element->diagram()->convertPosition(element->scenePos()).toString());
|
||||
m_insert_elements_query.bindValue(":type", element->elementData().typeToString());
|
||||
m_insert_elements_query.bindValue(":sub_type", element->kindInformations()["type"].toString());
|
||||
bindElementValues(m_insert_elements_query, element, element->diagram());
|
||||
if (!m_insert_elements_query.exec()) {
|
||||
qDebug() << "projectDataBase::addElement insert element error : " << m_insert_elements_query.lastError();
|
||||
}
|
||||
|
||||
m_insert_element_info_query.bindValue(":uuid", element->uuid().toString());
|
||||
auto hash = elementInfoToString(element);
|
||||
for (auto key : hash.keys())
|
||||
{
|
||||
QString value = hash.value(key);
|
||||
QString bind = key.prepend(":");
|
||||
m_insert_element_info_query.bindValue(bind, value);
|
||||
}
|
||||
|
||||
bindElementInfoValues(m_insert_element_info_query, element);
|
||||
if (!m_insert_element_info_query.exec()) {
|
||||
qDebug() << "projectDataBase::addElement insert element info error : " << m_insert_element_info_query.lastError();
|
||||
} else {
|
||||
@@ -153,6 +212,7 @@ void projectDataBase::addElement(Element *element)
|
||||
*/
|
||||
void projectDataBase::removeElement(Element *element)
|
||||
{
|
||||
m_content_changed = true;
|
||||
m_remove_element_query.bindValue(":uuid", element->uuid().toString());
|
||||
if(!m_remove_element_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeElement remove error : " << m_remove_element_query.lastError();
|
||||
@@ -167,6 +227,7 @@ void projectDataBase::removeElement(Element *element)
|
||||
*/
|
||||
void projectDataBase::elementInfoChanged(Element *element)
|
||||
{
|
||||
m_content_changed = true;
|
||||
auto hash = elementInfoToString(element);
|
||||
for (auto str : QETInformation::elementInfoKeys()) {
|
||||
m_update_element_query.bindValue(":" + str, hash.value(str));
|
||||
@@ -181,6 +242,7 @@ void projectDataBase::elementInfoChanged(Element *element)
|
||||
|
||||
void projectDataBase::elementInfoChanged(QList<Element *> elements)
|
||||
{
|
||||
m_content_changed = true;
|
||||
this->blockSignals(true);
|
||||
//Block signal for not emit dataBaseUpdated at
|
||||
//each call of the method elementInfoChanged(Element *element)
|
||||
@@ -197,6 +259,7 @@ void projectDataBase::elementInfoChanged(QList<Element *> elements)
|
||||
|
||||
void projectDataBase::addDiagram(Diagram *diagram)
|
||||
{
|
||||
m_content_changed = true;
|
||||
m_insert_diagram_query.bindValue(":uuid", diagram->uuid().toString());
|
||||
m_insert_diagram_query.bindValue(":pos", m_project->folioIndex(diagram)+1);
|
||||
if(!m_insert_diagram_query.exec()) {
|
||||
@@ -225,6 +288,7 @@ void projectDataBase::addDiagram(Diagram *diagram)
|
||||
|
||||
void projectDataBase::removeDiagram(Diagram *diagram)
|
||||
{
|
||||
m_content_changed = true;
|
||||
const QString uuid_str = diagram->uuid().toString();
|
||||
|
||||
//Order matters: element_info and terminal are scoped through a
|
||||
@@ -280,6 +344,7 @@ void projectDataBase::removeDiagram(Diagram *diagram)
|
||||
|
||||
void projectDataBase::diagramInfoChanged(Diagram *diagram)
|
||||
{
|
||||
m_content_changed = true;
|
||||
bindDiagramInfoValues(m_update_diagram_info_query, diagram);
|
||||
|
||||
if (!m_update_diagram_info_query.exec()) {
|
||||
@@ -291,6 +356,7 @@ void projectDataBase::diagramInfoChanged(Diagram *diagram)
|
||||
|
||||
void projectDataBase::diagramOrderChanged()
|
||||
{
|
||||
m_content_changed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,6 +365,7 @@ void projectDataBase::diagramOrderChanged()
|
||||
*/
|
||||
void projectDataBase::addConductor(Conductor *conductor)
|
||||
{
|
||||
m_content_changed = true;
|
||||
if (!conductor || !conductor->diagram()) {
|
||||
qDebug() << "projectDataBase::addConductor: null conductor or diagram";
|
||||
return;
|
||||
@@ -332,6 +399,7 @@ void projectDataBase::addConductor(Conductor *conductor)
|
||||
*/
|
||||
void projectDataBase::removeConductor(Conductor *conductor)
|
||||
{
|
||||
m_content_changed = true;
|
||||
m_remove_conductor_query.bindValue(":uuid", conductor->uuid().toString());
|
||||
if (!m_remove_conductor_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeConductor delete error : " << m_remove_conductor_query.lastError();
|
||||
@@ -352,6 +420,7 @@ void projectDataBase::removeConductor(Conductor *conductor)
|
||||
*/
|
||||
void projectDataBase::updateConductor(Conductor *conductor)
|
||||
{
|
||||
m_content_changed = true;
|
||||
if (!conductor) {
|
||||
return;
|
||||
}
|
||||
@@ -394,6 +463,7 @@ void projectDataBase::watchConductor(Conductor *conductor)
|
||||
*/
|
||||
void projectDataBase::conductorPropertiesChanged()
|
||||
{
|
||||
m_content_changed = true;
|
||||
if (auto *conductor = qobject_cast<Conductor *>(sender())) {
|
||||
updateConductor(conductor);
|
||||
}
|
||||
@@ -550,6 +620,7 @@ bool projectDataBase::createDataBase()
|
||||
|
||||
createElementNomenclatureView();
|
||||
createSummaryView();
|
||||
createWiringListView();
|
||||
prepareQuery();
|
||||
updateDB();
|
||||
return true;
|
||||
@@ -570,6 +641,11 @@ void projectDataBase::createElementNomenclatureView()
|
||||
"ei.designation AS designation,"
|
||||
"ei.manufacturer AS manufacturer,"
|
||||
"ei.manufacturer_reference AS manufacturer_reference,"
|
||||
"ei.model AS model,"
|
||||
"ei.category AS category,"
|
||||
"ei.voltage_rating AS voltage_rating,"
|
||||
"ei.current_rating AS current_rating,"
|
||||
"ei.notes AS notes,"
|
||||
"ei.machine_manufacturer_reference AS machine_manufacturer_reference,"
|
||||
"ei.supplier AS supplier,"
|
||||
"ei.quantity AS quantity,"
|
||||
@@ -628,7 +704,14 @@ void projectDataBase::createElementNomenclatureView()
|
||||
"di.folio AS folio,"
|
||||
"e.pos AS position "
|
||||
" FROM element_info ei, diagram_info di, element e, diagram d"
|
||||
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid AND (ei.exclude_from_bom IS NOT 'true')");
|
||||
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid"
|
||||
" AND COALESCE(LOWER(TRIM(ei.exclude_from_bom)), '') NOT IN ('true', '1', 'yes', 'on')"
|
||||
//The element table holds every element of the project; which
|
||||
//kinds belong in a nomenclature is this view's business, not
|
||||
//the table's. Kept identical to the mask populateElementTable()
|
||||
//used to apply, so what this view returns does not change --
|
||||
//a slave element (a relay contact) is still not a line item.
|
||||
" AND e.type IN ('simple', 'terminal', 'master', 'thumbnail')");
|
||||
|
||||
QSqlQuery query(m_data_base);
|
||||
if (!query.exec(create_view)) {
|
||||
@@ -667,6 +750,64 @@ void projectDataBase::createSummaryView()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::createWiringListView
|
||||
A from-to wiring list: one row per conductor, each endpoint resolved to
|
||||
its element label and terminal name.
|
||||
|
||||
Two deliberate differences from an ordinary inner-join view like
|
||||
element_nomenclature_view:
|
||||
|
||||
- No join to the element table. A terminal row already carries its
|
||||
element_uuid, so joining element back just to read the same uuid adds
|
||||
nothing -- and would actively drop rows, because populateElementTable()
|
||||
only inserts elements matching Simple|Terminal|Master|Thumbnail. Slave
|
||||
elements (relay contacts and the like, extremely common at the end of a
|
||||
wire) and report elements are absent from that table after a project
|
||||
load, so an inner join through it silently loses their conductors.
|
||||
- element_info is LEFT joined for the same reason. A wire whose endpoint
|
||||
element carries no info row still belongs in a wiring list; it comes
|
||||
back with an empty label rather than vanishing. Losing a wire from a
|
||||
wiring list is a worse failure than showing one with a blank end.
|
||||
|
||||
- diagram is LEFT joined for the same reason. It should
|
||||
always match, since QETProject::diagramAdded is wired to addDiagram()
|
||||
and a conductor cannot exist before its folio -- but an inner join here
|
||||
would make that an assumption the view silently enforces, and a wire
|
||||
missing from a wiring list is the one failure this view must not have.
|
||||
|
||||
The result is that this view returns exactly as many rows as the
|
||||
conductor table holds -- what is already excluded upstream (conductors
|
||||
on legacy terminals without uuids) stays excluded, and nothing new is
|
||||
dropped here. Only the terminal joins are inner, and both are guaranteed
|
||||
by insertTerminal() running for each endpoint before the conductor row
|
||||
is written.
|
||||
*/
|
||||
void projectDataBase::createWiringListView()
|
||||
{
|
||||
QString create_view ("CREATE VIEW wiring_list_view AS SELECT "
|
||||
"c.uuid AS conductor_uuid,"
|
||||
"c.text AS wire_number,"
|
||||
"t1.element_uuid AS from_element_uuid,"
|
||||
"ei1.label AS from_element_label,"
|
||||
"t1.name AS from_terminal,"
|
||||
"t2.element_uuid AS to_element_uuid,"
|
||||
"ei2.label AS to_element_label,"
|
||||
"t2.name AS to_terminal,"
|
||||
"d.pos AS diagram_position"
|
||||
" FROM conductor c"
|
||||
" JOIN terminal t1 ON c.terminal1_uuid = t1.uuid AND c.terminal1_element_uuid = t1.element_uuid"
|
||||
" JOIN terminal t2 ON c.terminal2_uuid = t2.uuid AND c.terminal2_element_uuid = t2.element_uuid"
|
||||
" LEFT JOIN element_info ei1 ON t1.element_uuid = ei1.element_uuid"
|
||||
" LEFT JOIN element_info ei2 ON t2.element_uuid = ei2.element_uuid"
|
||||
" LEFT JOIN diagram d ON c.diagram_uuid = d.uuid");
|
||||
|
||||
QSqlQuery query(m_data_base);
|
||||
if (!query.exec(create_view)) {
|
||||
qDebug() << query.lastError();
|
||||
}
|
||||
}
|
||||
|
||||
void projectDataBase::populateDiagramTable()
|
||||
{
|
||||
QSqlQuery query_(m_data_base);
|
||||
@@ -682,6 +823,30 @@ void projectDataBase::populateDiagramTable()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief allElementTypes
|
||||
Every ElementData::Type, i.e. no filtering at all.
|
||||
|
||||
The element table used to be populated with only
|
||||
Simple|Terminal|Master|Thumbnail, which quietly made it "the elements a
|
||||
nomenclature cares about" rather than "the elements of the project".
|
||||
Anything else reading the table -- the wiring list, and terminal plans
|
||||
later -- then could not see slave elements (relay contacts) or report
|
||||
elements, which are ordinary conductor endpoints. The filter now lives in
|
||||
element_nomenclature_view, where it belongs; see createElementNomenclatureView().
|
||||
*/
|
||||
static ElementData::Types allElementTypes()
|
||||
{
|
||||
return ElementData::Simple
|
||||
| ElementData::NextReport
|
||||
| ElementData::PreviousReport
|
||||
| ElementData::Master
|
||||
| ElementData::Slave
|
||||
| ElementData::Terminal
|
||||
| ElementData::Thumbnail
|
||||
| ElementData::ConductorDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::populateElementTable
|
||||
Populate the element table
|
||||
@@ -694,16 +859,11 @@ void projectDataBase::populateElementTable()
|
||||
for (auto diagram : m_project->diagrams())
|
||||
{
|
||||
const ElementProvider ep(diagram);
|
||||
const auto elmt_vector = ep.find(ElementData::Simple | ElementData::Terminal | ElementData::Master | ElementData::Thumbnail);
|
||||
const auto elmt_vector = ep.find(allElementTypes());
|
||||
//Insert all values into the database
|
||||
for (const auto &elmt : elmt_vector)
|
||||
{
|
||||
const auto elmt_data = elmt->elementData();
|
||||
m_insert_elements_query.bindValue(":uuid", elmt->uuid().toString());
|
||||
m_insert_elements_query.bindValue(":diagram_uuid", diagram->uuid().toString());
|
||||
m_insert_elements_query.bindValue(":pos", diagram->convertPosition(elmt->scenePos()).toString());
|
||||
m_insert_elements_query.bindValue(":type", elmt_data.typeToString());
|
||||
m_insert_elements_query.bindValue(":sub_type", elmt_data.masterTypeToString());
|
||||
bindElementValues(m_insert_elements_query, elmt, diagram);
|
||||
if (!m_insert_elements_query.exec()) {
|
||||
qDebug() << "projectDataBase::populateElementTable insert error : " << m_insert_elements_query.lastError();
|
||||
}
|
||||
@@ -723,20 +883,12 @@ void projectDataBase::populateElementInfoTable()
|
||||
for (const auto &diagram : m_project->diagrams())
|
||||
{
|
||||
const ElementProvider ep(diagram);
|
||||
const auto elmt_vector = ep.find(ElementData::Simple | ElementData::Terminal | ElementData::Master | ElementData::Thumbnail);
|
||||
const auto elmt_vector = ep.find(allElementTypes());
|
||||
|
||||
//Insert all values into the database
|
||||
for (const auto &elmt : elmt_vector)
|
||||
{
|
||||
m_insert_element_info_query.bindValue(QStringLiteral(":uuid"), elmt->uuid().toString());
|
||||
const auto hash = elementInfoToString(elmt);
|
||||
for (const auto &key : hash.keys())
|
||||
{
|
||||
QString value = hash.value(key);
|
||||
QString bind = QStringLiteral(":") + key;
|
||||
m_insert_element_info_query.bindValue(bind, value);
|
||||
}
|
||||
|
||||
bindElementInfoValues(m_insert_element_info_query, elmt);
|
||||
if (!m_insert_element_info_query.exec()) {
|
||||
qDebug() << "projectDataBase::populateElementInfoTable insert error : " << m_insert_element_info_query.lastError();
|
||||
}
|
||||
@@ -943,6 +1095,52 @@ QHash<QString, QString> projectDataBase::elementInfoToString(Element *elmt)
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::bindElementValues
|
||||
Bind one element's row for the element table.
|
||||
|
||||
Shared by addElement() (a single element added to a live diagram) and
|
||||
populateElementTable() (a full rebuild), because those two used to bind
|
||||
the same row differently: the incremental path wrote
|
||||
kindInformations()["type"] into sub_type while the bulk path wrote
|
||||
elementData().masterTypeToString(). The element table therefore held
|
||||
different values depending on whether the project had been reloaded
|
||||
since the element was placed. One binder means live and reloaded agree
|
||||
by construction rather than by coincidence.
|
||||
|
||||
The bulk path's values are the ones kept: they are what every already
|
||||
saved project contains, so nothing a reload produces changes.
|
||||
@param query : prepared insert query to bind into
|
||||
@param element : element to bind
|
||||
@param diagram : diagram holding @element
|
||||
*/
|
||||
void projectDataBase::bindElementValues(QSqlQuery &query, Element *element, Diagram *diagram)
|
||||
{
|
||||
const auto element_data = element->elementData();
|
||||
query.bindValue(QStringLiteral(":uuid"), element->uuid().toString());
|
||||
query.bindValue(QStringLiteral(":diagram_uuid"), diagram->uuid().toString());
|
||||
query.bindValue(QStringLiteral(":pos"), diagram->convertPosition(element->scenePos()).toString());
|
||||
query.bindValue(QStringLiteral(":type"), element_data.typeToString());
|
||||
query.bindValue(QStringLiteral(":sub_type"), element_data.masterTypeToString());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::bindElementInfoValues
|
||||
Bind one element's row for the element info table.
|
||||
Shared by addElement() and populateElementInfoTable() for the same
|
||||
reason as bindElementValues().
|
||||
@param query : prepared insert query to bind into
|
||||
@param element : element to bind
|
||||
*/
|
||||
void projectDataBase::bindElementInfoValues(QSqlQuery &query, Element *element)
|
||||
{
|
||||
query.bindValue(QStringLiteral(":uuid"), element->uuid().toString());
|
||||
const auto hash = elementInfoToString(element);
|
||||
for (const auto &key : hash.keys()) {
|
||||
query.bindValue(QStringLiteral(":") + key, hash.value(key));
|
||||
}
|
||||
}
|
||||
|
||||
void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram)
|
||||
{
|
||||
query.bindValue(":uuid", diagram->uuid());
|
||||
|
||||
@@ -47,8 +47,21 @@ class projectDataBase : public QObject
|
||||
virtual ~projectDataBase() override;
|
||||
|
||||
void updateDB();
|
||||
/**
|
||||
Suppress the full rebuild performed by updateDB().
|
||||
|
||||
While blocked, updateDB() returns immediately instead of
|
||||
repopulating every table. Meant for bulk operations -- notably
|
||||
loading a project, where each table model re-queries the
|
||||
database as it is built and would otherwise trigger one
|
||||
complete rebuild of it. The caller unblocks and calls
|
||||
updateDB() once when done; @see QETProject::readProjectXml().
|
||||
*/
|
||||
void setUpdateBlocked(bool blocked);
|
||||
QETProject *project() const;
|
||||
QSqlQuery newQuery(const QString &query = QString());
|
||||
QSqlDatabase database() const {return m_data_base;}
|
||||
int excludedConductorCount() const;
|
||||
|
||||
void addElement (Element *element);
|
||||
void removeElement (Element *element);
|
||||
@@ -77,6 +90,7 @@ class projectDataBase : public QObject
|
||||
bool createDataBase();
|
||||
void createElementNomenclatureView();
|
||||
void createSummaryView();
|
||||
void createWiringListView();
|
||||
void populateDiagramTable();
|
||||
void populateElementTable();
|
||||
void populateElementInfoTable();
|
||||
@@ -89,9 +103,17 @@ class projectDataBase : public QObject
|
||||
static QHash<QString, QString> elementInfoToString(
|
||||
Element *elmt);
|
||||
void bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram);
|
||||
static void bindElementValues(QSqlQuery &query, Element *element, Diagram *diagram);
|
||||
static void bindElementInfoValues(QSqlQuery &query, Element *element);
|
||||
|
||||
private:
|
||||
QPointer<QETProject> m_project;
|
||||
bool m_update_blocked = false;
|
||||
//Starts true : the database is empty until the first rebuild.
|
||||
//Set by every method of this class that writes rows, cleared by
|
||||
//updateDB(). Callers reach the database from outside only through
|
||||
//newQuery(), and every such call site reads.
|
||||
bool m_content_changed = true;
|
||||
QSqlDatabase m_data_base;
|
||||
QSqlQuery m_insert_elements_query,
|
||||
m_insert_element_info_query,
|
||||
|
||||
@@ -160,6 +160,8 @@ void ElementQueryWidget::setQuery(const QString &query)
|
||||
{
|
||||
auto str_type = rxm.captured(1);
|
||||
where.remove(str_type);
|
||||
const bool all_master_types = str_type.contains(
|
||||
ElementData::typeToString(ElementData::Master));
|
||||
|
||||
int c=0;
|
||||
ui->m_simple_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Simple)) ? true : false);
|
||||
@@ -170,23 +172,23 @@ void ElementQueryWidget::setQuery(const QString &query)
|
||||
if (ui->m_terminal_cb->isChecked()) {
|
||||
++c;
|
||||
}
|
||||
ui->m_coil_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::Coil)) ? true : false);
|
||||
ui->m_coil_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Coil)));
|
||||
if (ui->m_coil_cb->isChecked()) {
|
||||
++c;
|
||||
}
|
||||
ui->m_button_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::Commutator)) ? true : false);
|
||||
ui->m_button_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Commutator)));
|
||||
if (ui->m_button_cb->isChecked()) {
|
||||
++c;
|
||||
}
|
||||
ui->m_protection_cb->setChecked(str_type.contains(ElementData::masterTypeToString(ElementData::Protection)) ? true : false);
|
||||
if (ui->m_protection_cb) {
|
||||
ui->m_protection_cb->setChecked(all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Protection)));
|
||||
if (ui->m_protection_cb->isChecked()) {
|
||||
++c;
|
||||
}
|
||||
ui->m_thumbnail_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Thumbnail)) ? true : false);
|
||||
if (ui->m_thumbnail_cb->isChecked()) {
|
||||
++c;
|
||||
}
|
||||
ui->m_plc_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::PLC)) ? true : false);
|
||||
ui->m_plc_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::PLC)));
|
||||
if (ui->m_plc_cb->isChecked()) {
|
||||
++c;
|
||||
}
|
||||
@@ -352,22 +354,32 @@ QString ElementQueryWidget::queryStr() const
|
||||
where += QStringLiteral(" element_type = '") += ElementData::typeToString(ElementData::Simple) += "'";
|
||||
b = true;
|
||||
}
|
||||
if (ui->m_button_cb->isChecked()) {
|
||||
const bool all_master_types = ui->m_button_cb->isChecked()
|
||||
&& ui->m_coil_cb->isChecked()
|
||||
&& ui->m_protection_cb->isChecked()
|
||||
&& ui->m_plc_cb->isChecked();
|
||||
if (all_master_types) {
|
||||
if (b) where += " OR";
|
||||
where += QStringLiteral(" element_type = '")
|
||||
+= ElementData::typeToString(ElementData::Master) += "'";
|
||||
b = true;
|
||||
}
|
||||
if (!all_master_types && ui->m_button_cb->isChecked()) {
|
||||
if (b) where +=" OR";
|
||||
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Commutator) += "'";
|
||||
b = true;
|
||||
}
|
||||
if (ui->m_coil_cb->isChecked()) {
|
||||
if (!all_master_types && ui->m_coil_cb->isChecked()) {
|
||||
if (b) where +=" OR";
|
||||
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Coil) += "'";
|
||||
b = true;
|
||||
}
|
||||
if (ui->m_protection_cb->isChecked()) {
|
||||
if (!all_master_types && ui->m_protection_cb->isChecked()) {
|
||||
if (b) where +=" OR";
|
||||
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Protection) += "'";
|
||||
b = true;
|
||||
}
|
||||
if (ui->m_plc_cb->isChecked()) {
|
||||
if (!all_master_types && ui->m_plc_cb->isChecked()) {
|
||||
if (b) where +=" OR";
|
||||
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
|
||||
b = true;
|
||||
|
||||
+14
-1
@@ -56,7 +56,20 @@ namespace {
|
||||
/// non-deterministic across process runs for any legacy file.
|
||||
QString elementSortKey(Element *elmt)
|
||||
{
|
||||
return positionKey(elmt->pos());
|
||||
//Position alone is not a total order: two elements can sit at the
|
||||
//same x/y (lmdg.qet has a pair of text elements both at 780,350).
|
||||
//With equal keys std::stable_sort falls back to the order the
|
||||
//scene handed us, which varies per run, so those two swapped
|
||||
//places on every save. The uuid breaks the tie.
|
||||
//
|
||||
//For an element with a persisted uuid attribute this is fully
|
||||
//deterministic. For a legacy element without one, fromXml()
|
||||
//invents a fresh uuid per load, so a collision between two such
|
||||
//elements is no better ordered than before -- but no worse
|
||||
//either, and the tiebreaker is only consulted when the positions
|
||||
//are equal.
|
||||
return positionKey(elmt->pos())
|
||||
+ QLatin1Char(':') + elmt->uuid().toString();
|
||||
}
|
||||
|
||||
/// Sort key for a terminal: its parent element's position, then the
|
||||
|
||||
@@ -479,8 +479,13 @@ const QDomDocument ElementScene::toXml(bool all_parts)
|
||||
root.appendChild(m_element_data.kindInfoToXml(xml_document));
|
||||
}
|
||||
|
||||
//Slave is in this list because the element editor offers the
|
||||
//Informations tab for it, including the PLC-specific rows
|
||||
//populateTree() adds for a PLC slave. Without it the editor would
|
||||
//accept that data and silently drop it on save.
|
||||
if (type_ == ElementData::Simple ||
|
||||
type_ == ElementData::Master ||
|
||||
type_ == ElementData::Slave ||
|
||||
type_ == ElementData::Terminal ||
|
||||
type_ == ElementData::Thumbnail)
|
||||
{
|
||||
|
||||
@@ -245,7 +245,8 @@ void DynamicTextFieldEditor::fillInfoComboBox()
|
||||
else {
|
||||
strl = QETInformation::elementInfoKeys();
|
||||
|
||||
bool is_plc_slave = (type == ElementData::Slave
|
||||
bool is_slave = (type == ElementData::Slave);
|
||||
bool is_plc_slave = (is_slave
|
||||
&& ed.m_slave_type == ElementData::PLCSlave);
|
||||
|
||||
if (is_plc_slave) {
|
||||
@@ -274,6 +275,10 @@ void DynamicTextFieldEditor::fillInfoComboBox()
|
||||
strl.removeAll(QETInformation::ELMT_PLC_T3);
|
||||
strl.removeAll(QETInformation::ELMT_PLC_T4);
|
||||
}
|
||||
|
||||
if (is_slave) {
|
||||
strl.prepend(QETInformation::ELMT_XREF);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i=0; i<strl.size();++i) {
|
||||
|
||||
@@ -171,8 +171,17 @@ void ElementPropertiesEditorWidget::upDateInterface()
|
||||
ui->m_terminal_func_cb->setCurrentIndex(
|
||||
ui->m_terminal_func_cb->findData(
|
||||
m_data.m_terminal_function));
|
||||
|
||||
const DiagramContext &info = m_data.m_informations;
|
||||
ui->m_auto_num_locked_cb->setChecked(
|
||||
info.value(QStringLiteral("auto_num_locked")).toString() == QLatin1String("true"));
|
||||
ui->m_potential_isolating_cb->setChecked(
|
||||
info.value(QStringLiteral("potential_isolating")).toString() == QLatin1String("true"));
|
||||
}
|
||||
|
||||
ui->m_exclude_from_bom_cb->setChecked(
|
||||
m_data.m_informations.value(QStringLiteral("exclude_from_bom")).toString() == QLatin1String("true"));
|
||||
|
||||
on_m_base_type_cb_currentIndexChanged(ui->m_base_type_cb->currentIndex());
|
||||
}
|
||||
|
||||
@@ -292,7 +301,7 @@ void ElementPropertiesEditorWidget::updateTree()
|
||||
ui->m_tree->setEnabled(true);
|
||||
break;
|
||||
case ElementData::Slave:
|
||||
ui->m_tree->setDisabled(true);
|
||||
ui->m_tree->setEnabled(true);
|
||||
break;
|
||||
case ElementData::Terminal:
|
||||
ui->m_tree->setEnabled(true);
|
||||
@@ -366,12 +375,12 @@ void ElementPropertiesEditorWidget::on_m_buttonBox_accepted()
|
||||
m_data.m_slave_type = ui->m_type_cb->currentData().value<ElementData::SlaveType>();
|
||||
}
|
||||
m_data.m_contact_count = ui->m_number_ctc->value();
|
||||
}
|
||||
else if (m_data.m_type == ElementData::Master) {
|
||||
} else if (m_data.m_type == ElementData::Master)
|
||||
{
|
||||
m_data.m_master_type = ui->m_master_type_cb->currentData().value<ElementData::MasterType>();
|
||||
|
||||
//If the checkbox is checked, save the number; otherwise, -1 (infinity)
|
||||
if (ui->max_slaves_checkbox->isVisible() && ui->max_slaves_checkbox->isChecked()) {
|
||||
if ((m_data.m_master_type == ElementData::Coil || m_data.m_master_type == ElementData::Protection || m_data.m_master_type == ElementData::Commutator) && ui->max_slaves_checkbox->isChecked()) {
|
||||
m_data.m_max_slaves = ui->max_slaves_spinbox->value();
|
||||
} else {
|
||||
m_data.m_max_slaves = -1;
|
||||
@@ -382,13 +391,12 @@ void ElementPropertiesEditorWidget::on_m_buttonBox_accepted()
|
||||
} else {
|
||||
readSlaveGroupsFromTable();
|
||||
}
|
||||
}
|
||||
else if (m_data.m_type == ElementData::Terminal)
|
||||
} else if (m_data.m_type == ElementData::Terminal)
|
||||
{
|
||||
m_data.m_terminal_type = ui->m_terminal_type_cb->currentData().value<ElementData::TerminalType>();
|
||||
m_data.m_terminal_function = ui->m_terminal_func_cb->currentData().value<ElementData::TerminalFunction>();
|
||||
}
|
||||
|
||||
|
||||
for (QTreeWidgetItem *qtwi : ui->m_tree->invisibleRootItem()->takeChildren())
|
||||
{
|
||||
QString txt = qtwi->text(1);
|
||||
@@ -399,7 +407,25 @@ void ElementPropertiesEditorWidget::on_m_buttonBox_accepted()
|
||||
m_data.m_informations.addValue(qtwi->data(0, Qt::UserRole).toString(),
|
||||
txt);
|
||||
}
|
||||
|
||||
|
||||
if (m_data.m_type == ElementData::Terminal)
|
||||
{
|
||||
if (ui->m_auto_num_locked_cb->isChecked())
|
||||
m_data.m_informations.addValue(QStringLiteral("auto_num_locked"), QStringLiteral("true"));
|
||||
else
|
||||
m_data.m_informations.remove(QStringLiteral("auto_num_locked"));
|
||||
|
||||
if (ui->m_potential_isolating_cb->isChecked())
|
||||
m_data.m_informations.addValue(QStringLiteral("potential_isolating"), QStringLiteral("true"));
|
||||
else
|
||||
m_data.m_informations.remove(QStringLiteral("potential_isolating"));
|
||||
}
|
||||
|
||||
if (ui->m_exclude_from_bom_cb->isChecked())
|
||||
m_data.m_informations.addValue(QStringLiteral("exclude_from_bom"), QStringLiteral("true"));
|
||||
else
|
||||
m_data.m_informations.remove(QStringLiteral("exclude_from_bom"));
|
||||
|
||||
this->close();
|
||||
}
|
||||
|
||||
@@ -423,9 +449,16 @@ void ElementPropertiesEditorWidget::on_m_base_type_cb_currentIndexChanged(int in
|
||||
ui->m_master_gb->setVisible(master);
|
||||
ui->m_terminal_gb->setVisible(terminal);
|
||||
|
||||
//Every base type whose tree updateTree() enables and whose data
|
||||
//ElementScene::toXml() writes. These three checks were never
|
||||
//reconciled, which is how Terminal and Thumbnail ended up with a
|
||||
//working tree and write path behind a hidden tab.
|
||||
ui->tabWidget->setTabVisible(1,
|
||||
(type_ == ElementData::Simple ||
|
||||
type_ == ElementData::Master));
|
||||
type_ == ElementData::Master ||
|
||||
type_ == ElementData::Slave ||
|
||||
type_ == ElementData::Terminal ||
|
||||
type_ == ElementData::Thumbnail));
|
||||
|
||||
updateTree();
|
||||
}
|
||||
|
||||
@@ -54,6 +54,16 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_exclude_from_bom_cb">
|
||||
<property name="styleSheet">
|
||||
<string>margin: 5px; font-weight: bold;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Exclure de la nomenclature</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="m_slave_gb">
|
||||
<property name="title">
|
||||
@@ -229,6 +239,26 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="m_terminal_func_cb"/>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="m_auto_num_locked_cb">
|
||||
<property name="styleSheet">
|
||||
<string>margin: 5px; font-weight: bold;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Verrouiller la numérotation automatique</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="m_potential_isolating_cb">
|
||||
<property name="styleSheet">
|
||||
<string>margin: 5px; font-weight: bold;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Séparation de potentiel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -1102,29 +1102,39 @@ void QETElementEditor::updateAction()
|
||||
{
|
||||
//Action disabled if read only
|
||||
auto ro_list = m_add_part_action_grp->actions();
|
||||
ro_list << ui->m_select_all_act
|
||||
<< ui->m_revert_selection_action
|
||||
<< ui->m_paste_from_file_action
|
||||
ro_list << ui->m_paste_from_file_action
|
||||
<< ui->m_paste_from_element_action;
|
||||
for (auto action : std::as_const(ro_list)) {
|
||||
action->setDisabled(m_read_only);
|
||||
}
|
||||
|
||||
//Changing what is selected does not change the element, so these stay
|
||||
//available when it is read only -- otherwise there is no way to pick
|
||||
//out the primitive you want to copy out of it.
|
||||
ui->m_select_all_act->setEnabled(true);
|
||||
ui->m_revert_selection_action->setEnabled(true);
|
||||
|
||||
//Action enabled if a primitive is selected
|
||||
auto select_list = m_depth_action_group->actions();
|
||||
select_list << ui->m_deselect_all_action
|
||||
<< ui->m_cut_action
|
||||
<< ui->m_copy_action
|
||||
select_list << ui->m_cut_action
|
||||
<< ui->m_delete_action
|
||||
<< ui->m_rotate_action
|
||||
<< ui->m_rotateFine_action
|
||||
<< ui->m_flip_action
|
||||
<< ui->m_mirror_action;
|
||||
auto items_selected = !m_read_only && m_elmt_scene->selectedItems().count();
|
||||
const bool has_selection = m_elmt_scene->selectedItems().count() > 0;
|
||||
auto items_selected = !m_read_only && has_selection;
|
||||
for (auto action : std::as_const(select_list)) {
|
||||
action->setEnabled(items_selected);
|
||||
}
|
||||
|
||||
//Copying only reads the element -- ElementScene::copy() serialises the
|
||||
//selection to the clipboard and touches nothing else -- so it is
|
||||
//allowed on a read-only element too. Cut, paste and delete above stay
|
||||
//disabled, so the element itself is still protected.
|
||||
ui->m_copy_action->setEnabled(has_selection);
|
||||
ui->m_deselect_all_action->setEnabled(has_selection);
|
||||
|
||||
//Action about clipboard
|
||||
auto clipboard_contain_elmt = !m_read_only && ElementScene::clipboardMayContainElement();
|
||||
ui->m_paste_action->setEnabled(clipboard_contain_elmt);
|
||||
|
||||
@@ -36,6 +36,33 @@
|
||||
|
||||
ElementPictureFactory* ElementPictureFactory::m_factory = nullptr;
|
||||
|
||||
/**
|
||||
@brief ElementPictureFactory::cacheKey
|
||||
@param location
|
||||
@return the key under which the drawing of the element at location is
|
||||
cached.
|
||||
|
||||
An element definition normally carries its own uuid, and that is used
|
||||
directly. Definitions saved before uuids were written do not have one --
|
||||
a good share of the shipped example projects are still in that state --
|
||||
and they all presented the same null uuid as a key. The drawing of such
|
||||
an element was therefore either rebuilt for every instance placed, or
|
||||
stored under a key it shared with every other element lacking a uuid.
|
||||
Derive a stable key from the location for those instead:
|
||||
ElementsLocation::toString() qualifies an embedded path with the id of
|
||||
the project that owns it, and project ids come from an ever-increasing
|
||||
counter and are never reused, so the derived key cannot collide with an
|
||||
element of another project.
|
||||
*/
|
||||
QUuid ElementPictureFactory::cacheKey(const ElementsLocation &location)
|
||||
{
|
||||
const QUuid uuid = location.uuid();
|
||||
if (!uuid.isNull()) {
|
||||
return uuid;
|
||||
}
|
||||
return QUuid::createUuidV5(QUuid(), location.toString().toUtf8());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementPictureFactory::getPictures
|
||||
Set the picture of the element at location.
|
||||
@@ -50,12 +77,7 @@ void ElementPictureFactory::getPictures(const ElementsLocation &location, QPictu
|
||||
return;
|
||||
}
|
||||
|
||||
QUuid uuid = location.uuid();
|
||||
if(Q_UNLIKELY(uuid.isNull()))
|
||||
{
|
||||
build(location, &picture, &low_picture);
|
||||
return;
|
||||
}
|
||||
const QUuid uuid = cacheKey(location);
|
||||
|
||||
if(m_pictures_H.contains(uuid))
|
||||
{
|
||||
@@ -80,7 +102,7 @@ void ElementPictureFactory::getPictures(const ElementsLocation &location, QPictu
|
||||
*/
|
||||
QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
|
||||
{
|
||||
QUuid uuid = location.uuid();
|
||||
const QUuid uuid = cacheKey(location);
|
||||
|
||||
if (m_pixmap_H.contains(uuid)) {
|
||||
return m_pixmap_H.value(uuid);
|
||||
@@ -114,9 +136,7 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
|
||||
painter.translate(hsx, hsy);
|
||||
painter.drawPicture(0, 0, m_pictures_H.value(uuid));
|
||||
|
||||
if (!uuid.isNull()) {
|
||||
m_pixmap_H.insert(uuid, pix);
|
||||
}
|
||||
m_pixmap_H.insert(uuid, pix);
|
||||
return pix;
|
||||
}
|
||||
|
||||
@@ -132,10 +152,11 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
|
||||
ElementPictureFactory::primitives ElementPictureFactory::getPrimitives(
|
||||
const ElementsLocation &location)
|
||||
{
|
||||
if(!m_primitives_H.contains(location.uuid()))
|
||||
const QUuid uuid = cacheKey(location);
|
||||
if(!m_primitives_H.contains(uuid))
|
||||
build(location);
|
||||
|
||||
return m_primitives_H.value(location.uuid());
|
||||
return m_primitives_H.value(uuid);
|
||||
}
|
||||
|
||||
ElementPictureFactory::~ElementPictureFactory()
|
||||
@@ -270,7 +291,7 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
|
||||
painter.end();
|
||||
low_painter.end();
|
||||
|
||||
const auto uuid_ = location.uuid();
|
||||
const auto uuid_ = cacheKey(location);
|
||||
if (!picture) {
|
||||
m_pictures_H.insert(uuid_, pic);
|
||||
m_primitives_H.insert(uuid_, primitives_);
|
||||
@@ -621,7 +642,11 @@ void ElementPictureFactory::setPainterStyle(const QDomElement &dom, QPainter &pa
|
||||
//Get the couples style/value
|
||||
const QStringList styles = dom.attribute("style").split(";", Qt::SkipEmptyParts);
|
||||
|
||||
QRegularExpression rx("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$");
|
||||
//Built once : this runs for every primitive of every element
|
||||
//instance a project places, and recompiling the pattern each time
|
||||
//was the single largest cost of opening a project.
|
||||
static const QRegularExpression rx(
|
||||
QStringLiteral("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$"));
|
||||
if (!rx.isValid())
|
||||
{
|
||||
qWarning() <<QObject::tr("this is an error in the code")
|
||||
|
||||
@@ -92,6 +92,7 @@ class ElementPictureFactory
|
||||
ElementPictureFactory operator= (const ElementPictureFactory &);
|
||||
~ElementPictureFactory();
|
||||
|
||||
static QUuid cacheKey(const ElementsLocation &location);
|
||||
bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr);
|
||||
void parseElement(const QDomElement &dom, QPainter &painter, primitives &prim) const;
|
||||
void parseLine (const QDomElement &dom, QPainter &painter, primitives &prim) const;
|
||||
|
||||
@@ -159,6 +159,8 @@ void MachineInfo::send_info_to_debug()
|
||||
qInfo()<< "";
|
||||
qInfo()<< " System language defined in QET configuration:"<< QString(QETApp::langFromSetting().toLatin1());
|
||||
qInfo()<< " language Path:"<< QString(QETApp::languagesPath().toLatin1());
|
||||
qInfo()<< " Loaded QET translation:"<< QETApp::loadedQetTranslationFile();
|
||||
qInfo()<< " Loaded Qt translation:"<< QETApp::loadedQtTranslationFile();
|
||||
qInfo()<< " Common Elements Dir:"<< QString(QETApp::commonElementsDir().toLatin1());
|
||||
qInfo()<< " Common TitleBlock Templates Dir:"<< QString(QETApp::commonTitleBlockTemplatesDir().toLatin1());
|
||||
qInfo()<< " Custom Elements Dir:"<< QString(QETApp::customElementsDir().toLatin1());
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "machine_info.h"
|
||||
#include "qet.h"
|
||||
#include "qetapp.h"
|
||||
#include "qetmessagebox.h"
|
||||
#include "qetproject.h"
|
||||
#include "singleapplication.h"
|
||||
#include "utils/qetsettings.h"
|
||||
@@ -127,6 +128,11 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
||||
// runs on a background thread referencing the project and races the
|
||||
// process exit (intermittent segfault in QET::writeToFile).
|
||||
QETProject::setBackupEnabled(false);
|
||||
// Answer message boxes instead of showing them: opening a project
|
||||
// saved by an older QElectroTech raises a warning from
|
||||
// QETProject::readProjectXml(), and with nobody able to dismiss it
|
||||
// QDialog::exec() would spin its event loop forever.
|
||||
QET::QetMessageBox::setNonInteractive(true);
|
||||
return CLIExport::run(export_app.arguments());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "deviceinformation.h"
|
||||
|
||||
#include "../qetinformation.h"
|
||||
|
||||
namespace {
|
||||
struct DeviceField
|
||||
{
|
||||
QString m_key;
|
||||
QString DeviceInformation::*m_member;
|
||||
};
|
||||
|
||||
const DeviceField fields[] = {
|
||||
{QETInformation::ELMT_LABEL, &DeviceInformation::m_device_tag},
|
||||
{QETInformation::ELMT_MANUFACTURER, &DeviceInformation::m_manufacturer},
|
||||
{QETInformation::ELMT_MANUFACTURER_REF, &DeviceInformation::m_part_number},
|
||||
{QETInformation::ELMT_MODEL, &DeviceInformation::m_model},
|
||||
{QETInformation::ELMT_DESCRIPTION, &DeviceInformation::m_description},
|
||||
{QETInformation::ELMT_CATEGORY, &DeviceInformation::m_category},
|
||||
{QETInformation::ELMT_QUANTITY, &DeviceInformation::m_quantity},
|
||||
{QETInformation::ELMT_VOLTAGE_RATING, &DeviceInformation::m_voltage_rating},
|
||||
{QETInformation::ELMT_CURRENT_RATING, &DeviceInformation::m_current_rating},
|
||||
{QETInformation::ELMT_NOTES, &DeviceInformation::m_notes}
|
||||
};
|
||||
}
|
||||
|
||||
DeviceInformation DeviceInformation::fromContext(const DiagramContext &context)
|
||||
{
|
||||
DeviceInformation info;
|
||||
for (const auto &field : fields) {
|
||||
info.*(field.m_member) = context.value(field.m_key).toString();
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
DiagramContext DeviceInformation::toContext(DiagramContext context) const
|
||||
{
|
||||
for (const auto &field : fields)
|
||||
{
|
||||
const auto &value = this->*(field.m_member);
|
||||
if (value.isEmpty()) {
|
||||
context.remove(field.m_key);
|
||||
} else {
|
||||
const bool show = !context.contains(field.m_key) || context.keyMustShow(field.m_key);
|
||||
context.addValue(field.m_key, value, show);
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef DEVICEINFORMATION_H
|
||||
#define DEVICEINFORMATION_H
|
||||
|
||||
#include "../diagramcontext.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
/**
|
||||
* Device properties backed by ElementData::m_informations, not a second store.
|
||||
* Missing properties remain empty. Quantity and ratings retain the existing
|
||||
* free-text representation (including decimal quantities and units).
|
||||
*/
|
||||
struct DeviceInformation
|
||||
{
|
||||
QString m_device_tag;
|
||||
QString m_manufacturer;
|
||||
QString m_part_number;
|
||||
QString m_model;
|
||||
QString m_description;
|
||||
QString m_category;
|
||||
QString m_quantity;
|
||||
QString m_voltage_rating;
|
||||
QString m_current_rating;
|
||||
QString m_notes;
|
||||
|
||||
static DeviceInformation fromContext(const DiagramContext &context);
|
||||
DiagramContext toContext(DiagramContext context = DiagramContext()) const;
|
||||
};
|
||||
|
||||
#endif // DEVICEINFORMATION_H
|
||||
@@ -36,6 +36,7 @@ XRefProperties::XRefProperties()
|
||||
m_master_label = "%f-%l%c";
|
||||
m_slave_label = "(%f-%l%c)";
|
||||
m_offset = 0;
|
||||
m_slave_offset = 0;
|
||||
m_xref_pos = Qt::AlignBottom;
|
||||
}
|
||||
|
||||
@@ -56,6 +57,8 @@ void XRefProperties::toSettings(QSettings &settings,
|
||||
settings.setValue(prefix % "snapto", snap);
|
||||
int offset = m_offset;
|
||||
settings.setValue(prefix % "offset", offset);
|
||||
int slave_offset = m_slave_offset;
|
||||
settings.setValue(prefix % "slave_offset", slave_offset);
|
||||
QString master_label = m_master_label;
|
||||
settings.setValue(prefix % "master_label", master_label);
|
||||
QString slave_label = m_slave_label;
|
||||
@@ -86,6 +89,7 @@ void XRefProperties::fromSettings(const QSettings &settings,
|
||||
QString snap = settings.value(prefix % "snapto", "label").toString();
|
||||
snap == "bottom"? m_snap_to = Bottom : m_snap_to = Label;
|
||||
m_offset = settings.value(prefix % "offset", "0").toInt();
|
||||
m_slave_offset = settings.value(prefix % "slave_offset", "0").toInt();
|
||||
m_master_label = settings.value(prefix % "master_label", "%f-%l%c").toString();
|
||||
m_slave_label = settings.value(prefix % "slave_label", "(%f-%l%c)").toString();
|
||||
|
||||
@@ -123,6 +127,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const
|
||||
|
||||
int offset = m_offset;
|
||||
xml_element.setAttribute("offset", QString::number(offset));
|
||||
xml_element.setAttribute("slave_offset", QString::number(m_slave_offset));
|
||||
QString master_label = m_master_label;
|
||||
xml_element.setAttribute("master_label", master_label);
|
||||
QString slave_label = m_slave_label;
|
||||
@@ -157,6 +162,7 @@ bool XRefProperties::fromXml(const QDomElement &xml_element) {
|
||||
m_xref_pos = Qt::AlignBottom;
|
||||
|
||||
m_offset = xml_element.attribute("offset", "0").toInt();
|
||||
m_slave_offset = xml_element.attribute("slave_offset", "0").toInt();
|
||||
m_master_label = xml_element.attribute("master_label", "%f-%l%c");
|
||||
m_slave_label = xml_element.attribute("slave_label","(%f-%l%c)");
|
||||
foreach (QString key, m_prefix_keys) {
|
||||
@@ -199,6 +205,7 @@ bool XRefProperties::operator ==(const XRefProperties &xrp) const{
|
||||
&& m_prefix == xrp.m_prefix
|
||||
&& m_master_label== xrp.m_master_label
|
||||
&& m_offset == xrp.m_offset
|
||||
&& m_slave_offset== xrp.m_slave_offset
|
||||
&& m_xref_pos == xrp.m_xref_pos
|
||||
&& m_slave_label == xrp.m_slave_label);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ class XRefProperties : public PropertiesInterface
|
||||
void setOffset(const int offset) {m_offset = offset;}
|
||||
int offset() const {return m_offset;}
|
||||
|
||||
void setSlaveOffset(const int offset) {m_slave_offset = offset;}
|
||||
int slaveOffset() const {return m_slave_offset;}
|
||||
|
||||
void setKey(QString& key) {m_key = key;}
|
||||
|
||||
private:
|
||||
@@ -93,6 +96,7 @@ class XRefProperties : public PropertiesInterface
|
||||
QString m_master_label;
|
||||
QString m_slave_label;
|
||||
int m_offset;
|
||||
int m_slave_offset;
|
||||
QString m_key;
|
||||
};
|
||||
|
||||
|
||||
+30
-3
@@ -197,6 +197,26 @@ QETApp *QETApp::instance()
|
||||
return m_qetapp;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETApp::loadedQetTranslationFile
|
||||
@return path of the QET .qm file actually loaded, empty if none
|
||||
(diagnostic helper for the startup log, see MachineInfo)
|
||||
*/
|
||||
QString QETApp::loadedQetTranslationFile()
|
||||
{
|
||||
return m_qetapp ? m_qetapp->qetTranslator.filePath() : QString();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETApp::loadedQtTranslationFile
|
||||
@return path of the Qt .qm file actually loaded, empty if none
|
||||
(diagnostic helper for the startup log, see MachineInfo)
|
||||
*/
|
||||
QString QETApp::loadedQtTranslationFile()
|
||||
{
|
||||
return m_qetapp ? m_qetapp->qtTranslator.filePath() : QString();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETApp::setLanguage
|
||||
Change the language used by the application.
|
||||
@@ -234,14 +254,21 @@ void QETApp::setLanguage(const QString &desired_language) {
|
||||
// desired_language may be a full locale such as "pt_BR": try that exact
|
||||
// translation, then the base language ("pt"), then fall back to English.
|
||||
// French is the application's source language and needs no translation.
|
||||
// A .qm compiled from an untranslated .ts (0% done) loads "successfully"
|
||||
// but is empty: treat it as missing, so the user falls back to English
|
||||
// instead of silently getting the French source strings.
|
||||
const QString base_language = desired_language.section('_', 0, 0);
|
||||
bool loaded = qetTranslator.load("qet_" + desired_language, languages_path);
|
||||
auto loadQet = [this, &languages_path](const QString &name) {
|
||||
return qetTranslator.load(name, languages_path)
|
||||
&& !qetTranslator.isEmpty();
|
||||
};
|
||||
bool loaded = loadQet("qet_" + desired_language);
|
||||
if (!loaded && base_language != desired_language)
|
||||
loaded = qetTranslator.load("qet_" + base_language, languages_path);
|
||||
loaded = loadQet("qet_" + base_language);
|
||||
if (!loaded && base_language != "fr") {
|
||||
// use of the English version by default
|
||||
// utilisation de la version anglaise par defaut
|
||||
if(!qetTranslator.load("qet_en", languages_path))
|
||||
if(!loadQet("qet_en"))
|
||||
qWarning() << "failed to load"
|
||||
<< "qet_en" << languages_path << "(" << __FILE__
|
||||
<< __LINE__ << __FUNCTION__ << ")";
|
||||
|
||||
@@ -102,6 +102,8 @@ class QETApp : public QObject
|
||||
static QString documentDir();
|
||||
static QString pictureDir();
|
||||
static QString languagesPath();
|
||||
static QString loadedQetTranslationFile();
|
||||
static QString loadedQtTranslationFile();
|
||||
static QString realPath(const QString &);
|
||||
static QString symbolicPath(const QString &);
|
||||
static QStringList handledFileExtensions();
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
#include "ui/diagrameditorhandlersizewidget.h"
|
||||
#include "TerminalStrip/ui/addterminalstripitemdialog.h"
|
||||
#include "wiringlistexport.h"
|
||||
#include "ui/wiringlistdialog.h"
|
||||
#include "ui/terminalnumberingdialog.h"
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
@@ -519,6 +520,17 @@ void QETDiagramEditor::setUpActions()
|
||||
}
|
||||
});
|
||||
|
||||
// Show the wiring list read from the project database
|
||||
m_project_wiring_list_view = new QAction(QET::Icons::DocumentSpreadsheet, tr("Liste de câblage (base de données)"), this);
|
||||
connect(m_project_wiring_list_view, &QAction::triggered, [this]() {
|
||||
QETProject *project = this->currentProject();
|
||||
if (project)
|
||||
{
|
||||
WiringListDialog dialog(project, this);
|
||||
dialog.exec();
|
||||
}
|
||||
});
|
||||
|
||||
// Terminal Numbering
|
||||
m_terminal_numbering = new QAction(QET::Icons::TerminalStrip, tr("Numérotation automatique des bornes"), this);
|
||||
connect(m_terminal_numbering, &QAction::triggered, this, &QETDiagramEditor::slot_terminalNumbering);
|
||||
@@ -940,6 +952,7 @@ void QETDiagramEditor::setUpMenu()
|
||||
menu_project -> addAction(m_terminal_strip_dialog);
|
||||
menu_project -> addAction(m_project_terminalBloc);
|
||||
menu_project -> addAction(m_project_export_wiring_list);
|
||||
menu_project -> addAction(m_project_wiring_list_view);
|
||||
menu_project -> addAction(m_terminal_numbering);
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
menu_project -> addSeparator();
|
||||
@@ -1789,6 +1802,7 @@ void QETDiagramEditor::slot_updateActions()
|
||||
m_project_export_conductor_num-> setEnabled(opened_project);
|
||||
m_terminal_strip_dialog -> setEnabled(editable_project);
|
||||
m_project_export_wiring_list -> setEnabled(opened_project);
|
||||
m_project_wiring_list_view -> setEnabled(opened_project);
|
||||
m_terminal_numbering -> setEnabled(editable_project);
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
m_export_project_db -> setEnabled(editable_project);
|
||||
@@ -2177,6 +2191,7 @@ void QETDiagramEditor::openBackupFiles(QList<KAutoSaveFile *> backup_files)
|
||||
}
|
||||
delete project;
|
||||
DialogWaiting::dropInstance();
|
||||
continue;
|
||||
}
|
||||
addProject(project);
|
||||
DialogWaiting::dropInstance();
|
||||
|
||||
@@ -209,6 +209,7 @@ class QETDiagramEditor : public QETMainWindow
|
||||
*m_project_terminalBloc, ///< generate terminal block
|
||||
*m_project_export_conductor_num,///<Export the wire num to csv
|
||||
*m_project_export_wiring_list, ///< Action to export the wiring list
|
||||
*m_project_wiring_list_view, ///< Action to show the wiring list read from the project database
|
||||
*m_terminal_numbering, ///< Action to launch terminal numbering
|
||||
*m_export_project_db, ///Export to file the internal database of the current project
|
||||
*m_tile_window, ///< Show MDI subwindows as tile
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
#include "projectdbmodel.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "../../dataBase/projectdatabase.h"
|
||||
#include "../../qetapp.h"
|
||||
#include "../../qetinformation.h"
|
||||
@@ -266,7 +268,13 @@ QDomElement ProjectDBModel::toXml(QDomDocument &document) const
|
||||
//We save all data except the display role, because he was generated in the fly
|
||||
auto list = m_header_data.value(key).keys();
|
||||
list.removeAll(Qt::DisplayRole);
|
||||
|
||||
//Sorted: m_header_data's inner container is a QHash too, so its
|
||||
//key order is randomised per process. modelHeaderDataToXml()
|
||||
//writes the roles of a section in the order given here, so an
|
||||
//unsorted list reordered the <data> children of that section on
|
||||
//every save and kept the save irreproducible.
|
||||
std::sort(list.begin(), list.end());
|
||||
|
||||
horizontal_.insert(key, list);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "../../createdxf.h"
|
||||
#include "../../diagram.h"
|
||||
#include "../../elementprovider.h"
|
||||
#include "../../qetmessagebox.h"
|
||||
#include "../../qetxml.h"
|
||||
#include "../../utils/qetutils.h"
|
||||
#include "projectdbmodel.h"
|
||||
@@ -119,7 +120,7 @@ void QetGraphicsTableItem::checkInsufficientRowsCount(
|
||||
text = tr("Les information à afficher sont supérieurs à la quantité maximal pouvant être affiché par le tableau.\n"
|
||||
"Veuillez ajouter un nouveau tableau ou regler le tableau existant afin d'afficher l'integralité des informations.");
|
||||
}
|
||||
QMessageBox::information(parent, tr("Limitation de tableau"), text);
|
||||
QET::QetMessageBox::information(parent, tr("Limitation de tableau"), text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <QTimer>
|
||||
#include <QDomDocument>
|
||||
#include <QDomElement>
|
||||
#include <QtCore/qnumeric.h>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
|
||||
/**
|
||||
@@ -221,8 +222,16 @@ void DynamicElementTextItem::fromXml(const QDomElement &dom_elmt)
|
||||
//Force the update of the displayed text
|
||||
setTextFrom(m_text_from);
|
||||
|
||||
QGraphicsTextItem::setPos(dom_elmt.attribute("x", QString::number(0)).toDouble(),
|
||||
dom_elmt.attribute("y", QString::number(0)).toDouble());
|
||||
//QString::toDouble() accepts "nan"/"inf"/"-inf" as a successful
|
||||
//conversion, so a corrupted or hand-edited file can hand this a
|
||||
//non-finite position -- fall back to 0 the same way a missing
|
||||
//attribute already does, rather than letting it reach the scene
|
||||
//(see the matching comment in Element::valideXml(), the fromXml()
|
||||
//gate a plain element's position goes through; this text item has
|
||||
//no such gate to reject the whole item at, so it clamps instead).
|
||||
double x = dom_elmt.attribute("x", QString::number(0)).toDouble();
|
||||
double y = dom_elmt.attribute("y", QString::number(0)).toDouble();
|
||||
QGraphicsTextItem::setPos(qIsFinite(x) ? x : 0, qIsFinite(y) ? y : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -576,7 +585,7 @@ void DynamicElementTextItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
setDefaultTextColor(m_user_color);
|
||||
m_user_color = QColor(); //m_user_color is now invalid
|
||||
if(m_slave_Xref_item)
|
||||
m_slave_Xref_item->setDefaultTextColor(Qt::black);
|
||||
m_slave_Xref_item->setDefaultTextColor(color());
|
||||
}
|
||||
|
||||
// Shift or no parent initiates movement of dynamic text, otherwise movement of parent element
|
||||
@@ -815,7 +824,7 @@ bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *ev
|
||||
return true;
|
||||
}
|
||||
else if(event->type() == QEvent::GraphicsSceneHoverLeave) {
|
||||
m_slave_Xref_item->setDefaultTextColor(Qt::black);
|
||||
m_slave_Xref_item->setDefaultTextColor(color());
|
||||
return true;
|
||||
}
|
||||
else if(event->type() == QEvent::GraphicsSceneMouseDoubleClick) {
|
||||
@@ -1382,36 +1391,72 @@ void DynamicElementTextItem::updateXref()
|
||||
!m_parent_element.data()->linkedElements().isEmpty())
|
||||
{
|
||||
Element *master_elmt = m_parent_element.data()->linkedElements().first();
|
||||
if(master_elmt && !parentGroup() &&
|
||||
(
|
||||
if(master_elmt && !parentGroup())
|
||||
{
|
||||
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
|
||||
|
||||
//Champ de texte: store xref in element informations
|
||||
if(xrp.getXrefPos() == Qt::AlignHCenter)
|
||||
{
|
||||
if(m_text_from == DynamicElementTextItem::ElementInfo && m_info_name == "xref")
|
||||
{
|
||||
QString xref_label = xrp.slaveLabel();
|
||||
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
|
||||
|
||||
DiagramContext dc = m_parent_element->elementInformations();
|
||||
if(dc.value("xref").toString() != xref_label)
|
||||
{
|
||||
dc.addValue("xref", xref_label);
|
||||
m_parent_element->setElementInformations(dc);
|
||||
}
|
||||
|
||||
//Set up connections for future updates
|
||||
if(m_update_slave_Xref_connection.isEmpty())
|
||||
{
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram(), &Diagram::diagramInformationChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::projectDiagramsOrderChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::diagramRemoved, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref);
|
||||
}
|
||||
return;
|
||||
}
|
||||
//For label/composite text: fall through to cleanup (delete m_slave_Xref_item)
|
||||
}
|
||||
else if(
|
||||
(m_text_from == DynamicElementTextItem::ElementInfo && m_info_name == "label") ||
|
||||
(m_text_from == DynamicElementTextItem::CompositeText && m_composite_text.contains("%{label}"))
|
||||
)
|
||||
)
|
||||
{
|
||||
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
|
||||
QString xref_label = xrp.slaveLabel();
|
||||
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
|
||||
|
||||
if(!m_slave_Xref_item)
|
||||
{
|
||||
m_slave_Xref_item = new QGraphicsTextItem(xref_label, this);
|
||||
m_slave_Xref_item->setFont(QETApp::diagramTextsFont(5));
|
||||
m_slave_Xref_item->setDefaultTextColor(Qt::black);
|
||||
m_slave_Xref_item->installSceneEventFilter(this);
|
||||
QString xref_label = xrp.slaveLabel();
|
||||
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
|
||||
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram(), &Diagram::diagramInformationChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::projectDiagramsOrderChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::diagramRemoved, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref);
|
||||
if(!m_slave_Xref_item)
|
||||
{
|
||||
m_slave_Xref_item = new QGraphicsTextItem(xref_label, this);
|
||||
m_slave_Xref_item->setFont(QETApp::diagramTextsFont(5));
|
||||
// Match the parent text's user-configurable color instead of
|
||||
// hardcoding black, which renders invisible under dark themes
|
||||
// or stylesheets that do not affect this sub-item the same way
|
||||
// they affect the parent (bugtracker #247).
|
||||
m_slave_Xref_item->setDefaultTextColor(color());
|
||||
m_slave_Xref_item->installSceneEventFilter(this);
|
||||
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram(), &Diagram::diagramInformationChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::projectDiagramsOrderChanged, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::diagramRemoved, this, &DynamicElementTextItem::updateXref);
|
||||
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref);
|
||||
}
|
||||
else
|
||||
m_slave_Xref_item->setPlainText(xref_label);
|
||||
setXref_item(xrp.getXrefPos(), xrp.slaveOffset());
|
||||
return;
|
||||
}
|
||||
else
|
||||
m_slave_Xref_item->setPlainText(xref_label);
|
||||
setXref_item(xrp.getXrefPos());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1428,6 +1473,29 @@ void DynamicElementTextItem::updateXref()
|
||||
delete m_slave_Xref_item;
|
||||
m_slave_Xref_item = nullptr;
|
||||
m_update_slave_Xref_connection.clear();
|
||||
|
||||
//If position changed to Champ de texte, store xref in element info
|
||||
if(m_parent_element->linkType() == Element::Slave &&
|
||||
!m_parent_element->linkedElements().isEmpty())
|
||||
{
|
||||
Element *master_elmt = m_parent_element->linkedElements().first();
|
||||
if(master_elmt && diagram())
|
||||
{
|
||||
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
|
||||
if(xrp.getXrefPos() == Qt::AlignHCenter)
|
||||
{
|
||||
QString xref_label = xrp.slaveLabel();
|
||||
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
|
||||
|
||||
DiagramContext dc = m_parent_element->elementInformations();
|
||||
if(dc.value("xref").toString() != xref_label)
|
||||
{
|
||||
dc.addValue("xref", xref_label);
|
||||
m_parent_element->setElementInformations(dc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1476,7 +1544,7 @@ void DynamicElementTextItem::setPlainText(const QString &text)
|
||||
? nullptr : m_parent_element.data()->linkedElements().first();
|
||||
if (master_elmt) {
|
||||
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
|
||||
setXref_item(xrp.getXrefPos());
|
||||
setXref_item(xrp.getXrefPos(), xrp.slaveOffset());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1488,44 +1556,44 @@ void DynamicElementTextItem::setTextWidth(qreal width)
|
||||
emit textWidthChanged(width);
|
||||
}
|
||||
|
||||
void DynamicElementTextItem::setXref_item(Qt::AlignmentFlag m_exHrefPos)
|
||||
void DynamicElementTextItem::setXref_item(Qt::AlignmentFlag m_exHrefPos, int slave_offset)
|
||||
{
|
||||
QRectF r = boundingRect();
|
||||
QPointF pos;
|
||||
//QPointF pos(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top());
|
||||
if (m_exHrefPos == Qt::AlignBottom)
|
||||
{
|
||||
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom());
|
||||
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom() + slave_offset);
|
||||
}
|
||||
else if (m_exHrefPos == Qt::AlignTop)
|
||||
{
|
||||
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height());
|
||||
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height() - slave_offset);
|
||||
}
|
||||
else if (m_exHrefPos == Qt::AlignLeft) //
|
||||
{
|
||||
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width(),r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
|
||||
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width() - slave_offset,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
|
||||
}
|
||||
else if (m_exHrefPos == Qt::AlignRight) //
|
||||
{
|
||||
pos = QPointF(r.right() ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
|
||||
pos = QPointF(r.right() + slave_offset ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
|
||||
}
|
||||
else if (m_exHrefPos == Qt::AlignBaseline) //
|
||||
{
|
||||
if(this->alignment() &Qt::AlignBottom)
|
||||
{
|
||||
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom());
|
||||
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom() + slave_offset);
|
||||
}
|
||||
else if(this->alignment() &Qt::AlignTop)
|
||||
{
|
||||
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height());
|
||||
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height() - slave_offset);
|
||||
}
|
||||
else if(this->alignment() &Qt::AlignLeft)
|
||||
{
|
||||
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width(),r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
|
||||
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width() - slave_offset,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
|
||||
}
|
||||
else if(this->alignment() &Qt::AlignRight)
|
||||
{
|
||||
pos = QPointF(r.right() ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
|
||||
pos = QPointF(r.right() + slave_offset ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
|
||||
}
|
||||
}
|
||||
m_slave_Xref_item->setPos(pos);
|
||||
|
||||
@@ -112,7 +112,7 @@ class DynamicElementTextItem : public DiagramTextItem
|
||||
void updateXref();
|
||||
void setPlainText(const QString &text);
|
||||
void setTextWidth(qreal width);
|
||||
void setXref_item(Qt::AlignmentFlag m_exHrefPos);
|
||||
void setXref_item(Qt::AlignmentFlag m_exHrefPos, int slave_offset = 0);
|
||||
|
||||
void setKeepVisualRotation(bool set);
|
||||
bool keepVisualRotation() const;
|
||||
|
||||
@@ -53,6 +53,7 @@ static const QString plcTerminalKeys[] = {
|
||||
#include <QDebug>
|
||||
|
||||
#include <QDomElement>
|
||||
#include <QtCore/qnumeric.h>
|
||||
#include <utility>
|
||||
|
||||
class ElementXmlRetroCompatibility
|
||||
@@ -699,11 +700,16 @@ bool Element::valideXml(QDomElement &e)
|
||||
}
|
||||
|
||||
bool conv_ok;
|
||||
e.attribute(QStringLiteral("x")).toDouble(&conv_ok);
|
||||
if (!conv_ok) return(false);
|
||||
//QString::toDouble() accepts "nan"/"inf"/"-inf" and reports a
|
||||
//successful conversion for them, so conv_ok alone doesn't reject a
|
||||
//non-finite coordinate. A NaN position reaching the scene can hang
|
||||
//QGraphicsScene::addItem() forever inside Qt's own polygon-clipping
|
||||
//code when an existing conductor's collision test runs against it.
|
||||
double x = e.attribute(QStringLiteral("x")).toDouble(&conv_ok);
|
||||
if (!conv_ok || !qIsFinite(x)) return(false);
|
||||
|
||||
e.attribute(QStringLiteral("y")).toDouble(&conv_ok);
|
||||
if (!conv_ok) return(false);
|
||||
double y = e.attribute(QStringLiteral("y")).toDouble(&conv_ok);
|
||||
if (!conv_ok || !qIsFinite(y)) return(false);
|
||||
|
||||
return(true);
|
||||
}
|
||||
|
||||
@@ -263,7 +263,16 @@ void ElementTextItemGroup::updateAlignment()
|
||||
if(m_Xref_item)
|
||||
m_Xref_item->autoPos();
|
||||
if(m_slave_Xref_item)
|
||||
adjustSlaveXrefPos();
|
||||
{
|
||||
int slave_offset = 0;
|
||||
Element *master_elmt = m_parent_element->linkedElements().isEmpty()
|
||||
? nullptr : m_parent_element->linkedElements().first();
|
||||
if (master_elmt && m_parent_element->diagram()) {
|
||||
XRefProperties xrp = m_parent_element->diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
|
||||
slave_offset = xrp.slaveOffset();
|
||||
}
|
||||
adjustSlaveXrefPos(slave_offset);
|
||||
}
|
||||
if(m_hold_to_bottom_of_page)
|
||||
autoPos();
|
||||
}
|
||||
@@ -786,12 +795,46 @@ void ElementTextItemGroup::updateXref()
|
||||
!m_parent_element->linkedElements().isEmpty())
|
||||
{
|
||||
Element *master_elmt = m_parent_element->linkedElements().first();
|
||||
XRefProperties xrp = project->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
|
||||
|
||||
//Champ de texte: store xref in element informations
|
||||
if(xrp.getXrefPos() == Qt::AlignHCenter)
|
||||
{
|
||||
for(DynamicElementTextItem *deti : texts())
|
||||
{
|
||||
if(deti->textFrom() == DynamicElementTextItem::ElementInfo && deti->infoName() == "xref")
|
||||
{
|
||||
QString xref_label = xrp.slaveLabel();
|
||||
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
|
||||
|
||||
DiagramContext dc = m_parent_element->elementInformations();
|
||||
if(dc.value("xref").toString() != xref_label)
|
||||
{
|
||||
dc.addValue("xref", xref_label);
|
||||
m_parent_element->setElementInformations(dc);
|
||||
}
|
||||
|
||||
//Set up connections for future updates
|
||||
if(m_update_slave_Xref_connection.isEmpty())
|
||||
{
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &ElementTextItemGroup::updateXref);
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &ElementTextItemGroup::updateXref);
|
||||
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &ElementTextItemGroup::updateXref);
|
||||
m_update_slave_Xref_connection << connect(project, &QETProject::projectDiagramsOrderChanged, this, &ElementTextItemGroup::updateXref);
|
||||
m_update_slave_Xref_connection << connect(project, &QETProject::diagramRemoved, this, &ElementTextItemGroup::updateXref);
|
||||
m_update_slave_Xref_connection << connect(project, &QETProject::XRefPropertiesChanged, this, &ElementTextItemGroup::updateXref);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
//No "xref" text found: fall through to cleanup
|
||||
}
|
||||
|
||||
for(DynamicElementTextItem *deti : texts())
|
||||
{
|
||||
if((deti->textFrom() == DynamicElementTextItem::ElementInfo && deti->infoName() == "label") ||
|
||||
(deti->textFrom() == DynamicElementTextItem::CompositeText && deti->compositeText().contains("%{label")))
|
||||
{
|
||||
XRefProperties xrp = project->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
|
||||
QString xref_label = xrp.slaveLabel();
|
||||
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
|
||||
|
||||
@@ -810,7 +853,7 @@ void ElementTextItemGroup::updateXref()
|
||||
else
|
||||
m_slave_Xref_item->setPlainText(xref_label);
|
||||
|
||||
adjustSlaveXrefPos();
|
||||
adjustSlaveXrefPos(xrp.slaveOffset());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -830,14 +873,38 @@ void ElementTextItemGroup::updateXref()
|
||||
delete m_slave_Xref_item;
|
||||
m_slave_Xref_item = nullptr;
|
||||
m_update_slave_Xref_connection.clear();
|
||||
|
||||
//If position changed to Champ de texte, store xref in element info
|
||||
if(m_parent_element->linkType() == Element::Slave &&
|
||||
!m_parent_element->linkedElements().isEmpty() &&
|
||||
m_parent_element->diagram())
|
||||
{
|
||||
Element *master_elmt = m_parent_element->linkedElements().first();
|
||||
if(master_elmt)
|
||||
{
|
||||
XRefProperties xrp = m_parent_element->diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
|
||||
if(xrp.getXrefPos() == Qt::AlignHCenter)
|
||||
{
|
||||
QString xref_label = xrp.slaveLabel();
|
||||
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
|
||||
|
||||
DiagramContext dc = m_parent_element->elementInformations();
|
||||
if(dc.value("xref").toString() != xref_label)
|
||||
{
|
||||
dc.addValue("xref", xref_label);
|
||||
m_parent_element->setElementInformations(dc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ElementTextItemGroup::adjustSlaveXrefPos()
|
||||
void ElementTextItemGroup::adjustSlaveXrefPos(int slave_offset)
|
||||
{
|
||||
QRectF r = boundingRect();
|
||||
QPointF pos(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,
|
||||
r.bottom());
|
||||
r.bottom() + slave_offset);
|
||||
m_slave_Xref_item->setPos(pos);
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ class ElementTextItemGroup : public QObject, public QGraphicsItemGroup
|
||||
|
||||
private:
|
||||
void updateXref();
|
||||
void adjustSlaveXrefPos();
|
||||
void adjustSlaveXrefPos(int slave_offset = 0);
|
||||
void autoPos();
|
||||
|
||||
private:
|
||||
|
||||
@@ -227,12 +227,95 @@ void MasterElement::aboutDeleteXref()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterElement::contactUsage
|
||||
* Count the slave contacts currently linked to this master, by type.
|
||||
* This is the single place where that count is worked out: the cross ref
|
||||
* item, the properties dialog and the link widgets all read it from here,
|
||||
* so they cannot disagree with each other.
|
||||
* @return the per type usage
|
||||
*/
|
||||
namespace {
|
||||
|
||||
/**
|
||||
Map the element data's contact type onto the tally's own, so that
|
||||
the used count and the declared capacity cannot classify the same
|
||||
contact type differently.
|
||||
*/
|
||||
ContactUsage::Type contactType(ElementData::SlaveState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case ElementData::NO: return ContactUsage::NO;
|
||||
case ElementData::NC: return ContactUsage::NC;
|
||||
case ElementData::SW: return ContactUsage::SW;
|
||||
case ElementData::Other: break;
|
||||
}
|
||||
|
||||
return ContactUsage::Other;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ContactUsage MasterElement::contactUsage() const
|
||||
{
|
||||
ContactUsage usage;
|
||||
|
||||
for (Element *elmt : connected_elements)
|
||||
{
|
||||
if (!elmt) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ElementData &data = elmt->elementData();
|
||||
usage.addSlave(contactType(data.m_slave_state), data.m_contact_count);
|
||||
}
|
||||
|
||||
return usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterElement::contactCapacity
|
||||
* The contacts this master declares it provides, by type, summed over its
|
||||
* contact groups. A group stands for contactCount contacts of its type.
|
||||
* Returns an empty tally when the element declares no groups, which is the
|
||||
* case for every element in the standard collection today -- callers use
|
||||
* that to decide whether a capacity is worth showing at all.
|
||||
* @return the per type capacity
|
||||
*/
|
||||
ContactUsage MasterElement::contactCapacity() const
|
||||
{
|
||||
ContactUsage capacity;
|
||||
|
||||
for (const auto &group : m_data.m_slave_contact_groups) {
|
||||
capacity.addSlave(contactType(group.type), group.contactCount);
|
||||
}
|
||||
|
||||
return capacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterElement::isFull
|
||||
* @return true if the master has reached its maximum number of slaves
|
||||
*/
|
||||
bool MasterElement::isFull() const
|
||||
{
|
||||
//When the element declares contact groups, those groups are the
|
||||
//slots: a slave occupies exactly one, and ContactGroupSelectionDialog
|
||||
//offers exactly these. So the group count is the limit, and it is the
|
||||
//one the user can actually see.
|
||||
//
|
||||
//max_slaves is the fallback for elements which declare no groups. The
|
||||
//element editor keeps the two in step -- max_slaves sizes the group
|
||||
//table -- but nothing reconciles them on load, so a hand written or
|
||||
//generated file can carry five groups and max_slaves=2. Taking
|
||||
//max_slaves there capped linking at two while the dialog still
|
||||
//offered all five, which the user could only read as the dialog
|
||||
//being broken.
|
||||
if (!m_data.m_slave_contact_groups.isEmpty()) {
|
||||
return connected_elements.size() >= m_data.m_slave_contact_groups.size();
|
||||
}
|
||||
|
||||
// Set default value to -1 (unlimited slaves)
|
||||
int max_slaves = -1;
|
||||
QVariant max_slaves_variant = kindInformations().value("max_slaves");
|
||||
@@ -247,7 +330,10 @@ bool MasterElement::isFull() const
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return true if current connected elements reached or exceeded the limit
|
||||
// max_slaves is a number of slots, not of contacts: it sizes the
|
||||
// element's contact group table, and a slave occupies exactly one
|
||||
// group however many contacts that group stands for. So the slots
|
||||
// in use are the linked elements, not the contacts they carry.
|
||||
return connected_elements.size() >= max_slaves;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#define MASTERELEMENT_H
|
||||
|
||||
#include "element.h"
|
||||
#include "../contactusage.h"
|
||||
#include <QHash>
|
||||
#include <QMetaObject>
|
||||
|
||||
@@ -47,6 +48,8 @@ class MasterElement : public Element
|
||||
void initLink (QETProject *project) override;
|
||||
QRectF XrefBoundingRect() const;
|
||||
|
||||
ContactUsage contactUsage() const;
|
||||
ContactUsage contactCapacity() const;
|
||||
bool isFull() const; // Check Slave-Limit
|
||||
|
||||
protected:
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "conductortextitem.h"
|
||||
|
||||
#include <QtCore/qnumeric.h>
|
||||
#include <utility>
|
||||
|
||||
QColor Terminal::neutralColor = QColor(Qt::blue);
|
||||
@@ -741,13 +742,17 @@ bool Terminal::valideXml(QDomElement &terminal)
|
||||
if (!terminal.hasAttribute("orientation")) return(false);
|
||||
|
||||
bool conv_ok;
|
||||
//QString::toDouble() accepts "nan"/"inf"/"-inf" and reports a
|
||||
//successful conversion for them, so conv_ok alone doesn't reject a
|
||||
//non-finite coordinate -- see the matching comment in
|
||||
//Element::valideXml().
|
||||
// parse l'abscisse
|
||||
terminal.attribute("x").toDouble(&conv_ok);
|
||||
if (!conv_ok) return(false);
|
||||
double x = terminal.attribute("x").toDouble(&conv_ok);
|
||||
if (!conv_ok || !qIsFinite(x)) return(false);
|
||||
|
||||
// parse l'ordonnee
|
||||
terminal.attribute("y").toDouble(&conv_ok);
|
||||
if (!conv_ok) return(false);
|
||||
double y = terminal.attribute("y").toDouble(&conv_ok);
|
||||
if (!conv_ok || !qIsFinite(y)) return(false);
|
||||
|
||||
// parse l'id
|
||||
terminal.attribute("id").toInt(&conv_ok);
|
||||
|
||||
@@ -150,6 +150,11 @@ QStringList QETInformation::elementInfoKeys()
|
||||
ELMT_DESIGNATION,
|
||||
ELMT_MANUFACTURER,
|
||||
ELMT_MANUFACTURER_REF,
|
||||
ELMT_MODEL,
|
||||
ELMT_CATEGORY,
|
||||
ELMT_VOLTAGE_RATING,
|
||||
ELMT_CURRENT_RATING,
|
||||
ELMT_NOTES,
|
||||
ELMT_MACHINE_MANUFACTURER_REF,
|
||||
ELMT_SUPPLIER,
|
||||
ELMT_QUANTITY,
|
||||
@@ -194,8 +199,9 @@ QStringList QETInformation::elementInfoKeys()
|
||||
ELMT_PLC_ADDRESS,
|
||||
ELMT_PLC_FUNCTION,
|
||||
ELMT_PLC_COMMENT,
|
||||
ELMT_PLC_CROSSREF,
|
||||
"exclude_from_bom" };
|
||||
ELMT_PLC_CROSSREF,
|
||||
ELMT_XREF,
|
||||
"exclude_from_bom" };
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -233,7 +239,12 @@ QString QETInformation::infoToVar(const QString &info)
|
||||
*/
|
||||
QString QETInformation::translatedInfoKey(const QString &info)
|
||||
{
|
||||
if (info == DIA_AUTHOR) return QObject::tr("Auteur");
|
||||
if (info == ELMT_MODEL) return QObject::tr("Modèle");
|
||||
else if (info == ELMT_CATEGORY) return QObject::tr("Catégorie");
|
||||
else if (info == ELMT_VOLTAGE_RATING) return QObject::tr("Tension nominale");
|
||||
else if (info == ELMT_CURRENT_RATING) return QObject::tr("Courant nominal");
|
||||
else if (info == ELMT_NOTES) return QObject::tr("Notes");
|
||||
else if (info == DIA_AUTHOR) return QObject::tr("Auteur");
|
||||
else if (info == DIA_DATE) return QObject::tr("Date");
|
||||
else if (info == DIA_TITLE) return QObject::tr("Titre");
|
||||
else if (info == DIA_FILENAME) return QObject::tr("Fichier");
|
||||
@@ -316,6 +327,7 @@ QString QETInformation::translatedInfoKey(const QString &info)
|
||||
else if (info == ELMT_PLC_FUNCTION) return QObject::tr("Fonction PLC");
|
||||
else if (info == ELMT_PLC_COMMENT) return QObject::tr("Commentaire PLC");
|
||||
else if (info == ELMT_PLC_CROSSREF) return QObject::tr("Réf. croisée PLC");
|
||||
else if (info == ELMT_XREF) return QObject::tr("Réf. croisée");
|
||||
else return QString();
|
||||
}
|
||||
|
||||
@@ -330,6 +342,11 @@ QStringList QETInformation::elementEditorElementInfoKeys()
|
||||
ELMT_DESIGNATION,
|
||||
ELMT_MANUFACTURER,
|
||||
ELMT_MANUFACTURER_REF,
|
||||
ELMT_MODEL,
|
||||
ELMT_CATEGORY,
|
||||
ELMT_VOLTAGE_RATING,
|
||||
ELMT_CURRENT_RATING,
|
||||
ELMT_NOTES,
|
||||
ELMT_MACHINE_MANUFACTURER_REF,
|
||||
ELMT_SUPPLIER,
|
||||
ELMT_QUANTITY,
|
||||
|
||||
@@ -44,6 +44,11 @@ namespace QETInformation
|
||||
static QString ELMT_MACHINE_MANUFACTURER_REF = "machine_manufacturer_reference";
|
||||
static QString ELMT_SUPPLIER = "supplier";
|
||||
static QString ELMT_QUANTITY = "quantity";
|
||||
static QString ELMT_MODEL = "model";
|
||||
static QString ELMT_CATEGORY = "category";
|
||||
static QString ELMT_VOLTAGE_RATING = "voltage_rating";
|
||||
static QString ELMT_CURRENT_RATING = "current_rating";
|
||||
static QString ELMT_NOTES = "notes";
|
||||
static QString ELMT_UNITY = "unity";
|
||||
static QString ELMT_PLANT = "plant";
|
||||
static QString ELMT_LOCATION = "location";
|
||||
@@ -83,6 +88,7 @@ namespace QETInformation
|
||||
static QString ELMT_SUPPLIER_AUX4 = "supplier_auxiliary4";
|
||||
static QString ELMT_QUANTITY_AUX4 = "quantity_auxiliary4";
|
||||
static QString ELMT_UNITY_AUX4 = "unity_auxiliary4";
|
||||
static QString ELMT_XREF = "xref";
|
||||
|
||||
|
||||
/** Default information related to conductor **/
|
||||
|
||||
@@ -17,6 +17,84 @@
|
||||
*/
|
||||
#include "qetmessagebox.h"
|
||||
|
||||
#include <QTextStream>
|
||||
|
||||
namespace {
|
||||
bool g_non_interactive = false;
|
||||
|
||||
/**
|
||||
@brief autoAnswer
|
||||
Report a message box on stderr and pick an answer, for use when there
|
||||
is no user to click anything. @see QET::QetMessageBox::setNonInteractive
|
||||
@param severity : short word naming the kind of box, for the log line
|
||||
@param title
|
||||
@param text
|
||||
@param buttons : the buttons the caller offered
|
||||
@param defaultButton : the caller's preferred answer, may be NoButton
|
||||
@return the button to report as pressed
|
||||
*/
|
||||
QMessageBox::StandardButton autoAnswer(
|
||||
const char *severity,
|
||||
const QString &title,
|
||||
const QString &text,
|
||||
QMessageBox::StandardButtons buttons,
|
||||
QMessageBox::StandardButton defaultButton)
|
||||
{
|
||||
//Honour the caller's own default when it named one.
|
||||
if (defaultButton != QMessageBox::NoButton
|
||||
&& (buttons & defaultButton)) {
|
||||
QTextStream(stderr) << severity << ": " << title << " -- " << text
|
||||
<< "\n(no display: answered with the caller's default button)\n";
|
||||
return defaultButton;
|
||||
}
|
||||
|
||||
//Otherwise prefer a "carry on" answer over one that cancels, so a
|
||||
//batch run completes rather than silently doing nothing.
|
||||
static const QMessageBox::StandardButton preference[] = {
|
||||
QMessageBox::Ok, QMessageBox::Open, QMessageBox::Yes,
|
||||
QMessageBox::Save, QMessageBox::Apply, QMessageBox::YesToAll,
|
||||
QMessageBox::Retry, QMessageBox::Ignore, QMessageBox::Close
|
||||
};
|
||||
for (auto candidate : preference) {
|
||||
if (buttons & candidate) {
|
||||
QTextStream(stderr) << severity << ": " << title << " -- " << text
|
||||
<< "\n(no display: continuing)\n";
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
//Nothing affirmative on offer -- fall back to whatever is set.
|
||||
for (int bit = QMessageBox::Ok; bit <= QMessageBox::RestoreDefaults; bit <<= 1) {
|
||||
auto candidate = static_cast<QMessageBox::StandardButton>(bit);
|
||||
if (buttons & candidate) {
|
||||
QTextStream(stderr) << severity << ": " << title << " -- " << text
|
||||
<< "\n(no display: answered automatically)\n";
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
QTextStream(stderr) << severity << ": " << title << " -- " << text
|
||||
<< "\n(no display: no button offered)\n";
|
||||
return QMessageBox::NoButton;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QET::QetMessageBox::setNonInteractive
|
||||
@param non_interactive
|
||||
*/
|
||||
void QET::QetMessageBox::setNonInteractive(bool non_interactive) {
|
||||
g_non_interactive = non_interactive;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QET::QetMessageBox::isNonInteractive
|
||||
@return true when message boxes are answered without a user
|
||||
*/
|
||||
bool QET::QetMessageBox::isNonInteractive() {
|
||||
return g_non_interactive;
|
||||
}
|
||||
|
||||
/**
|
||||
@see Documentation Qt pour QMessageBox::critical
|
||||
*/
|
||||
@@ -27,6 +105,9 @@ QMessageBox::StandardButton QET::QetMessageBox::critical (
|
||||
QMessageBox::StandardButtons buttons,
|
||||
QMessageBox::StandardButton defaultButton)
|
||||
{
|
||||
if (g_non_interactive) {
|
||||
return autoAnswer("Critical", title, text, buttons, defaultButton);
|
||||
}
|
||||
#ifdef Q_OS_MACOS
|
||||
QMessageBox message_box(
|
||||
QMessageBox::Critical,
|
||||
@@ -59,6 +140,9 @@ QMessageBox::StandardButton QET::QetMessageBox::information(
|
||||
QMessageBox::StandardButtons buttons,
|
||||
QMessageBox::StandardButton defaultButton)
|
||||
{
|
||||
if (g_non_interactive) {
|
||||
return autoAnswer("Information", title, text, buttons, defaultButton);
|
||||
}
|
||||
#ifdef Q_OS_MACOS
|
||||
QMessageBox message_box(
|
||||
QMessageBox::Information,
|
||||
@@ -91,6 +175,9 @@ QMessageBox::StandardButton QET::QetMessageBox::question (
|
||||
QMessageBox::StandardButtons buttons,
|
||||
QMessageBox::StandardButton defaultButton)
|
||||
{
|
||||
if (g_non_interactive) {
|
||||
return autoAnswer("Question", title, text, buttons, defaultButton);
|
||||
}
|
||||
#ifdef Q_OS_MACOS
|
||||
QMessageBox message_box(
|
||||
QMessageBox::Question,
|
||||
@@ -123,6 +210,9 @@ QMessageBox::StandardButton QET::QetMessageBox::warning (
|
||||
QMessageBox::StandardButtons buttons,
|
||||
QMessageBox::StandardButton defaultButton)
|
||||
{
|
||||
if (g_non_interactive) {
|
||||
return autoAnswer("Warning", title, text, buttons, defaultButton);
|
||||
}
|
||||
#ifdef Q_OS_MACOS
|
||||
QMessageBox message_box(
|
||||
QMessageBox::Warning,
|
||||
|
||||
@@ -27,6 +27,28 @@ namespace QET {
|
||||
Qt:Sheet flag, thus enabling a better MacOS integration.
|
||||
*/
|
||||
namespace QetMessageBox {
|
||||
/**
|
||||
Enable non-interactive mode.
|
||||
|
||||
In non-interactive mode the functions below never construct a
|
||||
dialog. They write the message to stderr and return an answer
|
||||
immediately, so a headless run cannot block on a modal box that
|
||||
nobody is there to dismiss.
|
||||
|
||||
This is needed because these are reachable from the command-line
|
||||
tools: opening a project written by an older QElectroTech raises
|
||||
a warning from QETProject::readProjectXml(), and with no display
|
||||
to click it, QDialog::exec() spins its event loop forever.
|
||||
|
||||
The answer is chosen as: the caller's defaultButton when it gave
|
||||
one, otherwise the first "carry on" button among those offered
|
||||
(Ok, Open, Yes, Save, Apply...), otherwise the first button set.
|
||||
So the two warnings above resolve to Open and the project loads,
|
||||
which is what a batch invocation wants.
|
||||
*/
|
||||
void setNonInteractive(bool non_interactive);
|
||||
bool isNonInteractive();
|
||||
|
||||
QMessageBox::StandardButton critical (
|
||||
QWidget *,
|
||||
const QString &,
|
||||
|
||||
+40
-6
@@ -160,6 +160,11 @@ QETProject::~QETProject()
|
||||
//We block database signal to avoid hundreds of unnecessary emitted signal
|
||||
//due to deletion (diagram, item, etc...) and as much update made in the not yet deleted things.
|
||||
m_data_base.blockSignals(true);
|
||||
//Same reasoning for the rebuild itself : destroying a table relinks
|
||||
//the tables that were chained to it, which re-queries the database,
|
||||
//which rebuilds it completely -- for a project that is on its way out.
|
||||
//Nothing can observe the result : the database is destroyed with it.
|
||||
m_data_base.setUpdateBlocked(true);
|
||||
|
||||
//Each time a diagram is deleted we also remove it from m_diagram_list
|
||||
//because a lot of thing append during the destructor of a diagram class
|
||||
@@ -1038,7 +1043,14 @@ QDomDocument QETProject::toXml()
|
||||
// titleblock templates, if any
|
||||
if (m_titleblocks_collection.templates().count()) {
|
||||
QDomElement titleblocktemplates_elmt = xml_doc.createElement("titleblocktemplates");
|
||||
foreach (QString template_name, m_titleblocks_collection.templates()) {
|
||||
//Sorted, because templates() returns QHash::keys() and Qt
|
||||
//randomises hash order per process. Writing them unsorted put the
|
||||
//<titleblocktemplate> children in a different order on every save,
|
||||
//which is what made a project holding more than one template save
|
||||
//irreproducibly.
|
||||
QStringList template_names = m_titleblocks_collection.templates();
|
||||
template_names.sort();
|
||||
for (const QString &template_name : std::as_const(template_names)) {
|
||||
QDomElement e = m_titleblocks_collection.getTemplateXmlDescription(template_name);
|
||||
titleblocktemplates_elmt.appendChild(xml_doc.importNode(e, true));
|
||||
}
|
||||
@@ -1252,7 +1264,7 @@ ElementsLocation QETProject::importElement(ElementsLocation &location)
|
||||
// Warn if the new element introduces slave contact groups
|
||||
QDomElement new_kind = location.xml().firstChildElement("kindInformations");
|
||||
if (!new_kind.firstChildElement("slaveContactGroups").isNull()) {
|
||||
QMessageBox::StandardButton answer = QMessageBox::warning(nullptr,
|
||||
QMessageBox::StandardButton answer = QET::QetMessageBox::warning(nullptr,
|
||||
tr("Système de contacts modifié"),
|
||||
tr("Le nouvel élément définit des groupes de contacts esclaves.\n"
|
||||
"Les éléments esclaves existants ne seront pas automatiquement "
|
||||
@@ -1547,6 +1559,11 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
||||
}
|
||||
|
||||
m_data_base.blockSignals(true);
|
||||
//Blocking the signals is not enough : every table model built below
|
||||
//re-queries the database, and each of those queries used to trigger a
|
||||
//complete rebuild of it. The content being loaded is the same for all
|
||||
//of them, so a single rebuild once everything is in place is enough.
|
||||
m_data_base.setUpdateBlocked(true);
|
||||
|
||||
//Load the project-wide properties
|
||||
readProjectPropertiesXml(xml_project);
|
||||
@@ -1586,6 +1603,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
||||
const qint64 refresh_ms = phase_timer.restart();
|
||||
|
||||
m_data_base.blockSignals(false);
|
||||
m_data_base.setUpdateBlocked(false);
|
||||
m_data_base.updateDB();
|
||||
const qint64 database_ms = phase_timer.elapsed();
|
||||
|
||||
@@ -1899,7 +1917,14 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element)
|
||||
|
||||
// export default XRef properties
|
||||
QDomElement xrefs_elmt = xml_document.createElement("xrefs");
|
||||
for (QString key : defaultXRefProperties().keys())
|
||||
//Sorted, because defaultXRefProperties() is a QHash and its key order
|
||||
//is randomised per process. Writing it unsorted made two saves of an
|
||||
//unchanged project differ only in the order of these <xref> children,
|
||||
//so a save was not reproducible and diffing two saved files showed
|
||||
//spurious changes.
|
||||
QStringList xref_keys = defaultXRefProperties().keys();
|
||||
xref_keys.sort();
|
||||
for (QString &key : xref_keys)
|
||||
{
|
||||
auto xrp = defaultXRefProperties(key);
|
||||
xrp.setKey(key);
|
||||
@@ -1913,7 +1938,12 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element)
|
||||
conductor_autonums.setAttribute("current_autonum", m_current_conductor_autonum);
|
||||
conductor_autonums.setAttribute("freeze_new_conductors", m_freeze_new_conductors ? "true" : "false");
|
||||
conductor_autonums.setAttribute("auto_break_conductors", m_auto_break_conductor ? "true" : "false");
|
||||
foreach (QString key, conductorAutoNum().keys()) {
|
||||
//Sorted for the same reason as the xrefs above: these three
|
||||
//collections are QHash, whose key order is randomised per process,
|
||||
//so an unsorted write reorders these children on every save.
|
||||
QStringList conductor_autonum_keys = conductorAutoNum().keys();
|
||||
conductor_autonum_keys.sort();
|
||||
for (const QString &key : std::as_const(conductor_autonum_keys)) {
|
||||
QDomElement conductor_autonum = conductorAutoNum(key).toXml(xml_document, "conductor_autonum");
|
||||
if (key != "" && conductorAutoNumFormula(key) != "") {
|
||||
conductor_autonum.setAttribute("title", key);
|
||||
@@ -1925,7 +1955,9 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element)
|
||||
|
||||
//Export Folio Autonums
|
||||
QDomElement folio_autonums = xml_document.createElement("folio_autonums");
|
||||
foreach (QString key, folioAutoNum().keys()) {
|
||||
QStringList folio_autonum_keys = folioAutoNum().keys();
|
||||
folio_autonum_keys.sort();
|
||||
for (const QString &key : std::as_const(folio_autonum_keys)) {
|
||||
QDomElement folio_autonum = folioAutoNum(key).toXml(xml_document, "folio_autonum");
|
||||
folio_autonum.setAttribute("title", key);
|
||||
folio_autonums.appendChild(folio_autonum);
|
||||
@@ -1936,7 +1968,9 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element)
|
||||
QDomElement element_autonums = xml_document.createElement("element_autonums");
|
||||
element_autonums.setAttribute("current_autonum", m_current_element_autonum);
|
||||
element_autonums.setAttribute("freeze_new_elements", m_freeze_new_elements ? "true" : "false");
|
||||
foreach (QString key, elementAutoNum().keys()) {
|
||||
QStringList element_autonum_keys = elementAutoNum().keys();
|
||||
element_autonum_keys.sort();
|
||||
for (const QString &key : std::as_const(element_autonum_keys)) {
|
||||
QDomElement element_autonum = elementAutoNum(key).toXml(xml_document, "element_autonum");
|
||||
if (key != "" && elementAutoNumFormula(key) != "") {
|
||||
element_autonum.setAttribute("title", key);
|
||||
|
||||
+10
-1
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
#include "qetxml.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "NameList/nameslist.h"
|
||||
#include "utils/qetutils.h"
|
||||
|
||||
@@ -454,7 +456,14 @@ QDomElement QETXML::modelHeaderDataToXml(
|
||||
//Iterate twice, first for horizontal header and second to vertical header
|
||||
while (true)
|
||||
{
|
||||
for (auto section : data_hash.keys())
|
||||
//Sorted: data_hash is a QHash, whose key order is randomised per
|
||||
//process, so writing the sections in hash order reordered these
|
||||
//<data> children on every save and made the save irreproducible.
|
||||
//The roles within a section keep their given order, which is a
|
||||
//QList and therefore already stable.
|
||||
QList<int> sections = data_hash.keys();
|
||||
std::sort(sections.begin(), sections.end());
|
||||
for (auto section : std::as_const(sections))
|
||||
{
|
||||
for (auto role : data_hash.value(section))
|
||||
{
|
||||
|
||||
@@ -428,7 +428,9 @@ void TitleBlockTemplate::parseColumns(const QString &cols_string) {
|
||||
rel_col_size_format.setPattern("^([rt])([0-9]+)%$");
|
||||
rel_col_size_format.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
bool conv_ok;
|
||||
#ifdef TITLEBLOCK_TEMPLATE_DEBUG
|
||||
qDebug() <<"is QRegularExpression ok?";
|
||||
#endif
|
||||
|
||||
QStringList cols_descriptions =
|
||||
cols_string.split(QChar(';'), Qt::SkipEmptyParts);
|
||||
@@ -1830,10 +1832,13 @@ QString TitleBlockTemplate::interpreteVariables(
|
||||
QStringList TitleBlockTemplate::listOfVariables()
|
||||
{
|
||||
QStringList list;
|
||||
// Match every "%{name}" placeholder. The bare "%name" form can't be
|
||||
// extracted reliably without the variable list, and templates use the
|
||||
// braced form, so only that is collected here.
|
||||
static const QRegularExpression rx(QStringLiteral("%\\{([^}]+)\\}"));
|
||||
// Match both the braced "%{name}" form and the bare "%name" form
|
||||
// (bugtracker #245): a bare name is taken as the longest run of
|
||||
// identifier characters right after the '%', which correctly stops at
|
||||
// whitespace -- so "%name2 " and "%name2 %name3" are both detected,
|
||||
// matching what previously only worked for the braced form.
|
||||
static const QRegularExpression rx(
|
||||
QStringLiteral("%\\{([^}]+)\\}|%([A-Za-z0-9_-]+)"));
|
||||
// run through each individual cell
|
||||
for (int j = 0 ; j < rows_heights_.count() ; ++ j) {
|
||||
for (int i = 0 ; i < columns_width_.count() ; ++ i) {
|
||||
@@ -1844,7 +1849,9 @@ QStringList TitleBlockTemplate::listOfVariables()
|
||||
const QString cell_value = cells_[i][j] -> value.name();
|
||||
auto it = rx.globalMatch(cell_value);
|
||||
while (it.hasNext()) {
|
||||
const QString name = it.next().captured(1);
|
||||
const QRegularExpressionMatch m = it.next();
|
||||
const QString name = m.captured(1).isEmpty()
|
||||
? m.captured(2) : m.captured(1);
|
||||
if (!name.isEmpty() && !list.contains(name))
|
||||
list << name;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "bomexportdialog.h"
|
||||
|
||||
#include "../dataBase/ui/elementquerywidget.h"
|
||||
#include "../bomexport.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../qetproject.h"
|
||||
@@ -41,8 +42,8 @@ BOMExportDialog::BOMExportDialog(QETProject *project, QWidget *parent) :
|
||||
|
||||
m_query_widget = new ElementQueryWidget(this);
|
||||
ui->m_main_layout->insertWidget(0, m_query_widget);
|
||||
//By default format as bom is clicked
|
||||
on_m_format_as_bom_clicked(true);
|
||||
m_query_widget->setQuery(BomExport::defaultQuery());
|
||||
on_m_format_as_bom_clicked(false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,90 +68,58 @@ int BOMExportDialog::exec()
|
||||
if (dir.isEmpty()) dir = QETApp::documentDir();
|
||||
QString file_name = dir % "/" % tr("nomenclature_") % QString(m_project ->title() % ".csv");
|
||||
QString file_path = QFileDialog::getSaveFileName(this, tr("Enregister sous... "), file_name, tr("Fichiers csv (*.csv)"));
|
||||
QFile file(file_path);
|
||||
if (!file_path.isEmpty())
|
||||
{
|
||||
if (QFile::exists(file_path ))
|
||||
{
|
||||
// if file already exist -> delete it
|
||||
if (!QFile::remove(file_path) )
|
||||
{
|
||||
QMessageBox::critical(this, tr("Erreur"),
|
||||
tr("Impossible de remplacer le fichier!\n\n")+
|
||||
"Destination : "+file_path+"\n");
|
||||
}
|
||||
}
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
{
|
||||
QTextStream stream(&file);
|
||||
stream << getBom() << &Qt::endl(stream);
|
||||
QString error;
|
||||
const auto csv = getBom(&error);
|
||||
if (!error.isEmpty() || !BomExport::writeCsv(file_path, csv, &error)) {
|
||||
QMessageBox::critical(
|
||||
this, tr("Erreur"),
|
||||
tr("Impossible d'enregistrer la nomenclature dans %1.\n%2")
|
||||
.arg(file_path, error));
|
||||
}
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
QString BOMExportDialog::getBom()
|
||||
QByteArray BOMExportDialog::getBom(QString *error)
|
||||
{
|
||||
if (error) {
|
||||
error->clear();
|
||||
}
|
||||
m_project->dataBase()->updateDB();
|
||||
auto query_ = m_project->dataBase()->newQuery(m_query_widget->queryStr());
|
||||
QString return_string;
|
||||
|
||||
if (!query_.exec()) {
|
||||
qDebug() << "BOMExportDialog::getBom : query errir : " << query_.lastError();
|
||||
if (error) {
|
||||
*error = query_.lastError().text();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
else
|
||||
|
||||
QStringList header_names;
|
||||
if (ui->m_include_headers->isChecked())
|
||||
{
|
||||
//HEADERS
|
||||
if (ui->m_include_headers)
|
||||
const auto record_ = query_.record();
|
||||
for (int i = 0; i < record_.count(); ++i)
|
||||
{
|
||||
auto record_ = query_.record();
|
||||
QStringList header_name;
|
||||
for (auto i=0 ; i<record_.count() ; ++i)
|
||||
{
|
||||
auto field_name = record_.fieldName(i);
|
||||
|
||||
qDebug() << "field name = " << field_name;
|
||||
if (field_name == "position") {
|
||||
header_name << tr("Position");
|
||||
} else if (field_name == "diagram_position") {
|
||||
header_name << tr("Position du folio");
|
||||
} else if (field_name == "designation_qty") {
|
||||
header_name << tr("Quantité numéro d'article", "Special field with name : designation quantity");
|
||||
} else {
|
||||
header_name << QETInformation::translatedInfoKey(field_name);
|
||||
if (header_name.isEmpty()) {
|
||||
header_name << field_name;
|
||||
}
|
||||
}
|
||||
|
||||
const auto field_name = record_.fieldName(i);
|
||||
if (field_name == QLatin1String("position")) {
|
||||
header_names << tr("Position");
|
||||
} else if (field_name == QLatin1String("diagram_position")) {
|
||||
header_names << tr("Position du folio");
|
||||
} else if (field_name == QLatin1String("designation_qty")) {
|
||||
header_names << tr("Quantité numéro d'article", "Special field with name : designation quantity");
|
||||
} else {
|
||||
const auto translated = QETInformation::translatedInfoKey(field_name);
|
||||
header_names << (translated.isEmpty() ? field_name : translated);
|
||||
}
|
||||
return_string = header_name.join(";") % "\n";
|
||||
}
|
||||
|
||||
//ROWS
|
||||
while (query_.next())
|
||||
{
|
||||
auto i=0;
|
||||
QStringList values;
|
||||
while (query_.value(i).isValid())
|
||||
{
|
||||
auto date = query_.value(i).toDate();
|
||||
if (!date.isNull()) {
|
||||
values << QLocale::system().toString(query_.value(i).toDate(), QLocale::ShortFormat);
|
||||
} else {
|
||||
values << query_.value(i).toString();
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
return_string += values.join(";") % "\n";
|
||||
values.clear();
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << return_string;
|
||||
return return_string;
|
||||
return BomExport::toCsv(
|
||||
query_, header_names, ui->m_include_headers->isChecked());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#define BOMEXPORTDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QByteArray>
|
||||
|
||||
class QETProject;
|
||||
class ElementQueryWidget;
|
||||
@@ -39,7 +40,7 @@ class BOMExportDialog : public QDialog
|
||||
~BOMExportDialog() override;
|
||||
|
||||
virtual int exec() override;
|
||||
QString getBom();
|
||||
QByteArray getBom(QString *error = nullptr);
|
||||
|
||||
private slots:
|
||||
void on_m_format_as_bom_clicked(bool checked);
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string>Formater en tant que liste de materiel</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -290,9 +290,14 @@ ProjectAutoNumConfigPage::ProjectAutoNumConfigPage (QETProject *project,
|
||||
QWidget *parent) :
|
||||
ProjectConfigPage(project, parent)
|
||||
{
|
||||
initWidgets();
|
||||
buildConnections();
|
||||
readValuesFromProject();
|
||||
// Follow the same contract ProjectMainConfigPage's constructor does --
|
||||
// init() is documented to be the thing a subclass constructor calls
|
||||
// (see ProjectConfigPage::init()'s doc comment). Calling the pieces
|
||||
// individually here used to skip both initLayout() and adjustReadOnly(),
|
||||
// and called readValuesFromProject() even when m_project was null.
|
||||
// buildConnections() itself is invoked from the end of initWidgets()
|
||||
// below, in the same relative position it held here.
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,6 +357,8 @@ void ProjectAutoNumConfigPage::initWidgets()
|
||||
QHBoxLayout *main_layout = new QHBoxLayout();
|
||||
main_layout->addWidget(tab_widget);
|
||||
setLayout(main_layout);
|
||||
|
||||
buildConnections();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -269,6 +269,25 @@ void ShortcutsConfigPage::applyFilter()
|
||||
m_count_label->setText(tr("%n action(s)", nullptr, visible_actions));
|
||||
}
|
||||
|
||||
// IDs use stable context prefixes; translated display categories are not scopes.
|
||||
// Main-window actions are shared by the editors, and depth actions by the
|
||||
// diagram and element editors. Panel-specific shortcuts retain their own scope.
|
||||
static bool shortcutScopesOverlap(const QString &first_id, const QString &second_id)
|
||||
{
|
||||
const QString first = first_id.section(QLatin1Char('.'), 0, 0);
|
||||
const QString second = second_id.section(QLatin1Char('.'), 0, 0);
|
||||
if (first == second || first == QLatin1String("mainwindow")
|
||||
|| second == QLatin1String("mainwindow"))
|
||||
return true;
|
||||
|
||||
const auto has_depth = [](const QString &scope) {
|
||||
return scope == QLatin1String("diagrameditor")
|
||||
|| scope == QLatin1String("elementeditor");
|
||||
};
|
||||
return (first == QLatin1String("depth") && has_depth(second))
|
||||
|| (second == QLatin1String("depth") && has_depth(first));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::checkConflicts
|
||||
Highlight every row whose currently-edited sequence is shared, non-empty,
|
||||
@@ -287,8 +306,12 @@ void ShortcutsConfigPage::checkConflicts()
|
||||
for (int row = 0; row < m_rows.size(); ++row) {
|
||||
Row ¤t_row = m_rows[row];
|
||||
const QString sequence_text = current_row.edit->keySequence().toString();
|
||||
const QList<int> &conflicting_rows = sequence_to_rows.value(sequence_text);
|
||||
const bool conflicted = !sequence_text.isEmpty() && conflicting_rows.size() > 1;
|
||||
QList<int> conflicting_rows;
|
||||
for (int other : sequence_to_rows.value(sequence_text)) {
|
||||
if (other != row && shortcutScopesOverlap(current_row.id, m_rows.at(other).id))
|
||||
conflicting_rows << other;
|
||||
}
|
||||
const bool conflicted = !conflicting_rows.isEmpty();
|
||||
|
||||
current_row.conflicted = conflicted;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "../qetgraphicsitem/dynamicelementtextitem.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetgraphicsitem/elementtextitemgroup.h"
|
||||
#include "../qetgraphicsitem/masterelement.h"
|
||||
#include "../qeticons.h"
|
||||
#include "dynamicelementtextitemeditor.h"
|
||||
#include "elementinfowidget.h"
|
||||
@@ -380,9 +381,48 @@ QWidget *ElementPropertiesWidget::generalWidget()
|
||||
description_string += QString(tr("Rotation : %1°\n")).arg(m_element.data()->rotation());
|
||||
description_string += QString(tr("Dimensions : %1*%2\n")).arg(m_element -> size().width()).arg(m_element -> size().height());
|
||||
description_string += QString(tr("Bornes : %1\n")).arg(m_element -> terminals().count());
|
||||
if (m_element->linkType() == Element::Master){
|
||||
description_string += QString(tr("Nombre maximum de contacts esclaves définis : %1\n")).arg(m_element -> elementData().m_max_slaves);
|
||||
description_string += QString(tr("Nombre de contacts esclaves utilisés : %1\n")).arg(m_element ->linkedElements().count());
|
||||
if (m_element->linkType() == Element::Master)
|
||||
{
|
||||
//The declared limit is optional: -1 means the element sets no
|
||||
//limit at all, which is worth saying rather than printing "-1".
|
||||
const int max_slaves = m_element->elementData().m_max_slaves;
|
||||
description_string += max_slaves == -1
|
||||
? QString(tr("Nombre maximum de contacts esclaves définis : non défini\n"))
|
||||
: QString(tr("Nombre maximum de contacts esclaves définis : %1\n")).arg(max_slaves);
|
||||
|
||||
//Left as a count of linked elements: the line above is a number
|
||||
//of slots, and a slave fills one slot however many contacts it
|
||||
//carries, so the two stay in the same unit.
|
||||
description_string += QString(tr("Nombre de contacts esclaves utilisés : %1\n")).arg(m_element->linkedElements().count());
|
||||
|
||||
//The breakdown below is in contacts, not slots: it answers how
|
||||
//many contacts an auxiliary block must provide.
|
||||
const MasterElement *master =
|
||||
static_cast<const MasterElement *>(m_element.data());
|
||||
const ContactUsage usage = master->contactUsage();
|
||||
const ContactUsage capacity = master->contactCapacity();
|
||||
|
||||
if (capacity.total() > 0)
|
||||
{
|
||||
//The element declares contact groups, so it can say not
|
||||
//only what has been used but what it has to offer. A type
|
||||
//used beyond what is declared shows as e.g. "1/0", which
|
||||
//is the point: it says this contact does not fit the part.
|
||||
description_string += QString(tr(" Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8\n"))
|
||||
.arg(usage.no).arg(capacity.no)
|
||||
.arg(usage.nc).arg(capacity.nc)
|
||||
.arg(usage.sw).arg(capacity.sw)
|
||||
.arg(usage.other).arg(capacity.other);
|
||||
}
|
||||
else if (usage.total() > 0)
|
||||
{
|
||||
//No declared groups, so a plain count of what is in use.
|
||||
description_string += QString(tr(" Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4\n"))
|
||||
.arg(usage.no)
|
||||
.arg(usage.nc)
|
||||
.arg(usage.sw)
|
||||
.arg(usage.other);
|
||||
}
|
||||
}
|
||||
description_string += QString(tr("Emplacement : %1\n")).arg(m_element.data()->location().toString());
|
||||
|
||||
|
||||
@@ -65,7 +65,17 @@ ClickableImageLabel::ClickableImageLabel(const QImage &sourceImage, QWidget *par
|
||||
*/
|
||||
void ClickableImageLabel::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() != Qt::LeftButton || pixmap().isNull())
|
||||
// QLabel::pixmap() returns a pointer in Qt5 and a value in Qt6.
|
||||
// Qt 5.15 offers the by-value form behind Qt::ReturnByValue; the
|
||||
// pointer overload is deprecated there, so take the by-value one
|
||||
// on both and the difference reduces to the argument.
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
const QPixmap label_pixmap = pixmap(Qt::ReturnByValue);
|
||||
#else
|
||||
const QPixmap label_pixmap = pixmap();
|
||||
#endif
|
||||
|
||||
if (event->button() != Qt::LeftButton || label_pixmap.isNull())
|
||||
return;
|
||||
|
||||
// The label may be larger than its pixmap (layout stretching); the
|
||||
@@ -73,9 +83,9 @@ void ClickableImageLabel::mousePressEvent(QMouseEvent *event)
|
||||
// so the click has to be re-based against the pixmap's own rect
|
||||
// within the label, not the label's own top-left.
|
||||
const QRect pixmapRect(
|
||||
(width() - pixmap().width()) / 2,
|
||||
(height() - pixmap().height()) / 2,
|
||||
pixmap().width(), pixmap().height());
|
||||
(width() - label_pixmap.width()) / 2,
|
||||
(height() - label_pixmap.height()) / 2,
|
||||
label_pixmap.width(), label_pixmap.height());
|
||||
if (!pixmapRect.contains(event->pos()))
|
||||
return;
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
#include "linksingleelementwidget.h"
|
||||
#include "contactgroupselectiondialog.h"
|
||||
#include "../qetgraphicsitem/masterelement.h"
|
||||
#include "../qetgraphicsitem/conductor.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramposition.h"
|
||||
@@ -353,8 +352,16 @@ void LinkSingleElementWidget::buildTree()
|
||||
|
||||
QSettings settings;
|
||||
QVariant v = settings.value(QStringLiteral("link-element-widget/report-state"));
|
||||
if(!v.isNull())
|
||||
ui->m_tree_widget->header()->restoreState(v.toByteArray());
|
||||
auto *header = ui->m_tree_widget->header();
|
||||
if (v.isNull() || !header->restoreState(v.toByteArray()))
|
||||
{
|
||||
// Keep logical column IDs stable for saved layouts, but show the
|
||||
// folio identity first even when the candidate has no conductor.
|
||||
for (int column = 5; column < 8; ++column)
|
||||
header->moveSection(header->visualIndex(column), column - 5);
|
||||
ui->m_tree_widget->resizeColumnToContents(5);
|
||||
ui->m_tree_widget->resizeColumnToContents(6);
|
||||
}
|
||||
}
|
||||
|
||||
setUpCompleter();
|
||||
@@ -410,11 +417,12 @@ QVector <QPointer<Element>> LinkSingleElementWidget::availableElements()
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the master is full, we'll remove it from the list!
|
||||
MasterElement *master = static_cast<MasterElement*>(elmt);
|
||||
if (master->isFull()) {
|
||||
elmt_vector.removeAt(i);
|
||||
}
|
||||
// A master at its declared limit stays in the list. Removing
|
||||
// it made a full master indistinguishable from one that does
|
||||
// not exist: the candidate simply was not there, with nothing
|
||||
// to say why. The limit is advisory -- see the prompt in
|
||||
// MasterPropertiesWidget::on_link_button_clicked() -- so the
|
||||
// user decides, rather than the list deciding for them.
|
||||
}
|
||||
}
|
||||
return elmt_vector;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user