Merge upstream/master: translation wave plus generic device

Upstream moved 56 commits ahead of the last merge. Six files conflicted,
all of them files where upstream's French -> English tr() translation wave
touched lines we had also touched with the cable feature. Resolution rule
applied everywhere: take upstream's English source strings, keep our cable
additions.

Resolved:

- sources/ui/xrefpropertieswidget.ui : kept our structure (m_labels_gb
  pulled out of the display group, m_slave_row, the m_font_pb "Police..."
  button of the cable cross-reference) and mechanically applied upstream's
  20 English string replacements on top of it.

- sources/ui/xrefpropertieswidget.cpp : our version plus upstream's 7
  translated combo entries; the tr("Cable") item we add for the cable
  cross-reference type stays.

- sources/autoNum/ui/numparteditorw.cpp : upstream's English part names
  ("number format 1", "Sheet no.", "Plant", "Location", ...) for the
  number-format list, with our comment explaining why type 3 (cable) takes
  the conductor kinds of variable.

- sources/ui/configpage/configpages.cpp : upstream's English tab titles
  (Conductors / Elements / Sheets) plus our Cables tab.

- sources/ui/configpage/projectconfigpages.cpp : upstream's "Sheets" tab
  title plus our Cables tab block (single-rule SelectAutonumW(3)).

- sources/qetdiagrameditor.cpp : union of both sides - upstream's new
  "Add a generic device" action (icon, status tip, data id) and its
  English menu names ("File"/"Edit"/"Project"/"Display"), together with our
  cable action, the Numbering and Lists menus, the cable list actions and
  the CSV export in the Project menu.

Kept French: our own cable tr() source strings stay French for now. That
is the smaller change and the translation pass can follow separately.

Verified: full build 1065/1065, ctest 91/92 (only the pre-existing
headless tst_menubarkeyboard fails: no window manager, so Alt+F and F10
cannot open a menu bar), smoke start exit 124, no conflict marker left in
the tree.
This commit is contained in:
Kellermorph
2026-10-09 20:44:58 +02:00
437 changed files with 233166 additions and 197854 deletions
+13
View File
@@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.16)
project(handle_refresh_probe LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
add_executable(handle_refresh_probe main.cpp
${QET_SOURCE}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.cpp
${QET_SOURCE}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.h)
target_include_directories(handle_refresh_probe PRIVATE ${QET_SOURCE}/sources)
target_link_libraries(handle_refresh_probe PRIVATE Qt6::Widgets)
if(WIN32)
target_link_libraries(handle_refresh_probe PRIVATE psapi)
endif()
+69
View File
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Synthetic raster viewport workload, not a full editor latency benchmark.
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QElapsedTimer>
#include <QPainter>
#include <cstdio>
#include <ctime>
#ifdef Q_OS_WIN
#include <windows.h>
#include <psapi.h>
#endif
#include "QetGraphicsItemModeler/qetgraphicshandleritem.h"
static double cpuMilliseconds() {
#ifdef Q_OS_WIN
FILETIME created,exited,kernel,user;
if(GetProcessTimes(GetCurrentProcess(),&created,&exited,&kernel,&user)) {
ULARGE_INTEGER k,u; k.LowPart=kernel.dwLowDateTime;k.HighPart=kernel.dwHighDateTime;
u.LowPart=user.dwLowDateTime;u.HighPart=user.dwHighDateTime;
return double(k.QuadPart+u.QuadPart)/10000;
}
return 0;
#else
return double(std::clock())*1000/CLOCKS_PER_SEC;
#endif
}
int main(int argc,char **argv) {
QApplication app(argc,argv);
const qreal size=QString::fromLocal8Bit(argv[1]).toDouble();
const int count=QString::fromLocal8Bit(argv[2]).toInt();
const QString operation=QString::fromLocal8Bit(argv[3]);
QGraphicsScene scene; scene.setSceneRect(-2000,-2000,4000,4000);
QList<QGraphicsRectItem *> objects;
for(int i=0;i<count/4;++i) {
auto *object=scene.addRect(0,0,32,24,QPen(Qt::black),QBrush(Qt::lightGray));
object->setPos((i%32)*36-576,(i/32)*28-224); objects<<object;
for(auto *handle:QetGraphicsHandlerItem::handlerForPoint({{0,0},{32,0},{32,24},{0,24}},size)) {
handle->setParentItem(object); handle->setColor(Qt::darkGreen);
}
}
QGraphicsView view(&scene); view.resize(1280,800); view.setSceneRect(scene.sceneRect());
view.show(); QApplication::processEvents();
QImage image(1280,800,QImage::Format_ARGB32_Premultiplied);
auto frame=[&](int i) {
if(operation=="zoom") {view.resetTransform();view.scale(0.75+0.005*(i%60),0.75+0.005*(i%60));}
if(operation=="pan") view.centerOn((i%60)*2,0);
if(operation=="manipulate") for(auto *object:objects) {
object->setRotation(i%30); object->setRect(0,0,32+(i%8),24); object->moveBy(i%2?1:-1,0);
}
image.fill(Qt::white); QPainter painter(&image); view.render(&painter);
};
for(int i=0;i<20;++i) frame(i);
std::puts("size,handles,operation,repeat,ms_per_frame,cpu_ms_per_frame,working_set_bytes");
for(int repeat=0;repeat<7;++repeat) {
const auto cpu=cpuMilliseconds(); QElapsedTimer timer; timer.start();
for(int i=0;i<120;++i) frame(i);
const double elapsed=timer.nsecsElapsed()/1e6/120;
const double cpuElapsed=(cpuMilliseconds()-cpu)/120;
unsigned long long memory=0;
#ifdef Q_OS_WIN
PROCESS_MEMORY_COUNTERS counters{}; counters.cb=sizeof(counters);
if(GetProcessMemoryInfo(GetCurrentProcess(),&counters,sizeof(counters))) memory=counters.WorkingSetSize;
#endif
std::printf("%.2f,%d,%s,%d,%.6f,%.6f,%llu\n",size,count,qPrintable(operation),repeat,elapsed,cpuElapsed,memory);
}
}
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Run identical Release component workloads; preserve samples and variability."""
import argparse
import csv
import io
import os
import pathlib
import statistics
import subprocess
p = argparse.ArgumentParser()
p.add_argument('--reference', required=True)
p.add_argument('--modified', required=True)
p.add_argument('--output', required=True)
a = p.parse_args()
env = os.environ.copy()
env['QT_QPA_PLATFORM'] = 'offscreen'
rows = []
# Compare x1 first, then exercise every remaining size on the modified code.
for size in [10, 2.5, 5, 7.5, 20, 30]:
for count in [16, 2048]:
for operation in ['zoom', 'pan', 'manipulate']:
variants = [('reference', a.reference), ('modified', a.modified)] if size == 10 else [('modified', a.modified)]
for variant, binary in variants:
result = subprocess.run([binary, str(size), str(count), operation], env=env,
capture_output=True, text=True, check=True, timeout=120)
samples = list(csv.DictReader(io.StringIO(result.stdout)))
assert len(samples) == 7
rows.extend(dict(variant=variant, **sample) for sample in samples)
output = pathlib.Path(a.output)
output.mkdir(parents=True, exist_ok=True)
with (output / 'samples.csv').open('w', newline='', encoding='utf-8') as stream:
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
groups = {}
for row in rows:
key = (row['variant'], row['size'], row['handles'], row['operation'])
groups.setdefault(key, []).append(row)
lines = ['# Release handle refresh measurements', '',
'Synthetic QGraphicsView raster rendering at 1280×800, offscreen, on the same PC.',
'Each sample comprises 120 frames; 20 warm-up frames precede seven samples.',
'16 or 2048 handles; same scenes and workload code for both builds.',
'Manipulate changes position, rectangle size and rotation of all scene objects.',
'This is not full-editor mouse latency, GPU frame presentation or human usability.', '',
'| Code | Factor | Handles | Workload | Mean ms/frame ± SD | Min–max | CPU ms/frame | Working set MiB |',
'|---|---:|---:|---|---:|---:|---:|---:|']
for (variant, size, count, operation), samples in groups.items():
wall = [float(s['ms_per_frame']) for s in samples]
cpu = [float(s['cpu_ms_per_frame']) for s in samples]
memory = [int(s['working_set_bytes']) / 1048576 for s in samples]
lines.append(f'| {variant} | {float(size)/10:g} | {count} | {operation} | '
f'{statistics.mean(wall):.3f} ± {statistics.stdev(wall):.3f} | '
f'{min(wall):.3f}–{max(wall):.3f} | {statistics.mean(cpu):.3f} | '
f'{statistics.median(memory):.1f} |')
(output / 'measurements.md').write_text('\n'.join(lines)+'\n', encoding='utf-8')
print(f'{len(rows)} samples saved in {output}')
+63 -2
View File
@@ -74,6 +74,13 @@ target_include_directories(tst_darkimagelayers PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_darkimagelayers PRIVATE Qt::Test Qt::Widgets)
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
add_test(NAME tst_handlersizeintegration
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_handle_size_probe.py
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
--source ${CMAKE_CURRENT_SOURCE_DIR}/handle_size_probe.cpp
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
set_tests_properties(tst_handlersizeintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
add_test(NAME tst_attachedplainpaste
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_attached_plain_paste_probe.py
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
@@ -178,12 +185,24 @@ add_test(NAME tst_symbolscale COMMAND tst_symbolscale)
target_include_directories(tst_symbolscale PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_symbolscale PRIVATE Qt::Test)
# genericdevice.cpp is pure: the generic device wizard's layout, pattern
# lines, groups and pin numbers, and the symbol it writes.
add_executable(tst_genericdevice tst_genericdevice.cpp
${QET_DIR}/sources/genericdevice/genericdevice.cpp)
add_test(NAME tst_genericdevice COMMAND tst_genericdevice)
set_tests_properties(tst_genericdevice PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_genericdevice PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_genericdevice PRIVATE Qt::Test Qt::Gui Qt::Xml)
# configprofile.h is header-only: which settings a saved configuration
# profile carries, and how loading one replaces the live settings.
add_executable(tst_configprofile tst_configprofile.cpp)
# borderproperties.cpp: the real writer of the folio size defaults, to check
# that what it writes falls in the part for new projects.
add_executable(tst_configprofile tst_configprofile.cpp
${QET_DIR}/sources/borderproperties.cpp)
add_test(NAME tst_configprofile COMMAND tst_configprofile)
target_include_directories(tst_configprofile PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_configprofile PRIVATE Qt::Test Qt::Gui)
target_link_libraries(tst_configprofile PRIVATE Qt::Test Qt::Gui Qt::Xml)
# itemgroups.cpp keeps group membership and the selection rule apart from
# Diagram, so it is tested here on a plain scene of rectangles.
@@ -208,6 +227,14 @@ set_tests_properties(tst_textanchor PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offs
target_include_directories(tst_textanchor PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_textanchor PRIVATE Qt::Test Qt::Widgets)
# Shared handle geometry and picking, including fractional sizes.
add_executable(tst_handlersize tst_handlersize.cpp
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.cpp)
add_test(NAME tst_handlersize COMMAND tst_handlersize)
set_tests_properties(tst_handlersize PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_handlersize PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_handlersize PRIVATE Qt::Test Qt::Widgets)
# TextResizeHandles: the corner handles that change the width of a text box,
# on a plain QGraphicsTextItem with the "textWidth" property QET texts have.
add_executable(
@@ -621,6 +648,17 @@ target_link_libraries(tst_dxfcurves PRIVATE Qt::Test)
target_compile_definitions(tst_dxfcurves PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# A static text with anchor="alignment" has its x at the edge or centre its
# Halignment selects, not at its left edge (#1251).
add_executable(
tst_statictextanchor
tst_statictextanchor.cpp)
add_test(NAME tst_statictextanchor COMMAND tst_statictextanchor)
add_dependencies(tst_statictextanchor qelectrotech)
target_link_libraries(tst_statictextanchor PRIVATE Qt::Test)
target_compile_definitions(tst_statictextanchor PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# --export-dxf --dxf-blocks: a symbol placed twice is one BLOCK and two
# INSERTs (issue #1339).
add_executable(
@@ -922,6 +960,18 @@ target_compile_definitions(tst_derivedwireuuid PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# Symbols sharing one uuid on a folio keep their wires through save and
# reopen (#1408): nets of examples/Habitat-Schemas_developpes.qet.
add_executable(
tst_sharedsymboluuid
tst_sharedsymboluuid.cpp)
add_test(NAME tst_sharedsymboluuid COMMAND tst_sharedsymboluuid)
add_dependencies(tst_sharedsymboluuid qelectrotech)
target_link_libraries(tst_sharedsymboluuid PRIVATE Qt::Test Qt::Xml)
target_compile_definitions(tst_sharedsymboluuid PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# Element numbering schemes carry an id and elements follow them by it:
# a legacy file gets stable ids on --resave; through --run, rename, edit,
# refused removal and undo/redo keep the elements following the scheme.
@@ -1081,6 +1131,17 @@ add_test(NAME tst_borderpropertiesxml COMMAND tst_borderpropertiesxml)
target_include_directories(tst_borderpropertiesxml PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_borderpropertiesxml PRIVATE Qt::Test Qt::Xml)
# Picture files dropped on a folio: which files are taken, the size and
# pixel limits before a picture is embedded, fitting into the view and the
# cascade of several pictures.
add_executable(
tst_imagedrop
tst_imagedrop.cpp
${QET_DIR}/sources/imagedrop.cpp)
add_test(NAME tst_imagedrop COMMAND tst_imagedrop)
target_include_directories(tst_imagedrop PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_imagedrop PRIVATE Qt::Test Qt::Gui)
# Qt 6.4.2 writes the attributes of an element in an order that changes from
# one process to the next, so a project saved twice by two runs of the binary
# differs in bytes only (seen on projects without any numbering, too). A fixed
@@ -0,0 +1,685 @@
<?xml version='1.0' encoding='utf-8'?>
<project title="Habitat-Schemas_developpes" uuid="{947a7459-9d3d-57ef-896f-ccacfb38ab71}" version="0.200.1">
<properties>
<property name="saveddate" show="1">17/04/2021</property>
<property name="saveddate-eu" show="1">17-04-2021</property>
<property name="saveddate-us" show="1">2021-04-17</property>
<property name="savedfilename" show="1">Habitat-Schemas_developpes</property>
<property name="savedfilepath" show="1">C:/Users/VD5385/Documents/Github/qelectrotech-source-mirror/qelectrotech-source-mirror/examples/Habitat-Schemas_developpes.qet</property>
<property name="savedtime" show="1">14:45</property>
</properties>
<usage enabled="true" time_spent="0" />
<newdiagrams>
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80" />
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="%id/%total" indexrev="" locmach="" plant="" title="" version="" />
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="9" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="0" />
<report label="%f-%l%c" />
<xrefs>
<xref delayprefix="" displayhas="contacts" master_label="%f-%l%c" offset="40" powerprefix="" showallconfiguredslaves="false" showpowerctc="false" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="bottom" stackoverlapping="true" switchprefix="" type="coil" xrefpos="AlignBaseline" />
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="false" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" stackoverlapping="true" switchprefix="" type="commutator" xrefpos="AlignBottom" />
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" stackoverlapping="true" switchprefix="" type="plc" xrefpos="AlignBottom" />
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="40" powerprefix="" showallconfiguredslaves="false" showpowerctc="false" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" stackoverlapping="true" switchprefix="" type="protection" xrefpos="AlignBaseline" />
</xrefs>
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false" />
<folio_autonums />
<element_autonums current_autonum="" freeze_new_elements="false" />
<guides />
</newdiagrams>
<diagram author="QElectroTech Team" auto_page_num="" cols="10" colsize="60" date="20130601" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="%id/%total" freezeNewConductor="false" freezeNewElement="false" height="420" indexrev="" locmach="" order="1" plant="" rows="5" rowsize="80" title="Eclairage en simple allumage" uuid="{88807a77-e299-5e27-a274-103fe6bb51ae}" version="0.200.1-dev">
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="9" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="0" />
<elements>
<element freezeLabel="false" is_movable="1" orientation="1" prefix="" type="embed://import/sources/multifilaire/src_p.elmt" uuid="{7c229e90-c025-4c01-bbb7-7af8fd90aefc}" x="90" y="80" z="10">
<terminals>
<terminal id="0" orientation="1" x="6" y="0" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">L</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-90" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{7ffba7b0-932b-47e7-9fa5-d8c5a56a3c27}" x="-26.0822" y="6.73678">
<text>L</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/sources/multifilaire/src_p.elmt" uuid="{d001d8d3-e0b8-4f8c-bad3-2dc564ddbb1d}" x="90" y="400" z="10">
<terminals>
<terminal id="1" orientation="1" x="6" y="0" />
</terminals>
<inputs />
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{de902e74-cc32-4c39-991b-d8b633aed3f8}" x="-2.0425" y="16.5674">
<text />
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/protections/disjoncteurs/disjonct-m.elmt" uuid="{1a0c9ea9-bfac-4816-97a4-d1676abe08ff}" x="140" y="160" z="10">
<terminals>
<terminal id="2" orientation="0" x="10" y="-16" />
<terminal id="3" orientation="2" x="10" y="26" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">16A</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{9bf813a9-2a95-4f13-9baf-1906771e0040}" x="43.45" y="-13.3834">
<text>16A</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/protections/disjoncteurs/disjonct-m.elmt" uuid="{34153d81-c75e-4269-85b1-541d16982ad9}" x="140" y="350" z="10">
<terminals>
<terminal id="4" orientation="0" x="10" y="-16" />
<terminal id="5" orientation="2" x="10" y="26" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">20A</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{bccf3627-f134-41c6-a00f-952c5fa75c53}" x="43.45" y="-13.3834">
<text>20A</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/contacts/interrupteurs/interrupteur.elmt" uuid="{3534f52e-ebe0-4510-9706-3f7e2734080f}" x="230" y="150" z="10">
<terminals>
<terminal id="6" orientation="0" x="0" y="-17" />
<terminal id="7" orientation="2" x="0" y="17" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">S1</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{52f55b0e-243a-43e6-9cd8-e07e105413ce}" x="21.7" y="-5.55">
<text>S1</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/contacts/interrupteurs/interrupteur.elmt" uuid="{0f7a1074-334f-4173-a9e9-25a601ef4c52}" x="230" y="210" z="10">
<terminals>
<terminal id="8" orientation="0" x="0" y="-17" />
<terminal id="9" orientation="2" x="0" y="17" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">S2</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{dc88b1bd-21e2-43b6-b53e-f25099be0f4b}" x="21.7" y="-5.55">
<text>S2</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/pc.elmt" uuid="{92274d83-04c4-49eb-83b6-7c8d7d89e638}" x="300" y="340" z="10">
<terminals>
<terminal id="10" orientation="3" x="-25" y="0" />
<terminal id="11" orientation="1" x="1" y="0" />
<terminal id="12" orientation="2" x="-12" y="6" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">PC1</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{210108db-89bf-40e7-8c8d-6acfd1bedc8d}" x="-20" y="-33.5">
<text>PC1</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/lampe_pe.elmt" uuid="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" x="320" y="150" z="10">
<terminals>
<terminal id="13" orientation="0" x="0" y="-17" />
<terminal id="14" orientation="2" x="-10" y="12" />
<terminal id="15" orientation="2" x="0" y="17" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">L1</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{8570e0e6-41ec-47a3-b629-c58e8c4ccc57}" x="32.95" y="-11.55">
<text>L1</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/lampe_pe.elmt" uuid="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" x="320" y="210" z="10">
<terminals>
<terminal id="16" orientation="0" x="0" y="-17" />
<terminal id="17" orientation="2" x="-10" y="12" />
<terminal id="18" orientation="2" x="0" y="17" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">L2</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{5e1942f3-39a6-4b77-a862-5c2a9712a63a}" x="32.95" y="-11.55">
<text>L2</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/lampe_pe.elmt" uuid="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" x="320" y="270" z="10">
<terminals>
<terminal id="19" orientation="0" x="0" y="-17" />
<terminal id="20" orientation="2" x="-10" y="12" />
<terminal id="21" orientation="2" x="0" y="17" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">L3</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{29ef9f5b-b2ac-4ad7-b315-f5a5226549d3}" x="32.95" y="-11.55">
<text>L3</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/disj_neutre.elmt" uuid="{4c068000-8007-4a6d-82d1-21c157209335}" x="430" y="150" z="10">
<terminals>
<terminal id="22" orientation="0" x="0" y="-17" />
<terminal id="23" orientation="2" x="0" y="17" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">10A</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{e1dcd9cb-38a2-41d4-a54a-f137a4bce8be}" x="28.5161" y="-16.2484">
<text>10A</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/disj_neutre.elmt" uuid="{7c25df98-6c99-4dde-9080-a08214224b06}" x="430" y="340" z="10">
<terminals>
<terminal id="24" orientation="0" x="0" y="-17" />
<terminal id="25" orientation="2" x="0" y="17" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">20A</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{aa9eeb7b-9f43-46f9-adf3-dd3f161c0782}" x="28.5161" y="-16.2484">
<text>20A</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="1" prefix="" type="embed://import/sources/multifilaire/src_n.elmt" uuid="{0b635509-0349-4517-93fd-1c602d4cd8dd}" x="510" y="80" z="10">
<terminals>
<terminal id="26" orientation="1" x="6" y="0" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">N</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-90" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{1dc02080-1b95-4b54-9aeb-58dfa7289744}" x="-28.7219" y="7.23678">
<text>N</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/sources/multifilaire/src_n.elmt" uuid="{6eb3bd36-847c-403f-87b0-c7a4b7e8327f}" x="510" y="390" z="10">
<terminals>
<terminal id="27" orientation="1" x="6" y="0" />
</terminals>
<inputs />
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{b1001bac-77a1-45a5-9d79-0c0bde88ac15}" x="-6.8219" y="-9.76322">
<text />
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="1" prefix="" type="embed://import/sources/multifilaire/pe.elmt" uuid="{d6d0d1d9-21cc-4757-8fbf-d44726eaf18d}" x="560" y="80" z="10">
<terminals>
<terminal id="28" orientation="1" x="6" y="0" />
</terminals>
<inputs />
<elementInformations>
<elementInformation name="label" show="1">PE</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-90" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{ce539f0c-0df5-42c2-a6a4-01feb89d4a4e}" x="-27.369" y="12.45">
<text>PE</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
<element freezeLabel="false" is_movable="1" orientation="3" prefix="" type="embed://import/sources/multifilaire/pe.elmt" uuid="{4d7f799d-2263-448a-9350-35237b5345f0}" x="560" y="390" z="10">
<terminals>
<terminal id="29" orientation="1" x="6" y="0" />
</terminals>
<inputs />
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="MS Shell Dlg 2,9,-1,5,50,0,0,0,0,0,Normal" frame="false" keep_visual_rotation="true" rotation="-270" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{04862e60-07c2-4d6b-a271-fe77ba0ad07e}" x="-5.469" y="-13.55">
<text />
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups />
</element>
</elements>
<conductors>
<conductor bicolor="false" bus="" cable="" color="#ff0000" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{d001d8d3-e0b8-4f8c-bad3-2dc564ddbb1d}" element1_label="" element1_name="Phase source" element2="{7c229e90-c025-4c01-bbb7-7af8fd90aefc}" element2_label="L" element2_name="Phase source" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" rotation="270" tension_protocol="" terminal1="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminal2="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{affdf717-0a37-5ccf-b9ae-61ca395e0f82}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#ff0000" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{1a0c9ea9-bfac-4816-97a4-d1676abe08ff}" element1_label="16A" element1_name="Circuit-breaker" element2="{7c229e90-c025-4c01-bbb7-7af8fd90aefc}" element2_label="L" element2_name="Phase source" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" rotation="270" tension_protocol="" terminal1="{ab69f5f1-346f-5514-b8f4-e23282dd76ad}" terminal2="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{8423eab0-c93d-5cb7-b670-ed915025647e}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#ff0000" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{34153d81-c75e-4269-85b1-541d16982ad9}" element1_label="20A" element1_name="Circuit-breaker" element2="{7c229e90-c025-4c01-bbb7-7af8fd90aefc}" element2_label="L" element2_name="Phase source" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" rotation="270" tension_protocol="" terminal1="{ab69f5f1-346f-5514-b8f4-e23282dd76ad}" terminal2="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{6fd96e4d-6d22-551f-afed-d0d28ab3d026}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#ff0000" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{1a0c9ea9-bfac-4816-97a4-d1676abe08ff}" element1_label="16A" element1_name="Circuit-breaker" element2="{3534f52e-ebe0-4510-9706-3f7e2734080f}" element2_label="S1" element2_name="Switch" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" tension_protocol="" terminal1="{3a9a1158-fcda-555c-ad99-d6b92a4f04a7}" terminal2="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" terminalname1="" terminalname2="" text_color="#000000" type="multi" userx="160" usery="120" uuid="{4170dcca-a770-5e1c-abdc-00460596271a}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#ff0000" color2="#000000" condsize="1" conductor_color="" conductor_section="2,5mm²" dash-size="2" displaytext="1" element1="{34153d81-c75e-4269-85b1-541d16982ad9}" element1_label="20A" element1_name="Circuit-breaker" element2="{92274d83-04c4-49eb-83b6-7c8d7d89e638}" element2_label="PC1" element2_name="c" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="2,5mm²" numsize="9" onetextperfolio="0" tension_protocol="" terminal1="{3a9a1158-fcda-555c-ad99-d6b92a4f04a7}" terminal2="{0c6b6b61-7e22-53c9-8e34-25f816145de3}" terminalname1="" terminalname2="" text_color="#000000" type="multi" userx="200" usery="350" uuid="{3d91adfc-5ea3-5a8f-8c84-a32c43c725c6}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#ff0000" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{3534f52e-ebe0-4510-9706-3f7e2734080f}" element1_label="S1" element1_name="Switch" element2="{0f7a1074-334f-4173-a9e9-25a601ef4c52}" element2_label="S2" element2_name="Switch" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="1,5mm²" numsize="9" onetextperfolio="0" rotation="270" tension_protocol="" terminal1="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" terminal2="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" terminalname1="" terminalname2="" text_color="#000000" type="multi" userx="160" usery="210" uuid="{683fa12e-0275-5cfb-ae1e-1d5bb293660c}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<segment length="-9" orientation="horizontal" />
<segment length="-10" orientation="horizontal" />
<segment length="60" orientation="vertical" />
<segment length="10" orientation="horizontal" />
<segment length="9" orientation="horizontal" />
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{3534f52e-ebe0-4510-9706-3f7e2734080f}" element1_label="S1" element1_name="Switch" element2="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element2_label="L1" element2_name="e" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" tension_protocol="" terminal1="{6c830253-af7a-5723-9fc2-304c81fe739f}" terminal2="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{995071b4-5237-5328-b1fe-f37115b72d60}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#ff5500" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{0f7a1074-334f-4173-a9e9-25a601ef4c52}" element1_label="S2" element1_name="Switch" element2="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element2_label="L2" element2_name="e" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" tension_protocol="" terminal1="{6c830253-af7a-5723-9fc2-304c81fe739f}" terminal2="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{e23364f3-f08b-50dd-8f6e-6b2d0533d24e}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#ff5500" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{0f7a1074-334f-4173-a9e9-25a601ef4c52}" element1_label="S2" element1_name="Switch" element2="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element2_label="L3" element2_name="e" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" rotation="270" tension_protocol="" terminal1="{6c830253-af7a-5723-9fc2-304c81fe739f}" terminal2="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{68ef76c3-03b2-5d9e-a377-d3067d3f12e0}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#0000ff" color2="#000000" condsize="1" conductor_color="" conductor_section="2,5mm²" dash-size="2" displaytext="1" element1="{7c25df98-6c99-4dde-9080-a08214224b06}" element1_label="20A" element1_name="Circuit-breaker" element2="{92274d83-04c4-49eb-83b6-7c8d7d89e638}" element2_label="PC1" element2_name="c" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" tension_protocol="" terminal1="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" terminal2="{5e7df160-501d-5ae1-9dc2-59d2ba0daf9a}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{95ef461f-72e0-51d4-bb0f-e6f012928528}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<segment length="-9" orientation="horizontal" />
<segment length="-50" orientation="horizontal" />
<segment length="0" orientation="horizontal" />
<segment length="-36" orientation="horizontal" />
<segment length="0" orientation="horizontal" />
<segment length="-9" orientation="horizontal" />
<sequentialNumbers />
</conductor>
<conductor bicolor="true" bus="" cable="" color="#00aa00" color2="#ffff00" condsize="1" conductor_color="" conductor_section="2,5mm²" dash-size="4" displaytext="1" element1="{92274d83-04c4-49eb-83b6-7c8d7d89e638}" element1_label="PC1" element1_name="c" element2="{4d7f799d-2263-448a-9350-35237b5345f0}" element2_label="" element2_name="e" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" style="line-style: dashdotted" tension_protocol="" terminal1="{7c6f5395-9daf-5fe6-89e0-c5c4145e0f9c}" terminal2="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{521bfde2-4240-5095-8317-4234f14b1b50}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<segment length="9" orientation="vertical" />
<segment length="11" orientation="vertical" />
<segment length="272" orientation="horizontal" />
<segment length="1" orientation="vertical" />
<segment length="9" orientation="vertical" />
<sequentialNumbers />
</conductor>
<conductor bicolor="true" bus="" cable="" color="#00aa00" color2="#ffff00" condsize="1" conductor_color="" conductor_section="2,5mm²" dash-size="4" displaytext="1" element1="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element1_label="L1" element1_name="e" element2="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element2_label="L2" element2_name="e" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" rotation="270" style="line-style: dashdotted" tension_protocol="" terminal1="{696f18f9-9973-56d7-ad54-543740e0d6e1}" terminal2="{696f18f9-9973-56d7-ad54-543740e0d6e1}" terminalname1="" terminalname2="" text_color="#000000" type="multi" userx="360" usery="190" uuid="{e42f0149-8853-50e6-9097-264036a5dde0}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<segment length="9" orientation="horizontal" />
<segment length="15" orientation="horizontal" />
<segment length="60" orientation="vertical" />
<segment length="-15" orientation="horizontal" />
<segment length="-9" orientation="horizontal" />
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#0000ff" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element1_label="L1" element1_name="e" element2="{4c068000-8007-4a6d-82d1-21c157209335}" element2_label="10A" element2_name="Circuit-breaker" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" tension_protocol="" terminal1="{6c830253-af7a-5723-9fc2-304c81fe739f}" terminal2="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{cf7af2d6-2fce-52b1-856e-78ae7db37426}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="true" bus="" cable="" color="#00aa00" color2="#ffff00" condsize="1" conductor_color="" conductor_section="2,5mm²" dash-size="4" displaytext="1" element1="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element1_label="L2" element1_name="e" element2="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element2_label="L3" element2_name="e" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" rotation="270" style="line-style: dashdotted" tension_protocol="" terminal1="{696f18f9-9973-56d7-ad54-543740e0d6e1}" terminal2="{696f18f9-9973-56d7-ad54-543740e0d6e1}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{c53823e4-42d6-590e-a52a-52c98efa1be9}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<segment length="9" orientation="horizontal" />
<segment length="15" orientation="horizontal" />
<segment length="60" orientation="vertical" />
<segment length="-15" orientation="horizontal" />
<segment length="-9" orientation="horizontal" />
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#0000ff" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element1_label="L2" element1_name="e" element2="{4c068000-8007-4a6d-82d1-21c157209335}" element2_label="10A" element2_name="Circuit-breaker" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" rotation="270" tension_protocol="" terminal1="{6c830253-af7a-5723-9fc2-304c81fe739f}" terminal2="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" terminalname1="" terminalname2="" text_color="#000000" type="multi" userx="380" usery="180" uuid="{74cc2956-3b6d-5f78-be23-ddeffa4e1907}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<segment length="9" orientation="horizontal" />
<segment length="30" orientation="horizontal" />
<segment length="-60" orientation="vertical" />
<segment length="20" orientation="horizontal" />
<segment length="9" orientation="horizontal" />
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#0000ff" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element1_label="L3" element1_name="e" element2="{4c068000-8007-4a6d-82d1-21c157209335}" element2_label="10A" element2_name="Circuit-breaker" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" rotation="270" tension_protocol="" terminal1="{6c830253-af7a-5723-9fc2-304c81fe739f}" terminal2="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" terminalname1="" terminalname2="" text_color="#000000" type="multi" userx="380" usery="190" uuid="{ff5bed0e-6cda-5f72-b711-e13a957a027d}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<segment length="9" orientation="horizontal" />
<segment length="30" orientation="horizontal" />
<segment length="-120" orientation="vertical" />
<segment length="20" orientation="horizontal" />
<segment length="9" orientation="horizontal" />
<sequentialNumbers />
</conductor>
<conductor bicolor="true" bus="" cable="" color="#00aa00" color2="#ffff00" condsize="1" conductor_color="" conductor_section="2,5mm²" dash-size="4" displaytext="1" element1="{3ab3ddf0-c969-4e96-9981-d40d63f92306}" element1_label="L3" element1_name="e" element2="{d6d0d1d9-21cc-4757-8fbf-d44726eaf18d}" element2_label="PE" element2_name="e" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" style="line-style: dashdotted" tension_protocol="" terminal1="{696f18f9-9973-56d7-ad54-543740e0d6e1}" terminal2="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{7a6815bf-d956-52a4-b270-bb8aa3df8e89}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#0000ff" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{4c068000-8007-4a6d-82d1-21c157209335}" element1_label="10A" element1_name="Circuit-breaker" element2="{0b635509-0349-4517-93fd-1c602d4cd8dd}" element2_label="N" element2_name="Neutral source" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" tension_protocol="" terminal1="{6c830253-af7a-5723-9fc2-304c81fe739f}" terminal2="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{daea0830-ba4a-5e08-8fdc-751bcfe6f85f}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#0000ff" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{7c25df98-6c99-4dde-9080-a08214224b06}" element1_label="20A" element1_name="Circuit-breaker" element2="{6eb3bd36-847c-403f-87b0-c7a4b7e8327f}" element2_label="" element2_name="Neutral source" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" tension_protocol="" terminal1="{6c830253-af7a-5723-9fc2-304c81fe739f}" terminal2="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{97bc19fc-b224-5693-ba45-38f4ae9ed1bd}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="false" bus="" cable="" color="#0000ff" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6eb3bd36-847c-403f-87b0-c7a4b7e8327f}" element1_label="" element1_name="Neutral source" element2="{0b635509-0349-4517-93fd-1c602d4cd8dd}" element2_label="N" element2_name="Neutral source" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" rotation="270" tension_protocol="" terminal1="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminal2="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{a1214aa3-69d5-5490-8e86-3150665bd6b8}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
<conductor bicolor="true" bus="" cable="" color="#00aa00" color2="#ffff00" condsize="1" conductor_color="" conductor_section="2,5mm²" dash-size="4" displaytext="1" element1="{4d7f799d-2263-448a-9350-35237b5345f0}" element1_label="" element1_name="e" element2="{d6d0d1d9-21cc-4757-8fbf-d44726eaf18d}" element2_label="PE" element2_name="e" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="" numsize="9" onetextperfolio="0" rotation="270" style="line-style: dashdotted" tension_protocol="" terminal1="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminal2="{cf882328-3321-5cf9-ad83-ff420cb81927}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{9998cb90-3fb4-54af-b79a-069918ca8c8f}" vertical-alignment="AlignRight" vertirotatetext="0" x="0" y="0">
<sequentialNumbers />
</conductor>
</conductors>
<inputs>
<input font="MS Shell Dlg 2,8,-1,5,50,0,0,0,0,0,Normal" rotation="0" text="&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;&#10;&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;&#10;p, li { white-space: pre-wrap; }&#10;hr { height: 1px; border-width: 0; }&#10;li.unchecked::marker { content: &quot;\2610&quot;; }&#10;li.checked::marker { content: &quot;\2612&quot;; }&#10;&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;&#10;&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;L1 en simple allumage par S1&lt;/span&gt;&lt;/p&gt;&#10;&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;L2 et L3 en simple allumage par S2&lt;/span&gt;&lt;/p&gt;&#10;&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;1 PC 2p+T&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;" uuid="{946b5b47-981a-5743-b84e-d0a9fdb49584}" x="210" y="30" />
</inputs>
</diagram>
<collection>
<category name="import">
<names>
<name lang="ru">Импортированные элементы</name>
<name lang="en">Imported elements</name>
<name lang="it">Elementi importati</name>
<name lang="fr">Éléments importés</name>
<name lang="pl">Elementy importowane</name>
<name lang="es">Elementos importados</name>
<name lang="cs">Zavedené prvky</name>
</names>
<category name="sources">
<names>
<name lang="ar">مصادر</name>
<name lang="de">Quellen</name>
<name lang="ru">Источники</name>
<name lang="pt">Fontes</name>
<name lang="en">Sources</name>
<name lang="it">Sorgenti</name>
<name lang="fr">Sources</name>
<name lang="pl">Źródła zasilania</name>
<name lang="es">Fuentes</name>
<name lang="cs">Zdroje</name>
</names>
<category name="multifilaire">
<names>
<name lang="ar">متعددة الأسلاك</name>
<name lang="de">Mehradrig</name>
<name lang="ru">Многолинейные</name>
<name lang="pt">Multifilar</name>
<name lang="en">Multiline</name>
<name lang="it">Multifilari</name>
<name lang="fr">Multifilaire</name>
<name lang="pl">Schemat wielokreskowyowy</name>
<name lang="es">Multihilo</name>
<name lang="cs">Několik vedení</name>
</names>
<element name="pe.elmt">
<definition height="30" hotspot_x="25" hotspot_y="15" type="element" version="0.3" width="40">
<names>
<name lang="ru">Нейтраль</name>
<name lang="fr">Protection Electrique</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="10" y1="0" y2="0" />
<input size="9" tagg="label" text="PE" x="-23" y="0" />
<terminal orientation="e" uuid="{cf882328-3321-5cf9-ad83-ff420cb81927}" x="10" y="0" />
</description>
</definition>
</element>
<element name="src_p.elmt">
<definition height="30" hotspot_x="15" hotspot_y="15" type="element" version="0.3" width="30">
<names>
<name lang="ar">مصدر وجه</name>
<name lang="de">Phase</name>
<name lang="ru">Фаза</name>
<name lang="pt">Fonte de fase</name>
<name lang="en">Phase source</name>
<name lang="it">Sorgente fase</name>
<name lang="fr">Source phase</name>
<name lang="pl">Przewód liniowy</name>
<name lang="es">Fuente fase</name>
<name lang="cs">Fázový zdroj</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="10" y1="0" y2="0" />
<input size="9" tagg="label" text="L" x="-16" y="0" />
<terminal orientation="e" uuid="{cf882328-3321-5cf9-ad83-ff420cb81927}" x="10" y="0" />
</description>
</definition>
</element>
<element name="src_n.elmt">
<definition height="30" hotspot_x="15" hotspot_y="15" type="element" version="0.3" width="30">
<names>
<name lang="ar">مصدر محايد</name>
<name lang="de">Neutralleiter</name>
<name lang="ru">Нейтраль</name>
<name lang="pt">Fonte de neutro</name>
<name lang="en">Neutral source</name>
<name lang="it">Sorgente neutro</name>
<name lang="fr">Source neutre</name>
<name lang="pl">Przewód neutralny</name>
<name lang="es">Fuente neutro</name>
<name lang="cs">Nulový zdroj</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="10" y1="0" y2="0" />
<input size="9" tagg="label" text="N" x="-17" y="0" />
<terminal orientation="e" uuid="{cf882328-3321-5cf9-ad83-ff420cb81927}" x="10" y="0" />
</description>
</definition>
</element>
</category>
</category>
<category name="protections">
<names>
<name lang="ar">حمايات</name>
<name lang="de">Sicherheit</name>
<name lang="ru">Защитные уст-ва</name>
<name lang="pt">Protecções</name>
<name lang="en">Protections</name>
<name lang="it">Protezioni</name>
<name lang="fr">Protections</name>
<name lang="pl">Zabezpieczenia</name>
<name lang="es">Protecciones</name>
<name lang="cs">Ochrany</name>
</names>
<category name="disjoncteurs">
<names>
<name lang="ar">قواطع</name>
<name lang="de">Lastschalter</name>
<name lang="ru">Выключатели</name>
<name lang="pt">Disjuntores</name>
<name lang="en">Circuit-breakers</name>
<name lang="it">Interruttori</name>
<name lang="fr">Disjoncteurs</name>
<name lang="pl">Wyłączniki</name>
<name lang="es">Disyuntores</name>
<name lang="cs">Jističe</name>
</names>
<element name="disjonct-m.elmt">
<definition height="60" hotspot_x="0" hotspot_y="25" orientation="dnny" type="element" version="0.3" width="30">
<names>
<name lang="ar">قاطع أحدي القطب</name>
<name lang="en">Circuit-breaker</name>
<name lang="fr">Disjoncteur unipolaire</name>
<name lang="pl">Wyłącznik</name>
<name lang="cs">Jednopólový jistič</name>
<name lang="it">Int. Aut. Magneto-termico 1P</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="10" x2="10" y1="26" y2="30" />
<polygon antialias="true" closed="false" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="10" x2="10" x3="5" y1="13" y2="10" y3="-10" />
<input size="9" tagg="label" text="_" x="16" y="3.6666" />
<arc angle="-180" antialias="true" height="5" start="90" style="line-style:normal;line-weight:normal;filling:none;color:black" width="8" x="6" y="20.5" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="10" x2="10" y1="18" y2="20" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="10" x2="15" y1="18" y2="18" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="15" x2="15" y1="13" y2="18" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="10" x2="15" y1="13" y2="13" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="10" x2="10" y1="-20" y2="-10" />
<line antialias="true" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="12" x2="8" y1="-12" y2="-8" />
<line antialias="true" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="8" x2="12" y1="-12" y2="-8" />
<terminal orientation="s" uuid="{3a9a1158-fcda-555c-ad99-d6b92a4f04a7}" x="10" y="30" />
<terminal orientation="n" uuid="{ab69f5f1-346f-5514-b8f4-e23282dd76ad}" x="10" y="-20" />
</description>
</definition>
</element>
</category>
</category>
<category name="contacts">
<names>
<name lang="ar">ملامسات</name>
<name lang="de">Kontakte</name>
<name lang="ru">Контакты</name>
<name lang="pt">Contactos</name>
<name lang="en">Contacts</name>
<name lang="it">Contatti</name>
<name lang="fr">Contacts</name>
<name lang="pl">Zestyki</name>
<name lang="es">Terminales</name>
<name lang="cs">Kontakty</name>
</names>
<category name="interrupteurs">
<names>
<name lang="ar">مفاتيح</name>
<name lang="de">Schalter</name>
<name lang="ru">переключателей</name>
<name lang="pt">Interruptores</name>
<name lang="en">Switches</name>
<name lang="it">Interruttori</name>
<name lang="fr">Interrupteurs</name>
<name lang="pl">Przełącznik</name>
<name lang="es">Interruptores</name>
<name lang="cs">Spínače</name>
</names>
<element name="interrupteur.elmt">
<definition height="40" hotspot_x="10" hotspot_y="20" orientation="dnny" type="element" version="0.3" width="20">
<names>
<name lang="ar">مفتاح</name>
<name lang="de">Schalter</name>
<name lang="ru">Переключатель</name>
<name lang="pt">Interruptor</name>
<name lang="en">Switch</name>
<name lang="it">Interruttore</name>
<name lang="fr">Interrupteur</name>
<name lang="pl">Łącznik</name>
<name lang="es">Interruptor</name>
<name lang="cs">Spínač</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<polygon antialias="true" closed="false" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="-5" x2="0" x3="0" y1="-10" y2="10" y3="20" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="-20" y2="-12.5" />
<input size="9" tagg="label" text="_" x="2.75" y="3" />
<circle antialias="true" diameter="3.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x="-1.75" y="-11.75" />
<terminal orientation="n" uuid="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" x="0" y="-21" />
<terminal orientation="s" uuid="{6c830253-af7a-5723-9fc2-304c81fe739f}" x="0" y="21" />
</description>
</definition>
</element>
</category>
</category>
<element name="lampe_pe.elmt">
<definition height="50" hotspot_x="15" hotspot_y="25" orientation="dnny" type="element" version="0.3" width="50">
<names>
<name lang="fr">Point Lumineux</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<circle antialias="true" diameter="20" style="line-style:normal;line-weight:normal;filling:none;color:black" x="-10" y="-10" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="7.03" x2="-7.03" y1="-7.03" y2="7.03" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="-7.03" x2="7.03" y1="-7.03" y2="7.03" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="-20" y2="-10" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="10" y2="20" />
<input size="9" tagg="label" text="L_" x="10" y="1" />
<arc angle="-90" antialias="true" height="16" start="270" style="line-style:normal;line-weight:normal;filling:none;color:black" width="15" x="-13" y="-3" />
<terminal orientation="n" uuid="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" x="0" y="-21" />
<terminal orientation="s" uuid="{6c830253-af7a-5723-9fc2-304c81fe739f}" x="0" y="21" />
<terminal orientation="s" uuid="{696f18f9-9973-56d7-ad54-543740e0d6e1}" x="-10" y="16" />
</description>
</definition>
</element>
<element name="disj_neutre.elmt">
<definition height="50" hotspot_x="10" hotspot_y="25" orientation="dnny" type="element" version="0.3" width="30">
<names>
<name lang="ar">قاطع</name>
<name lang="de">Lastschalter</name>
<name lang="ru">Выключатель</name>
<name lang="pt">Disjuntor</name>
<name lang="en">Circuit-breaker</name>
<name lang="it">Sezionatore 1P</name>
<name lang="fr">Disjoncteur</name>
<name lang="pl">Wyłącznik</name>
<name lang="es">Disyuntor</name>
<name lang="cs">Jistič</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="-20" y2="-10" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="10" y2="20" />
<line antialias="true" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="-5" y1="-10" y2="10" />
<input size="9" tagg="label" text="_" x="6" y="3" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="2" x2="-2" y1="14" y2="10" />
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="-2" x2="2" y1="14" y2="10" />
<terminal orientation="n" uuid="{cef8a446-540b-53f6-a474-6cfcb20d1abb}" x="0" y="-21" />
<terminal orientation="s" uuid="{6c830253-af7a-5723-9fc2-304c81fe739f}" x="0" y="21" />
</description>
</definition>
</element>
<element name="pc.elmt">
<definition height="60" hotspot_x="35" hotspot_y="40" type="element" version="0.3" width="50">
<names>
<name lang="ru">Нейтраль</name>
<name lang="fr">Protection Electrique</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<arc angle="-90" antialias="true" height="18" start="270" style="line-style:normal;line-weight:normal;filling:none;color:black" width="18" x="-24" y="-9" />
<arc angle="-90" antialias="true" height="18" start="180" style="line-style:normal;line-weight:normal;filling:none;color:black" width="18" x="-24" y="-9" />
<arc angle="-90" antialias="true" height="18" start="90" style="line-style:normal;line-weight:normal;filling:none;color:black" width="18" x="-18" y="-9" />
<input size="9" tagg="label" text="_" x="-20" y="-22" />
<arc angle="-90" antialias="true" height="18" start="0" style="line-style:normal;line-weight:normal;filling:none;color:black" width="18" x="-18" y="-9" />
<rect antialias="false" height="10" style="line-style:normal;line-weight:normal;filling:black;color:black" width="4" x="-14" y="-5" />
<terminal orientation="e" uuid="{5e7df160-501d-5ae1-9dc2-59d2ba0daf9a}" x="5" y="0" />
<terminal orientation="w" uuid="{0c6b6b61-7e22-53c9-8e34-25f816145de3}" x="-29" y="0" />
<terminal orientation="s" uuid="{7c6f5395-9daf-5fe6-89e0-c5c4145e0f9c}" x="-12" y="10" />
</description>
</definition>
</element>
</category>
</collection>
</project>
@@ -0,0 +1,59 @@
<project title="Anchored static texts" uuid="{6c1a2b3d-1251-4c1e-9e57-3f0d0f0a7b10}" version="0.200.1">
<properties>
<property name="saveddate" show="1">10/2/26</property>
<property name="saveddate-eu" show="1">02-10-2026</property>
<property name="saveddate-us" show="1">2026-10-02</property>
<property name="savedfilename" show="1">static_text_anchor</property>
<property name="savedtime" show="1">18:39</property>
</properties>
<usage enabled="true" time_spent="0"/>
<newdiagrams>
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<report label="%f-%l%c"/>
<xrefs>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
</xrefs>
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
<folio_autonums/>
<element_autonums current_autonum="" freeze_new_elements="false"/>
<guides/>
</newdiagrams>
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{6c1a2b3d-1251-4c1e-9e57-3f0d0f0a7a01}" version="0.200.1-dev">
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/anchor.elmt" uuid="{6c1a2b3d-1251-4c1e-9e57-3f0d0f0a7a02}" x="300" y="300" z="10">
<terminals/>
<inputs/>
<dynamic_texts/>
<texts_groups/>
</element>
</elements>
</diagram>
<collection>
<category name="import">
<names>
<name lang="en">Imported elements</name>
</names>
<element name="anchor.elmt">
<definition height="80" hotspot_x="100" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="200">
<uuid uuid="{6c1a2b3d-1251-4c1e-9e57-3f0d0f0a7a05}"/>
<names>
<name lang="en">Anchored static texts</name>
</names>
<description>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="0" y2="60"/>
<text text="GPIO21 PCM_DOUT" x="0" y="10" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" color="#000000" rotation="0"/>
<text text="GPIO21 PCM_DOUT" x="0" y="20" Halignment="AlignRight" anchor="alignment" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" color="#000000" rotation="0"/>
<text text="GPIO21 PCM_DOUT" x="0" y="30" Halignment="AlignHCenter" anchor="alignment" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" color="#000000" rotation="0"/>
<text text="GPIO21 PCM_DOUT" x="0" y="40" Halignment="AlignRight" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" color="#000000" rotation="0"/>
<text text="GPIO21 PCM_DOUT" x="0" y="50" anchor="alignment" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" color="#000000" rotation="0"/>
</description>
</definition>
</element>
</category>
</collection>
</project>
+229
View File
@@ -0,0 +1,229 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QApplication>
#include <QComboBox>
#include <QTemporaryDir>
#include <QSettings>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsSceneHoverEvent>
#include <QStandardItemModel>
#include <cstdio>
#include <stdexcept>
#include <algorithm>
#include "../../sources/qetapp.h"
#include "../../sources/qetdiagrameditor.h"
#include "../../sources/qetproject.h"
#include "../../sources/qetresult.h"
#include "../../sources/qetmessagebox.h"
#include "../../sources/projectview.h"
#include "../../sources/diagram.h"
#include "../../sources/utils/qetutils.h"
#include "../../sources/ui/diagrameditorhandlersizewidget.h"
#include "../../sources/QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "../../sources/qetgraphicsitem/independenttextitem.h"
#include "../../sources/qetgraphicsitem/dynamicelementtextitem.h"
#include "../../sources/qetgraphicsitem/diagramimageitem.h"
#include "../../sources/qetgraphicsitem/qetshapeitem.h"
#include "../../sources/editor/ui/qetelementeditor.h"
#include "../../sources/editor/elementscene.h"
#include "../../sources/editor/graphicspart/partrectangle.h"
#include "../../sources/editor/graphicspart/partdynamictextfield.h"
#include "../../sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "../../sources/qetgraphicsitem/conductor.h"
static void check(bool value,const char *why){if(!value)throw std::runtime_error(why);}
static QString canonicalProjectXml(QDomDocument document) {
// Scene index updates can reorder equal-z conductors. Compare their
// complete serialized content in UUID order, rather than scene order.
const auto groups=document.elementsByTagName("conductors");
for(int i=0;i<groups.size();++i) {
auto group=groups.at(i).toElement();QList<QDomElement> wires;
for(auto wire=group.firstChildElement("conductor");!wire.isNull();wire=wire.nextSiblingElement("conductor"))wires<<wire;
std::sort(wires.begin(),wires.end(),[](const QDomElement &a,const QDomElement &b){return a.attribute("uuid")<b.attribute("uuid");});
for(auto wire:wires)group.appendChild(wire);
}
return document.toString();
}
static void mouse(Diagram *scene,QGraphicsItem *item,QEvent::Type type,const QPointF &point,const QPointF *press=nullptr) {
QGraphicsSceneMouseEvent event(type);event.setButton(Qt::LeftButton);
event.setButtons(type==QEvent::GraphicsSceneMouseRelease?Qt::NoButton:Qt::LeftButton);
event.setScenePos(point);event.setPos(item->mapFromScene(point));
event.setButtonDownScenePos(Qt::LeftButton,press?*press:point);scene->sendEvent(item,&event);
}
static QetGraphicsHandlerItem *bottomRightHandle(Diagram *scene) {
QetGraphicsHandlerItem *found=nullptr;
for(auto *item:scene->items())if(item->type()==QetGraphicsHandlerItem::Type&&item->isVisible()) {
auto *handle=qgraphicsitem_cast<QetGraphicsHandlerItem *>(item);
if(!found||handle->scenePos().x()+handle->scenePos().y()>found->scenePos().x()+found->scenePos().y())found=handle;
}
check(found,"mouse gesture handle present");return found;
}
static void drag(Diagram *scene,QGraphicsItem *item,const QPointF &end) {
const auto start=item->scenePos();
mouse(scene,item,QEvent::GraphicsSceneMousePress,start);
mouse(scene,item,QEvent::GraphicsSceneMouseMove,end,&start);
mouse(scene,item,QEvent::GraphicsSceneMouseRelease,end,&start);
}
int main(int argc,char **argv) {
const QString fixture=QString::fromLocal8Bit(argv[1]);
const QString report=QString::fromLocal8Bit(argv[2]);
QTemporaryDir isolated;
QByteArray config="--config-dir="+isolated.path().toUtf8();
QByteArray data="--data-dir="+isolated.path().toUtf8();
QByteArray collection="--common-elements-dir="+isolated.path().toUtf8();
char *args[]={argv[0],config.data(),data.data(),collection.data()};int count=4;
QApplication app(count,args);
std::freopen(report.toLocal8Bit().constData(),"w",stdout);
std::setbuf(stdout,nullptr);
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat,QSettings::UserScope,isolated.path());
QETProject::setBackupEnabled(false);QET::QetMessageBox::setNonInteractive(true);
try {
auto *qet=new QETApp; Q_UNUSED(qet);
auto editors=QETApp::diagramEditors();check(!editors.isEmpty(),"editor created");
auto *editor=editors.first();
auto *project=new QETProject(fixture);check(project->state()==QETProject::Ok,"fixture loaded");
editor->addProjectView(new ProjectView(project));
auto *scene=project->diagrams().first();
auto widgets=editor->findChildren<DiagramEditorHandlerSizeWidget *>();check(!widgets.isEmpty(),"toolbar widget present");
auto *combo=widgets.first()->findChild<QComboBox *>();check(combo&&combo->count()==6,"six choices");
check(combo->currentIndex()==3&&editor->property("graphics_handler_size").toReal()==10,"default x1");
auto *free=new IndependentTextItem;free->setPlainText("Mouse placement");scene->addItem(free);free->setSelected(true);
QPixmap pixmap(30,20);pixmap.fill(Qt::red);auto *image=new DiagramImageItem(pixmap);scene->addItem(image);image->setSelected(true);
auto *shape=new QetShapeItem({100,100},{150,140},QetShapeItem::Rectangle);scene->addItem(shape);shape->setSelected(true);
auto *attached=scene->elements().first()->dynamicTextItems().first();attached->setSelected(true);
auto *background=project->addNewDiagram();
auto *backgroundHandle=new QetGraphicsHandlerItem;
background->addItem(backgroundHandle);
QApplication::processEvents();
const QString xml=project->toXml().toString();
const int undoCount=scene->undoStack().count();const int undoIndex=scene->undoStack().index();
const bool dirty=project->projectWasModified();
const qreal sizes[]={2.5,5,7.5,10,20,30};
for(int i=0;i<6;++i) {
combo->setCurrentIndex(i);QApplication::processEvents();
check(editor->property("graphics_handler_size").toReal()==sizes[i],"fractional editor property");
check(QETUtils::graphicsHandlerSize(image)==sizes[i],"new handle size lookup");
check(backgroundHandle->property("currentSize").toReal()==sizes[i],"inactive folio handles updated");
int handles=0;for(auto *item:scene->items())if(item->type()==QetGraphicsHandlerItem::Type){
++handles;auto *handle=qgraphicsitem_cast<QetGraphicsHandlerItem *>(item);
check(handle->property("currentSize").toReal()==sizes[i],"existing handles updated immediately");
}
check(handles>=8,"multiple selected object handles tested");
image->setSelected(false);image->setSelected(true);
for(auto *item:scene->items())if(item->type()==QetGraphicsHandlerItem::Type)
check(qgraphicsitem_cast<QetGraphicsHandlerItem *>(item)->property("currentSize").toReal()==sizes[i],"recreated handles retain fractions");
DiagramEditorHandlerSizeWidget recreated(editor);
check(recreated.findChild<QComboBox *>()->currentIndex()==i,"toolbar recreation preserves chosen size");
check(project->toXml().toString()==xml,"handle size leaves project XML unchanged");
check(scene->undoStack().count()==undoCount&&scene->undoStack().index()==undoIndex,"handle size leaves undo stack unchanged");
check(project->projectWasModified()==dirty,"handle size leaves dirty state unchanged");
}
for(int i=0;i<6;++i) {
combo->setCurrentIndex(i);
scene->clearSelection();
auto *picture=new DiagramImageItem(pixmap);scene->addItem(picture);picture->setPos(400,400);picture->setSelected(true);
auto *handle=bottomRightHandle(scene);
check(handle->property("currentSize").toReal()==sizes[i],"image mouse handle size");
drag(scene,handle,handle->scenePos()+QPointF(20,20));
check(picture->scaleFactorX()!=1,"image resized by mouse events");
const qreal changed=picture->scaleFactorX();scene->undoStack().undo();
check(picture->scaleFactorX()==1,"image resize undo");scene->undoStack().redo();
check(picture->scaleFactorX()==changed,"image resize redo");scene->undoStack().undo();
const auto centre=picture->mapToScene(picture->imageRect().center());
mouse(scene,picture,QEvent::GraphicsSceneMousePress,centre);
mouse(scene,picture,QEvent::GraphicsSceneMouseRelease,centre);
handle=bottomRightHandle(scene);
const auto pivot=picture->mapToScene(picture->pivot());QTransform turn;turn.rotate(30);
drag(scene,handle,pivot+turn.map(handle->scenePos()-pivot));
check(qAbs(picture->rotationAngle())>1,"image rotated by mouse events");
const qreal angle=picture->rotationAngle();scene->undoStack().undo();
check(qAbs(picture->rotationAngle())<0.001,"image rotation undo");scene->undoStack().redo();
check(qAbs(picture->rotationAngle()-angle)<0.001,"image rotation redo");scene->undoStack().undo();
scene->undoStack().clear();delete picture;
auto *rectangle=new QetShapeItem({0,0},{50,40},QetShapeItem::Rectangle);scene->addItem(rectangle);rectangle->setPos(400,400);rectangle->setSelected(true);
const auto rect=rectangle->rect();handle=bottomRightHandle(scene);
drag(scene,handle,handle->scenePos()+QPointF(20,20));check(rectangle->rect()!=rect,"shape resized by mouse events");
const auto resized=rectangle->rect();scene->undoStack().undo();check(rectangle->rect()==rect,"shape resize undo");
scene->undoStack().redo();check(rectangle->rect()==resized,"shape resize redo");scene->undoStack().undo();
for(int click=0;click<2;++click) {
const auto middle=rectangle->mapToScene(rect.center());
mouse(scene,rectangle,QEvent::GraphicsSceneMousePress,middle);mouse(scene,rectangle,QEvent::GraphicsSceneMouseRelease,middle);
}
handle=bottomRightHandle(scene);const auto shapePivot=rectangle->mapToScene(rectangle->pivot());
drag(scene,handle,shapePivot+turn.map(handle->scenePos()-shapePivot));
check(qAbs(rectangle->rotation())>1,"shape rotated by mouse events");
const qreal shapeAngle=rectangle->rotation();scene->undoStack().undo();check(qAbs(rectangle->rotation())<0.001,"shape rotation undo");
scene->undoStack().redo();check(qAbs(rectangle->rotation()-shapeAngle)<0.001,"shape rotation redo");scene->undoStack().undo();
scene->undoStack().clear();
auto *companion=new DiagramImageItem(pixmap);scene->addItem(companion);companion->setPos(600,400);companion->setSelected(true);
rectangle->setSelected(true);
const auto rectanglePos=rectangle->pos();const auto companionPos=companion->pos();
const auto start=companion->mapToScene(companion->imageRect().center());const auto end=start+QPointF(20,20);
mouse(scene,companion,QEvent::GraphicsSceneMousePress,start);
mouse(scene,companion,QEvent::GraphicsSceneMouseMove,end,&start);
mouse(scene,companion,QEvent::GraphicsSceneMouseRelease,end,&start);
check(companion->pos()==companionPos+QPointF(20,20)&&rectangle->pos()==rectanglePos+QPointF(20,20),"multiple objects moved by mouse events");
scene->undoStack().undo();check(companion->pos()==companionPos&&rectangle->pos()==rectanglePos,"multiple object movement undo");
scene->undoStack().redo();check(companion->pos()==companionPos+QPointF(20,20)&&rectangle->pos()==rectanglePos+QPointF(20,20),"multiple object movement redo");
scene->undoStack().undo();scene->undoStack().clear();delete companion;delete rectangle;
}
std::puts("PASS: image and rectangle mouse resize/rotation, multiple object movement, and undo/redo at all six sizes");
project->setFilePath(QString::fromLocal8Bit(argv[3])+"/handles.qet");
check(project->write().isOk(),"save after handle size changes");
QETProject reopened(project->filePath());check(reopened.state()==QETProject::Ok,"reopen after handle size changes");
bool imageFound=false,textFound=false;
for(auto *item:reopened.diagrams().first()->items()) {
if(auto *loaded=qgraphicsitem_cast<DiagramImageItem *>(item))imageFound|=loaded->pixmap().size()==pixmap.size();
if(auto *loaded=qgraphicsitem_cast<IndependentTextItem *>(item))textFound|=loaded->toPlainText()==free->toPlainText();
}
check(imageFound&&textFound,"object content survives reopen");
auto *elementEditor=new QETElementEditor;elementEditor->show();
auto *elementScene=elementEditor->elementScene();
auto *part=new PartRectangle(elementEditor);part->setRect({0,0,50,40});elementScene->addItems({part});part->setSelected(true);
QApplication::processEvents();
auto checkElementHandles=[&] {
int count=0;
for(auto *item:elementScene->items())if(item->type()==QetGraphicsHandlerItem::Type&&item->isVisible()) {
++count;check(qgraphicsitem_cast<QetGraphicsHandlerItem *>(item)->property("currentSize").toReal()==10,"element editor retains default handle size");
}
check(count>=4,"element editor shared handles present");
};
for(int i=0;i<6;++i){combo->setCurrentIndex(i);checkElementHandles();check(part->rect()==QRectF(0,0,50,40),"element primitive geometry unchanged");}
elementScene->clearSelection();
auto *field=new PartDynamicTextField(elementEditor);field->setPlainText("Element field");elementScene->addItems({field});field->setSelected(true);
QApplication::processEvents();checkElementHandles();
std::puts("PASS: element editor rectangle and dynamic field keep their default shared handles");
QStandardItemModel model(2,2);model.setData(model.index(0,0),"Table");
auto *table=new QetGraphicsTableItem;table->setModel(&model);scene->addItem(table);
const auto tableSize=table->size();
for(int i=0;i<6;++i) {
combo->setCurrentIndex(i);
QGraphicsSceneHoverEvent enter(QEvent::GraphicsSceneHoverEnter);scene->sendEvent(table,&enter);
check(bottomRightHandle(scene)->property("currentSize").toReal()==sizes[i],"table hover handle retains fraction");
check(table->size()==tableSize,"table dimensions unchanged");
QGraphicsSceneHoverEvent leave(QEvent::GraphicsSceneHoverLeave);scene->sendEvent(table,&leave);
}
delete table;std::puts("PASS: table hover handles retain all six sizes without changing the table");
const auto wireFixture=QFileInfo(fixture).dir().absoluteFilePath("../../../examples/tremie_vibrante.qet");
auto *wireProject=new QETProject(wireFixture);check(wireProject->state()==QETProject::Ok,"conductor fixture opens");
editor->addProjectView(new ProjectView(wireProject));
Diagram *wireScene=nullptr;for(auto *diagram:wireProject->diagrams())if(!diagram->conductors().isEmpty()){wireScene=diagram;break;}
check(wireScene,"conductor scene present");auto *wire=wireScene->conductors().first();
wire->setSelected(true);QApplication::processEvents();
const auto wireXml=canonicalProjectXml(wireProject->toXml());
for(int i=0;i<6;++i) {
combo->setCurrentIndex(i);wire->setSelected(false);wire->setSelected(true);
int count=0;for(auto *item:wireScene->items())if(item->type()==QetGraphicsHandlerItem::Type) {
++count;check(qgraphicsitem_cast<QetGraphicsHandlerItem *>(item)->property("currentSize").toReal()==sizes[i],"conductor handles retain fractions");
}
check(count>0,"conductor handles present");
const auto after=canonicalProjectXml(wireProject->toXml());
check(after==wireXml,"conductor project content unchanged");
}
std::puts("PASS: conductor handles retain all six sizes without changing their project");
std::puts("PASS: six toolbar choices, default, fractions, existing/recreated handles, multi-selection, toolbar recreation, unchanged XML/undo/dirty state");
}catch(const std::exception &error){std::printf("FAIL: %s\n",error.what());std::fflush(stdout);std::_Exit(1);}
std::fflush(stdout);std::_Exit(0);
}
@@ -66,8 +66,8 @@ int main(int argc, char **argv)
const QByteArray pristine=png(image->pixmap()); const auto transform=image->transform(); const auto bounds=image->boundingRect();
ImagePropertiesWidget widget(image); auto *box=widget.findChild<QCheckBox *>("m_adapt_to_dark_theme_cb");
check(box && !box->isChecked(),"unchecked properties default");
check(box->text()==QString::fromUtf8("Adapter l’image au thème sombre"),"checkbox wording");
check(box->toolTip()==QString::fromUtf8("Adapte les couleurs à l’affichage sombre uniquement. L’image originale, les exports et les impressions restent inchangés."),"tooltip wording");
check(box->text()==QStringLiteral("Adapt the picture to the dark theme"),"checkbox wording");
check(box->toolTip()==QStringLiteral("Adapts the colors for the dark display only. The original picture, exports and printouts stay unchanged."),"tooltip wording");
box->setChecked(true); check(image->adaptToDarkTheme(),"preview"); widget.reset(); check(!image->adaptToDarkTheme(),"reset cancels preview");
diagram->undoStack().clear(); box->setChecked(true); widget.apply(); check(image->adaptToDarkTheme(),"apply");
diagram->undoStack().undo(); check(!image->adaptToDarkTheme()&&!box->isChecked(),"undo updates checkbox");
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Link the handle-size integration probe using an existing Ninja app build.
Reuses app objects without duplicating the app or introducing a core-library
refactor. No application source or existing executable is replaced. The
generated manifest, probe binary, reports and export fixtures stay in build/.
"""
import argparse
import pathlib
import re
import shutil
import subprocess
import sys
parser = argparse.ArgumentParser()
parser.add_argument('--build', required=True)
parser.add_argument('--binary', required=True)
parser.add_argument('--source', required=True)
parser.add_argument('--fixture', required=True)
args = parser.parse_args()
build = pathlib.Path(args.build).resolve()
binary = pathlib.Path(args.binary).resolve()
source = pathlib.Path(args.source).resolve()
fixture = pathlib.Path(args.fixture).resolve()
manifest = (build / 'build.ninja').read_text(encoding='utf-8')
match = re.search(r'^build (\S*?/sources/main\.cpp\.(?:obj|o)):', manifest, re.MULTILINE)
if not match:
sys.exit('Cannot find the application main object in the Ninja build.')
main_object = match.group(1)
probe_object = main_object.replace('main.cpp.', 'handle_size_probe.cpp.')
probe_binary = binary.with_name('handle_size_probe' + binary.suffix)
def escape(path):
return pathlib.Path(path).as_posix().replace('$', '$$').replace(':', '$:').replace(' ', '$ ')
main_source = source.parents[2] / 'sources' / 'main.cpp'
manifest = manifest.replace(main_object, probe_object)
manifest = manifest.replace(escape(main_source), escape(source))
if binary.suffix:
manifest = manifest.replace(binary.name, probe_binary.name)
else:
old_target = escape(binary.name if binary.parent == build else binary)
new_target = escape(probe_binary.name if probe_binary.parent == build else probe_binary)
manifest = manifest.replace('build ' + old_target + ':', 'build ' + new_target + ':')
old_file = binary.name if binary.parent == build else binary.as_posix()
new_file = probe_binary.name if probe_binary.parent == build else probe_binary.as_posix()
manifest = re.sub(r'^ TARGET_FILE = ' + re.escape(old_file) + r'$',
' TARGET_FILE = ' + new_file, manifest, flags=re.MULTILINE)
probe_manifest = build / 'handle-size-probe.ninja'
probe_manifest.write_text(manifest, encoding='utf-8')
ninja = shutil.which('ninja')
if not ninja:
sys.exit('Ninja is required for the integration probe.')
target = probe_binary.name if probe_binary.parent == build else str(probe_binary)
subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], check=True)
output = build / 'handle-size'
output.mkdir(exist_ok=True)
report = output / 'results.txt'
result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=90)
if report.exists():
print(report.read_text(encoding='utf-8', errors='replace'))
sys.exit(result.returncode)
+132
View File
@@ -2,6 +2,7 @@
#include <QTemporaryDir>
#include "utils/configprofile.h"
#include "borderproperties.h"
class tst_configprofile : public QObject
{
@@ -17,6 +18,7 @@ private slots:
QVERIFY(ConfigProfile::isLocalKey("diagrameditor/geometry"));
QVERIFY(ConfigProfile::isLocalKey("elementeditor/state"));
QVERIFY(ConfigProfile::isLocalKey("projects-recentfiles/file1"));
QVERIFY(ConfigProfile::isLocalKey("dialoggeometry/ConfigDialog"));
QVERIFY(ConfigProfile::isLocalKey(ConfigProfile::marker_key));
QVERIFY(!ConfigProfile::isLocalKey("diagrameditor/Xgrid"));
QVERIFY(!ConfigProfile::isLocalKey("shortcuts/mainwindow.fullscreen"));
@@ -31,6 +33,7 @@ private slots:
live.setValue("lang", "de");
live.setValue("diagrameditor/geometry", QByteArray("xyz"));
live.setValue("projects-recentfiles/file1", "/home/a/b.qet");
live.setValue("dialoggeometry/ConfigDialog", QByteArray("xyz"));
QSettings file(path("profile1.conf"), QSettings::IniFormat);
QCOMPARE(ConfigProfile::exportTo(live, file), 2);
@@ -38,6 +41,7 @@ private slots:
QCOMPARE(file.value("diagrameditor/Xgrid").toInt(), 7);
QVERIFY(!file.contains("diagrameditor/geometry"));
QVERIFY(!file.contains("projects-recentfiles/file1"));
QVERIFY(!file.contains("dialoggeometry/ConfigDialog"));
}
// Import replaces every setting, removes the ones the profile does not
@@ -87,6 +91,134 @@ private slots:
QCOMPARE(b.value(k), a.value(k));
}
// Every key belongs to one part; keys nobody listed are Other.
void partOfKeys()
{
using P = ConfigProfile::Part;
QCOMPARE(ConfigProfile::partOf("shortcuts/mainwindow.fullscreen"), P::Controls);
QCOMPARE(ConfigProfile::partOf("toolbars/icon_size"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/toolbars/diagram"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/custom_toolbars/names"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/shortcut_bar/canvas"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/gestures/directions"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/mouse_gestures"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/context_toolbar"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagramview/gestures"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/defaultconductortype"), P::NewProject);
QCOMPARE(ConfigProfile::partOf("diagrameditor/defaultreportlabel"), P::NewProject);
QCOMPARE(ConfigProfile::partOf("diagrameditor/defaultxrefcoil"), P::NewProject);
QCOMPARE(ConfigProfile::partOf("diagrameditor/defaultguides/1/position"), P::NewProject);
QCOMPARE(ConfigProfile::partOf("autonum/conductor/current"), P::NewProject);
QCOMPARE(ConfigProfile::partOf("elements-collections/custom-collection-path"), P::Folders);
QCOMPARE(ConfigProfile::partOf("elements-collections/macros-path"), P::Folders);
QCOMPARE(ConfigProfile::partOf("diagrameditor/Xgrid"), P::Other);
QCOMPARE(ConfigProfile::partOf("diagrameditor/auto_break_conductor"), P::Other);
QCOMPARE(ConfigProfile::partOf("lang"), P::Other);
QCOMPARE(ConfigProfile::partOf("customColors"), P::Other);
}
// What the New project page writes for the folio size is the
// NewProject part, whatever key names BorderProperties uses.
void folioDefaultsAreNewProject()
{
QSettings live(path("live_border.conf"), QSettings::IniFormat);
BorderProperties().toSettings(live, "diagrameditor/default");
const QStringList keys = live.allKeys();
QVERIFY(!keys.isEmpty());
for (const QString &key : keys)
QCOMPARE(ConfigProfile::partOf(key), ConfigProfile::Part::NewProject);
}
// A file of some parts holds only their keys, never the folders, and
// is format 2 so that an older QElectroTech refuses it.
void exportSomeParts()
{
QSettings live(path("live4.conf"), QSettings::IniFormat);
live.setValue("shortcuts/mainwindow.fullscreen", "F11");
live.setValue("diagrameditor/defaultconductortype", "Multi");
live.setValue("diagrameditor/Xgrid", 7);
live.setValue("elements-collections/custom-collection-path", "/home/a/sym");
QSettings file(path("profile4.conf"), QSettings::IniFormat);
QCOMPARE(ConfigProfile::exportTo(live, file, {ConfigProfile::Part::NewProject}), 1);
QCOMPARE(file.value(ConfigProfile::marker_key).toInt(), 2);
QVERIFY(ConfigProfile::isProfile(file));
QCOMPARE(ConfigProfile::partsOf(file), QList<ConfigProfile::Part>{ConfigProfile::Part::NewProject});
QVERIFY(file.contains("diagrameditor/defaultconductortype"));
QVERIFY(!file.contains("shortcuts/mainwindow.fullscreen"));
QVERIFY(!file.contains("diagrameditor/Xgrid"));
QVERIFY(!file.contains("elements-collections/custom-collection-path"));
//Other without the two others: still no folders
QSettings file2(path("profile4b.conf"), QSettings::IniFormat);
ConfigProfile::exportTo(live, file2, {ConfigProfile::Part::Other});
QVERIFY(file2.contains("diagrameditor/Xgrid"));
QVERIFY(!file2.contains("elements-collections/custom-collection-path"));
}
// Every choosable part is a complete profile: format 1, folders
// included, readable by the QElectroTech that only knows format 1.
void exportEveryPartIsComplete()
{
QSettings live(path("live5.conf"), QSettings::IniFormat);
live.setValue("diagrameditor/Xgrid", 7);
live.setValue("elements-collections/custom-collection-path", "/home/a/sym");
QSettings file(path("profile5.conf"), QSettings::IniFormat);
ConfigProfile::exportTo(live, file, ConfigProfile::choosableParts());
QCOMPARE(file.value(ConfigProfile::marker_key).toInt(), 1);
QVERIFY(!file.contains(ConfigProfile::parts_key));
QVERIFY(file.contains("elements-collections/custom-collection-path"));
QCOMPARE(ConfigProfile::partsOf(file).size(), 4);
}
// Loading some parts replaces those parts only, removes their keys the
// file lacks, and keeps every other setting, folders included.
void importSomePartsKeepsTheRest()
{
QSettings file(path("profile6.conf"), QSettings::IniFormat);
file.setValue("diagrameditor/defaultconductortype", "Single");
file.setValue("diagrameditor/Xgrid", 99); // not in its parts: ignored
file.setValue(ConfigProfile::marker_key, 2);
file.setValue(ConfigProfile::parts_key, "newproject");
QSettings live(path("live6.conf"), QSettings::IniFormat);
live.setValue("diagrameditor/defaultconductortype", "Multi");
live.setValue("autonum/folio/current", "mine");
live.setValue("diagrameditor/Xgrid", 10);
live.setValue("shortcuts/mainwindow.fullscreen", "F11");
live.setValue("elements-collections/custom-collection-path", "/home/me/sym");
ConfigProfile::importFrom(file, live);
QCOMPARE(live.value("diagrameditor/defaultconductortype").toString(), QString("Single"));
QVERIFY(!live.contains("autonum/folio/current"));
QCOMPARE(live.value("diagrameditor/Xgrid").toInt(), 10);
QCOMPARE(live.value("shortcuts/mainwindow.fullscreen").toString(), QString("F11"));
QCOMPARE(live.value("elements-collections/custom-collection-path").toString(),
QString("/home/me/sym"));
QVERIFY(!live.contains(ConfigProfile::parts_key));
}
void restartOnlyWhenNeeded()
{
using P = ConfigProfile::Part;
QVERIFY(!ConfigProfile::needsRestart({P::NewProject}));
QVERIFY(ConfigProfile::needsRestart({P::NewProject, P::Controls}));
QVERIFY(ConfigProfile::needsRestart({P::Other}));
}
// A file of format 2 naming no known part is refused by the caller.
void unknownPartsGiveNothing()
{
QSettings file(path("profile7.conf"), QSettings::IniFormat);
file.setValue(ConfigProfile::marker_key, 2);
file.setValue(ConfigProfile::parts_key, "colours");
QVERIFY(ConfigProfile::partsOf(file).isEmpty());
}
void plainFileIsNotAProfile()
{
QSettings other(path("other.conf"), QSettings::IniFormat);
@@ -21,6 +21,7 @@
#include <QAction>
#include <QComboBox>
#include <QCoreApplication>
#include <QListWidget>
#include <QPushButton>
#include <QSettings>
@@ -86,6 +87,11 @@ private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
//A scope of this test's own. Without an organization name,
//QSettings on Windows has no registry key: it drops every
//write and reads only defaults.
QCoreApplication::setOrganizationName(QStringLiteral("QElectroTech-tst_diagramtoolbarsettings"));
QCoreApplication::setApplicationName(QStringLiteral("tst_diagramtoolbarsettings"));
clearSettings();
//A few of the diagram editor's commands, as it registers them
for (const QString &id : {QStringLiteral("diagrameditor.copy"),
+502
View File
@@ -0,0 +1,502 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include "genericdevice/genericdevice.h"
using namespace GenericDevice;
namespace {
/// QElectroTech's default diagram font, at the sizes the wizard uses
Fonts fonts()
{
QFont names(QStringLiteral("Liberation Sans"));
names.setPointSizeF(7);
QFont label(QStringLiteral("Liberation Sans"));
label.setPointSizeF(9);
return {names, label};
}
QStringList names(const Slots &slot_list)
{
QStringList list;
for (const Slot &s : slot_list)
list << (s.kind == Slot::Terminal ? s.name : s.kind == Slot::Gap ? QStringLiteral("_")
: QStringLiteral("~"));
return list;
}
Slots parse(const QString &pattern)
{
const PatternResult r = expandPattern(pattern);
Q_ASSERT(r.ok());
return r.list;
}
/// The VSD of the scope document, §3
Spec vsd()
{
Spec spec;
spec.name = QStringLiteral("VSD");
spec.label = QStringLiteral("-U1");
spec.manufacturer = QStringLiteral("ACME");
spec.reference = QStringLiteral("VF-400");
spec.sides[Left] = parse(QStringLiteral("L1,L2,L3,,PE"));
spec.sides[Right] = parse(QStringLiteral("U,V,W,,PE"));
spec.sides[Bottom] = parse(QStringLiteral("DI{4},COM,+24V"));
return spec;
}
QString toText(const QDomElement &e)
{
QString out;
QTextStream stream(&out);
e.save(stream, 1);
return out;
}
} // namespace
class tst_genericdevice : public QObject
{
Q_OBJECT
private slots:
void expandPattern_data()
{
QTest::addColumn<QString>("pattern");
QTest::addColumn<QStringList>("expected");
QTest::newRow("plain") << "L1" << QStringList{"L1"};
QTest::newRow("gap") << "L3,,PE" << QStringList{"L3", "_", "PE"};
QTest::newRow("count") << "In{5}" << QStringList{"In1", "In2", "In3", "In4", "In5"};
QTest::newRow("from zero") << "Q{z5}" << QStringList{"Q0", "Q1", "Q2", "Q3", "Q4"};
QTest::newRow("range") << "X{3-8}" << QStringList{"X3", "X4", "X5", "X6", "X7", "X8"};
QTest::newRow("prefix") << "X1:{4}" << QStringList{"X1:1", "X1:2", "X1:3", "X1:4"};
QTest::newRow("suffix") << "{2}.1" << QStringList{"1.1", "2.1"};
QTest::newRow("spaces") << " L1 , L2 " << QStringList{"L1", "L2"};
QTest::newRow("empty") << "" << QStringList{};
QTest::newRow("vsd bottom") << "DI{4},COM,+24V"
<< QStringList{"DI1", "DI2", "DI3", "DI4", "COM", "+24V"};
}
void expandPattern()
{
QFETCH(QString, pattern);
QFETCH(QStringList, expected);
const PatternResult r = GenericDevice::expandPattern(pattern);
QVERIFY2(r.ok(), qPrintable(r.error));
QCOMPARE(names(r.list), expected);
}
void patternErrors_data()
{
QTest::addColumn<QString>("pattern");
QTest::newRow("two braces") << "X{2}{3}";
QTest::newRow("unclosed") << "X{2";
QTest::newRow("letters") << "X{a}";
QTest::newRow("z with range") << "X{z1-3}";
QTest::newRow("backwards range") << "X{5-3}";
QTest::newRow("zero count") << "X{0}";
QTest::newRow("over the cap") << "X{201}";
}
void patternErrors()
{
QFETCH(QString, pattern);
const PatternResult r = GenericDevice::expandPattern(pattern);
QVERIFY(!r.ok());
QVERIFY(r.list.isEmpty());
}
void capIsInclusive()
{
QCOMPARE(GenericDevice::expandPattern(QStringLiteral("X{200}")).list.size(), 200);
}
void toPatternFoldsRuns()
{
bool ok = false;
QCOMPARE(toPattern(parse(QStringLiteral("In{5},COM")), &ok), QStringLiteral("In{5},COM"));
QVERIFY(ok);
QCOMPARE(toPattern(parse(QStringLiteral("Q{z4}"))), QStringLiteral("Q{z4}"));
QCOMPARE(toPattern(parse(QStringLiteral("X{3-8}"))), QStringLiteral("X{3-8}"));
QCOMPARE(toPattern(parse(QStringLiteral("L1,L2,L3,,PE"))), QStringLiteral("L{3},,PE"));
//Two in a row stay as written
QCOMPARE(toPattern(parse(QStringLiteral("A1,A2"))), QStringLiteral("A1,A2"));
//Leading zeros are not numbers to fold
QCOMPARE(toPattern(parse(QStringLiteral("01,02,03"))), QStringLiteral("01,02,03"));
}
void toPatternRoundTrips_data()
{
QTest::addColumn<QString>("pattern");
for (const char *p : {"L1,L2,L3,,PE", "DI{4},COM,+24V", "Q{z5}", "X1:{4}", ",A,,B,",
"1,2,3,4,5", "X9,X10,X11", "A,A,A"})
QTest::newRow(p) << QString::fromLatin1(p);
}
void toPatternRoundTrips()
{
QFETCH(QString, pattern);
const Slots original = parse(pattern);
QCOMPARE(names(parse(toPattern(original))), names(original));
}
void toPatternFlagsWhatItCannotSay()
{
bool ok = true;
Slots s{Slot::terminal(QStringLiteral("A,B"))};
toPattern(s, &ok);
QVERIFY(!ok);
ok = true;
toPattern({Slot::terminal(QStringLiteral("{x}"))}, &ok);
QVERIFY(!ok);
}
void applyPatternKeepsWhatTheTableHolds()
{
Slots current = parse(QStringLiteral("1,2"));
current[0].function = QStringLiteral("OUT1");
current[0].uuid = QUuid::createUuid();
current[1].type = QStringLiteral("Inner");
const Slots result = applyPattern(current, parse(QStringLiteral("A,,B,C")));
QCOMPARE(names(result), (QStringList{"A", "_", "B", "C"}));
QCOMPARE(result[0].function, QStringLiteral("OUT1"));
QCOMPARE(result[0].uuid, current[0].uuid);
QCOMPARE(result[2].type, QStringLiteral("Inner"));
QVERIFY(result[3].uuid.isNull());
}
void groupEvery_data()
{
QTest::addColumn<int>("count");
QTest::addColumn<int>("n");
QTest::addColumn<QString>("expected");
QTest::newRow("9 by 4") << 9 << 4 << "1 2 3 4 _ 5 6 7 8 _ 9";
QTest::newRow("8 by 4") << 8 << 4 << "1 2 3 4 _ 5 6 7 8";
QTest::newRow("none") << 3 << 0 << "1 2 3";
QTest::newRow("by 1") << 3 << 1 << "1 _ 2 _ 3";
QTest::newRow("n = count") << 4 << 4 << "1 2 3 4";
QTest::newRow("n > count") << 3 << 8 << "1 2 3";
}
void groupEvery()
{
QFETCH(int, count);
QFETCH(int, n);
QFETCH(QString, expected);
const Slots s = parse(QStringLiteral("{%1}").arg(count));
QCOMPARE(names(GenericDevice::groupEvery(s, n)).join(' '), expected);
}
void groupEveryReplacesHandGaps()
{
QCOMPARE(names(GenericDevice::groupEvery(parse(QStringLiteral("1,,2,3,,,4")), 2)).join(' '),
QStringLiteral("1 2 _ 3 4"));
}
void resizeKeepsTypedNames()
{
const Slots s = parse(QStringLiteral("L1,L2,,PE"));
QCOMPARE(names(resize(s, 5, 0)).join(' '), QStringLiteral("L1 L2 _ PE "));
QCOMPARE(names(resize(s, 2, 0)).join(' '), QStringLiteral("L1 L2"));
QCOMPARE(names(resize(s, 0, 0)), QStringList());
QCOMPARE(names(resize(s, 5, 2)).join(' '), QStringLiteral("L1 L2 _ PE _ "));
}
void lineUpGroups()
{
Slots a = parse(QStringLiteral("L1,L2,L3,N,,PE"));
Slots b = parse(QStringLiteral("U,V,W,,PE"));
GenericDevice::lineUpGroups(a, b);
QCOMPARE(names(a).join(' '), QStringLiteral("L1 L2 L3 N _ PE"));
QCOMPARE(names(b).join(' '), QStringLiteral("U V W ~ _ PE"));
QCOMPARE(a.size(), b.size());
}
void lineUpLeavesEqualAndUngroupedSidesAlone()
{
Slots a = parse(QStringLiteral("L1,L2,L3,,PE"));
Slots b = parse(QStringLiteral("U,V,W,,PE"));
GenericDevice::lineUpGroups(a, b);
QCOMPARE(names(b).join(' '), QStringLiteral("U V W _ PE"));
//No gap on one side: nothing faces the other's groups
Slots c = parse(QStringLiteral("1,2,3,4,5"));
Slots d = parse(QStringLiteral("A,,B"));
GenericDevice::lineUpGroups(c, d);
QCOMPARE(names(c).join(' '), QStringLiteral("1 2 3 4 5"));
QCOMPARE(names(d).join(' '), QStringLiteral("A _ B"));
//More groups on one side: the extra ones simply follow
Slots e = parse(QStringLiteral("A,,B,B2,,C"));
Slots f = parse(QStringLiteral("X,X2,,Y"));
GenericDevice::lineUpGroups(e, f);
QCOMPARE(names(e).join(' '), QStringLiteral("A ~ _ B B2 _ C"));
QCOMPARE(names(f).join(' '), QStringLiteral("X X2 _ Y"));
}
void numberPinsCounterClockwise()
{
Spec spec;
spec.sides[Left] = resize({}, 3, 0);
spec.sides[Bottom] = {Slot::terminal({}), Slot::gap(), Slot::terminal({})};
spec.sides[Right] = resize({}, 2, 0);
spec.sides[Top] = resize({}, 2, 0);
numberPins(spec);
QCOMPARE(names(spec.sides[Left]).join(' '), QStringLiteral("1 2 3"));
QCOMPARE(names(spec.sides[Bottom]).join(' '), QStringLiteral("4 _ 5"));
QCOMPARE(names(spec.sides[Right]).join(' '), QStringLiteral("7 6"));
QCOMPARE(names(spec.sides[Top]).join(' '), QStringLiteral("9 8"));
QVERIFY(hasNumberedPins(spec));
spec.sides[Top][0].name = QStringLiteral("PE");
QVERIFY(!hasNumberedPins(spec));
}
void nameUnnamedLeavesTypedNames()
{
Spec spec;
spec.sides[Left] = {Slot::terminal(QStringLiteral("L1")), Slot::terminal({}), Slot::gap(),
Slot::terminal({})};
nameUnnamed(spec);
QCOMPARE(names(spec.sides[Left]).join(' '), QStringLiteral("L1 2 _ 3"));
}
void pasteTabsAndCommasReadAlike()
{
const QString tabs = QStringLiteral(
"side\tname\tfunction\n"
"L\tL1\n" "L\tL2\n" "L\tL3\n" "L\t\n" "L\tPE\n"
"right\tU\tmotor\n" "R\tV\n" "R\tW\n" "R\t\n" "R\tPE\n"
"bottom\tDI1\t\tInner\n");
QString commas = tabs;
commas.replace('\t', ',');
for (const QString &text : {tabs, commas})
{
const PasteResult r = parsePastedTable(text);
QVERIFY2(r.bad_rows.isEmpty(), qPrintable(r.bad_rows.join('|')));
QCOMPARE(r.rows, 11);
QCOMPARE(names(r.sides[Left]).join(' '), QStringLiteral("L1 L2 L3 _ PE"));
QCOMPARE(r.sides[Right][0].function, QStringLiteral("motor"));
QCOMPARE(r.sides[Bottom][0].type, QStringLiteral("Inner"));
QVERIFY(r.has_side[Left] && r.has_side[Right] && r.has_side[Bottom]);
QVERIFY(!r.has_side[Top]);
}
}
void pasteListsBadRows()
{
const PasteResult r = parsePastedTable(QStringLiteral(
"left,A\r\nmiddle,B\r\ntop,C,,Sideways\r\n\r\nbottom,D,f,Generic,extra\r\nT,E"));
QCOMPARE(r.rows, 2);
QCOMPARE(r.bad_rows, (QStringList{"middle,B", "top,C,,Sideways", "bottom,D,f,Generic,extra"}));
QCOMPARE(names(r.sides[Top]), QStringList{"E"});
}
/// Scope §3: the VSD body is 160 × 140 with the terminals packed
/// from the top and clear of the corners
void vsdSize()
{
const Layout l = layout(vsd(), fonts());
QCOMPARE(l.body_width, 160);
QCOMPARE(l.body_height, 140);
QCOMPARE(l.terminals.size(), 14);
QCOMPARE(l.dividers.size(), 2);
QVERIFY(l.warnings.join(' ').contains(QStringLiteral("PE")));
}
void everythingOnTheGrid()
{
Spec spec = vsd();
spec.sides[Top] = parse(QStringLiteral("X{3}"));
spec.extra_width = 3;
QDomDocument doc;
const QDomElement def = toDefinition(spec, fonts(), doc);
for (const char *a : {"width", "height", "hotspot_x", "hotspot_y"})
QCOMPARE(def.attribute(a).toInt() % Grid, 0);
const QDomNodeList terminals = def.elementsByTagName(QStringLiteral("terminal"));
QCOMPARE(terminals.size(), 17);
for (int i = 0 ; i < terminals.size() ; ++i) {
const QDomElement t = terminals.at(i).toElement();
QCOMPARE(qRound(t.attribute("x").toDouble()) % Grid, 0);
QCOMPARE(qRound(t.attribute("y").toDouble()) % Grid, 0);
QVERIFY(!QUuid(t.attribute("uuid")).isNull());
}
}
void lineUpPutsPeOnOneRow()
{
Spec spec;
spec.sides[Left] = parse(QStringLiteral("L1,L2,L3,N,,PE"));
spec.sides[Right] = parse(QStringLiteral("U,V,W,,PE"));
Layout l = layout(spec, fonts());
QList<qreal> pe;
for (const auto &t : l.terminals)
if (t.slot.name == QLatin1String("PE")) pe << t.point.y();
QCOMPARE(pe.size(), 2);
QCOMPARE(pe[0], pe[1]);
//Both gaps marked, the padding not
QCOMPARE(l.dividers.size(), 2);
QCOMPARE(l.dividers[0].y1(), l.dividers[1].y1());
spec.line_up = false;
l = layout(spec, fonts());
pe.clear();
for (const auto &t : l.terminals)
if (t.slot.name == QLatin1String("PE")) pe << t.point.y();
QVERIFY(pe[0] != pe[1]);
spec.mark_gaps = false;
QVERIFY(layout(spec, fonts()).dividers.isEmpty());
}
void edgeCases()
{
Spec empty;
Layout l = layout(empty, fonts());
//No terminals: the minimum, or as wide as the label needs
QVERIFY(l.body_width >= MinWidth);
QCOMPARE(l.body_width % Grid, 0);
QCOMPARE(l.body_height, MinHeight);
QVERIFY(l.terminals.isEmpty());
Spec one;
one.sides[Top] = parse(QStringLiteral("A"));
l = layout(one, fonts());
QCOMPARE(l.terminals.size(), 1);
QCOMPARE(l.terminals[0].point.y(), qreal(-Stub));
Spec many;
many.sides[Left] = parse(QStringLiteral("{50}"));
l = layout(many, fonts());
QCOMPARE(l.terminals.size(), 50);
QVERIFY(l.body_height >= 50 * many.pitch);
}
void extraSizeOnlyAdds()
{
Spec spec = vsd();
const Layout base = layout(spec, fonts());
spec.extra_width = 2;
spec.extra_height = 1;
const Layout grown = layout(spec, fonts());
QCOMPARE(grown.body_width, base.body_width + 20);
QCOMPARE(grown.body_height, base.body_height + 10);
spec.extra_width = -5;
QCOMPARE(layout(spec, fonts()).body_width, base.body_width);
}
void definitionRoundTrip()
{
Spec spec = vsd();
spec.sides[Right][0].function = QStringLiteral("motor");
spec.sides[Left][1].type = QStringLiteral("Inner");
spec.sides[Top] = parse(QStringLiteral("A,B"));
spec.extra_width = 2;
spec.extra_height = 1;
spec.pitch = 30;
QDomDocument doc;
const QDomElement def = toDefinition(spec, fonts(), doc, QStringLiteral("0.100.0"));
Spec back;
QVERIFY(fromDefinition(def, fonts(), &back));
QCOMPARE(back.name, spec.name);
QCOMPARE(back.label, spec.label);
QCOMPARE(back.manufacturer, spec.manufacturer);
QCOMPARE(back.reference, spec.reference);
QCOMPARE(back.show_reference, true);
QCOMPARE(back.pitch, 30);
QCOMPARE(back.extra_width, 2);
QCOMPARE(back.extra_height, 1);
QVERIFY(back.mark_gaps);
QVERIFY(back.line_up);
for (int s = 0 ; s < SideCount ; ++s)
QCOMPARE(names(back.sides[s]), names(spec.sides[s]));
QCOMPARE(back.sides[Right][0].function, QStringLiteral("motor"));
QCOMPARE(back.sides[Left][1].type, QStringLiteral("Inner"));
//Written again, the terminals keep their uuids and positions
QDomDocument doc2;
const QDomElement def2 = toDefinition(back, fonts(), doc2, QStringLiteral("0.100.0"));
const QDomNodeList t1 = def.elementsByTagName(QStringLiteral("terminal"));
const QDomNodeList t2 = def2.elementsByTagName(QStringLiteral("terminal"));
QCOMPARE(t2.size(), t1.size());
for (int i = 0 ; i < t1.size() ; ++i)
for (const char *a : {"x", "y", "uuid", "name", "orientation", "type"})
QCOMPARE(t2.at(i).toElement().attribute(a), t1.at(i).toElement().attribute(a));
for (const char *a : {"width", "height", "hotspot_x", "hotspot_y"})
QCOMPARE(def2.attribute(a), def.attribute(a));
}
void roundTripWithPaddingAndNoMarks()
{
Spec spec;
spec.name = QStringLiteral("X");
spec.sides[Left] = parse(QStringLiteral("L1,L2,L3,N,,PE"));
spec.sides[Right] = parse(QStringLiteral("U,V,W,,PE"));
for (bool marks : {true, false})
{
spec.mark_gaps = marks;
QDomDocument doc;
const QDomElement def = toDefinition(spec, fonts(), doc);
Spec back;
QVERIFY(fromDefinition(def, fonts(), &back));
QDomDocument doc2;
const QDomElement def2 = toDefinition(back, fonts(), doc2);
const QDomNodeList t1 = def.elementsByTagName(QStringLiteral("terminal"));
const QDomNodeList t2 = def2.elementsByTagName(QStringLiteral("terminal"));
QCOMPARE(t2.size(), t1.size());
for (int i = 0 ; i < t1.size() ; ++i)
QCOMPARE(t2.at(i).toElement().attribute("y"), t1.at(i).toElement().attribute("y"));
QCOMPARE(def2.elementsByTagName("line").size(), def.elementsByTagName("line").size());
}
}
void foreignDefinitionsAreRefused()
{
QDomDocument doc;
QDomElement def = toDefinition(vsd(), fonts(), doc);
Spec back;
QVERIFY(fromDefinition(def, fonts(), &back));
//A circle drawn on it in the symbol editor
QDomElement description = def.firstChildElement(QStringLiteral("description"));
QDomElement extra = doc.createElement(QStringLiteral("ellipse"));
description.appendChild(extra);
QVERIFY(!fromDefinition(def, fonts(), &back));
description.removeChild(extra);
//A terminal moved off its edge
QDomElement t = description.firstChildElement(QStringLiteral("terminal"));
t.setAttribute(QStringLiteral("x"), 5);
QVERIFY(!fromDefinition(def, fonts(), &back));
}
/// The output is stable apart from the uuids
void definitionIsDeterministic()
{
Spec spec = vsd();
for (Slots &side : spec.sides)
for (Slot &s : side)
s.uuid = QUuid::createUuid();
QDomDocument a, b;
QString one = toText(toDefinition(spec, fonts(), a));
QString two = toText(toDefinition(spec, fonts(), b));
static const QRegularExpression uuid(QStringLiteral("uuid=\"\\{[^}]+\\}\""));
static const QRegularExpression part_uuid(QStringLiteral("<uuid uuid=\"[^\"]+\"/>"));
//Only parts and the definition get fresh uuids; the terminals keep theirs
QCOMPARE(one.count(QStringLiteral("<terminal ")), 14);
for (QString *s : {&one, &two}) {
const QStringList lines = s->split('\n');
QStringList kept;
for (QString line : lines) {
if (!line.contains(QLatin1String("<terminal")))
line.replace(uuid, QStringLiteral("uuid=\"\""));
kept << line;
}
*s = kept.join('\n');
}
QCOMPARE(one, two);
}
};
QTEST_MAIN(tst_genericdevice)
#include "tst_genericdevice.moc"
+6
View File
@@ -22,6 +22,7 @@
#include <QAction>
#include <QComboBox>
#include <QCoreApplication>
#include <QDropEvent>
#include <QListWidget>
#include <QMainWindow>
@@ -64,6 +65,11 @@ private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
//A scope of this test's own. Without an organization name,
//QSettings on Windows has no registry key: it drops every
//write and reads only defaults.
QCoreApplication::setOrganizationName(QStringLiteral("QElectroTech-tst_gesturesettings"));
QCoreApplication::setApplicationName(QStringLiteral("tst_gesturesettings"));
QSettings().remove(QStringLiteral("diagrameditor"));
addCommand(QStringLiteral("diagrameditor.copy"), QStringLiteral("Copier"));
addCommand(QStringLiteral("diagrameditor.cut"), QStringLiteral("Couper"));
+53
View File
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QGraphicsSceneHoverEvent>
#include "QetGraphicsItemModeler/qetgraphicshandleritem.h"
class tst_handlersize : public QObject
{
Q_OBJECT
private slots:
void sizeChangeCancelsOldHoverAnimation() {
QGraphicsScene scene;
auto *handle=new QetGraphicsHandlerItem(30);scene.addItem(handle);
QGraphicsSceneHoverEvent event(QEvent::GraphicsSceneHoverEnter);
scene.sendEvent(handle,&event);
handle->setSize(2.5);
QTest::qWait(250);
QCOMPARE(handle->property("currentSize").toReal(),qreal(2.5));
}
void fractionalGeometryAndPicking() {
for (qreal size : {2.5, 5.0, 7.5, 10.0, 20.0, 30.0}) {
QGraphicsScene scene;
auto *object = scene.addRect(QRectF(0,0,100,60));
object->setPos(80,40); object->setRotation(37); object->setSelected(true);
const QRectF geometry=object->rect(); const auto transform=object->sceneTransform();
auto handles=QetGraphicsHandlerItem::handlerForPoint({QPointF(120,80)},size);
auto *handle=handles.first(); scene.addItem(handle); handle->setColor(Qt::darkGreen);
QCOMPARE(handle->property("currentSize").toReal(),size);
QCOMPARE(handle->boundingRect().width(),size+2);
const auto position=handle->pos();
for (qreal next : {2.5,5.0,7.5,10.0,20.0,30.0}) {
handle->setSize(next);
QCOMPARE(handle->property("currentSize").toReal(),next);
QCOMPARE(handle->pos(),position); QVERIFY(handle->isVisible());
QCOMPARE(object->rect(),geometry); QCOMPARE(object->sceneTransform(),transform);
for (qreal zoom : {0.25,1.0,4.0}) {
QGraphicsView view(&scene); view.scale(zoom,zoom);
const auto device=handle->deviceTransform(view.viewportTransform());
QCOMPARE(device.mapRect(handle->boundingRect()).width(),next+2);
const QPointF centre=handle->scenePos();
QVERIFY(scene.items(centre,Qt::IntersectsItemShape,Qt::DescendingOrder,view.viewportTransform()).contains(handle));
const QPointF outside=centre+QPointF((next/2+2)/zoom,0);
QVERIFY(!scene.items(outside,Qt::IntersectsItemShape,Qt::DescendingOrder,view.viewportTransform()).contains(handle));
}
}
handle->hide(); QVERIFY(!scene.items(position).contains(handle));
}
}
};
QTEST_MAIN(tst_handlersize)
#include "tst_handlersize.moc"
+254
View File
@@ -0,0 +1,254 @@
/*
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 "imagedrop.h"
#include <QtEndian>
#include <QtTest>
#include <QImage>
#include <QMimeData>
#include <QTemporaryDir>
#include <QUrl>
/**
Picture files dropped on a folio: which files are taken, the limits a
file must meet before it is embedded in the project, and where the
pictures of one drop land.
*/
class tst_imagedrop : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
QString path(const QString &name) const { return m_dir.filePath(name); }
static QMimeData *urls(const QStringList &paths)
{
auto *mime = new QMimeData();
QList<QUrl> list;
for (const QString &p : paths)
list << (p.startsWith("http") ? QUrl(p) : QUrl::fromLocalFile(p));
mime->setUrls(list);
return mime;
}
// A BMP header announcing width x height pixels, without the pixels.
static QByteArray bmpHeader(qint32 width, qint32 height)
{
QByteArray b(54, '\0');
b[0] = 'B'; b[1] = 'M';
qToLittleEndian<quint32>(54, b.data() + 2);
qToLittleEndian<quint32>(54, b.data() + 10);
qToLittleEndian<quint32>(40, b.data() + 14);
qToLittleEndian<qint32>(width, b.data() + 18);
qToLittleEndian<qint32>(height, b.data() + 22);
qToLittleEndian<quint16>(1, b.data() + 26);
qToLittleEndian<quint16>(24, b.data() + 28);
return b;
}
static void write(const QString &p, const QByteArray &data)
{
QFile f(p);
QVERIFY(f.open(QIODevice::WriteOnly));
f.write(data);
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QImage img(40, 20, QImage::Format_ARGB32);
img.fill(Qt::red);
QVERIFY(img.save(path("a.png")));
QVERIFY(img.save(path("b.JPG"), "JPG"));
write(path("plan.qet"), "<project/>");
write(path("fake.png"), "this is not a picture");
write(path("bomb.bmp"), bmpHeader(10000, 10000));
QFile big(path("big.png"));
QVERIFY(big.open(QIODevice::WriteOnly));
QVERIFY(big.resize(ImageDrop::maxFileBytes + 1));
}
// Only local files with a picture suffix are taken, in drop order and
// whatever the case of the suffix.
void picturesAreTakenInOrder()
{
std::unique_ptr<QMimeData> mime(urls({path("b.JPG"), path("plan.qet"), path("a.png"),
"https://example.org/remote.png"}));
QCOMPARE(ImageDrop::imageFiles(mime.get()), QStringList({path("b.JPG"), path("a.png")}));
QVERIFY(!ImageDrop::hasOnlyOtherUrls(mime.get()));
}
// A drop holding only a project is left to the main window, which
// opens it; plain text is not a URL drop at all.
void otherFilesAreLeftAlone()
{
std::unique_ptr<QMimeData> project(urls({path("plan.qet")}));
QVERIFY(ImageDrop::imageFiles(project.get()).isEmpty());
QVERIFY(ImageDrop::hasOnlyOtherUrls(project.get()));
QMimeData text;
text.setText("K1");
QVERIFY(!ImageDrop::hasOnlyOtherUrls(&text));
QVERIFY(ImageDrop::imageFiles(&text).isEmpty());
QVERIFY(ImageDrop::imageFiles(nullptr).isEmpty());
}
void validPictureLoads()
{
QString error;
const QImage image = ImageDrop::load(path("a.png"), &error);
QCOMPARE(image.size(), QSize(40, 20));
QVERIFY(error.isEmpty());
}
void unusableFilesAreRefusedWithAReason_data()
{
QTest::addColumn<QString>("file");
QTest::addColumn<QString>("reason");
QTest::newRow("missing") << path("missing.png") << "file";
QTest::newRow("directory") << m_dir.path() << "file";
QTest::newRow("not a picture") << path("fake.png") << "read";
QTest::newRow("over 10 MB") << path("big.png") << "10 MB";
// refused on its header, before the 300 MB of pixels are allocated
QTest::newRow("too many pixels") << path("bomb.bmp") << "pixels";
}
void unusableFilesAreRefusedWithAReason()
{
QFETCH(QString, file);
QFETCH(QString, reason);
QString error;
QVERIFY(ImageDrop::load(file, &error).isNull());
QVERIFY2(error.contains(reason), qPrintable(error));
}
// A picture too large for the folio is scaled down until it leaves 20 %
// of the frame free on every side, i.e. into 60 % of its width and
// height; a picture that already fits keeps its size.
void largePicturesLeaveAMarginToTheFrame_data()
{
QTest::addColumn<QSizeF>("size");
QTest::addColumn<qreal>("scale");
// frame: 1000 x 800, so the inner frame is 600 x 480
QTest::newRow("small, kept") << QSizeF(100, 50) << 1.0;
QTest::newRow("exactly the inner frame") << QSizeF(600, 480) << 1.0;
QTest::newRow("wide") << QSizeF(3000, 1000) << 0.2;
QTest::newRow("tall photo") << QSizeF(3024, 4032) << 480.0 / 4032;
}
void largePicturesLeaveAMarginToTheFrame()
{
QFETCH(QSizeF, size);
QFETCH(qreal, scale);
const QRectF frame(50, 30, 1000, 800);
QCOMPARE(ImageDrop::innerFrame(frame), QRectF(250, 190, 600, 480));
QCOMPARE(ImageDrop::fitScale(size, frame), scale);
const QSizeF scaled = size * ImageDrop::fitScale(size, frame);
QVERIFY(scaled.width() <= 600.0001 && scaled.height() <= 480.0001);
}
void noFrameMeansNoScaling()
{
QCOMPARE(ImageDrop::fitScale(QSizeF(4000, 3000), QRectF()), 1.0);
}
// A picture dropped near an edge is moved back inside, by the least
// amount; one larger than the area is centred on it.
void picturesAreKeptInsideTheArea_data()
{
QTest::addColumn<QRectF>("picture");
QTest::addColumn<QRectF>("expected");
const QRectF inside(300, 300, 100, 50);
QTest::newRow("already inside") << inside << inside;
QTest::newRow("over the top edge") << QRectF(300, -20, 100, 50) << QRectF(300, 0, 100, 50);
QTest::newRow("past the right and bottom") << QRectF(980, 790, 100, 50)
<< QRectF(900, 750, 100, 50);
QTest::newRow("wider than the area") << QRectF(-50, 100, 1200, 50)
<< QRectF(-100, 100, 1200, 50);
}
void picturesAreKeptInsideTheArea()
{
QFETCH(QRectF, picture);
QFETCH(QRectF, expected);
QCOMPARE(ImageDrop::keepInside(picture, QRectF(0, 0, 1000, 800)), expected);
}
// Several pictures dropped together are spread side by side: a
// roughly square grid over the area, each shrunk into its cell.
void severalPicturesAreSpreadInAGrid_data()
{
QTest::addColumn<int>("count");
QTest::addColumn<int>("columns");
QTest::addColumn<int>("rows");
QTest::newRow("2") << 2 << 2 << 1;
QTest::newRow("3") << 3 << 2 << 2;
QTest::newRow("4") << 4 << 2 << 2;
QTest::newRow("5") << 5 << 3 << 2;
QTest::newRow("9") << 9 << 3 << 3;
}
void severalPicturesAreSpreadInAGrid()
{
QFETCH(int, count);
QFETCH(int, columns);
QFETCH(int, rows);
const QRectF area(100, 50, 600, 480);
const QList<QSizeF> sizes(count, QSizeF(3024, 4032));
const QList<QRectF> rects = ImageDrop::gridLayout(sizes, area);
QCOMPARE(rects.size(), count);
const QSizeF cell(area.width() / columns, area.height() / rows);
for (int i = 0 ; i < count ; ++i)
{
const QRectF r = rects.at(i);
// inside the area, proportions kept
QVERIFY(area.contains(r));
QVERIFY(qAbs(r.width() / r.height() - 3024.0 / 4032.0) < 1e-6);
// centred in its own cell, row by row
const QPointF centre = area.topLeft() + QPointF(cell.width() * (i % columns + 0.5),
cell.height() * (i / columns + 0.5));
QVERIFY(qAbs(r.center().x() - centre.x()) < 1e-6);
QVERIFY(qAbs(r.center().y() - centre.y()) < 1e-6);
// and overlapping no other
for (int j = i + 1 ; j < count ; ++j)
QVERIFY2(!r.intersects(rects.at(j)), qPrintable(QString("%1 and %2").arg(i).arg(j)));
}
}
void smallPicturesAreNotEnlargedInTheGrid()
{
const QList<QRectF> rects = ImageDrop::gridLayout({QSizeF(40, 20), QSizeF(30, 30)},
QRectF(0, 0, 600, 480));
QCOMPARE(rects.at(0).size(), QSizeF(40, 20));
QCOMPARE(rects.at(1).size(), QSizeF(30, 30));
QCOMPARE(rects.at(0).center(), QPointF(150, 240));
QCOMPARE(rects.at(1).center(), QPointF(450, 240));
}
void emptyDropHasNoLayout()
{
QVERIFY(ImageDrop::gridLayout({}, QRectF(0, 0, 600, 480)).isEmpty());
}
};
QTEST_MAIN(tst_imagedrop)
#include "tst_imagedrop.moc"
+42
View File
@@ -191,6 +191,48 @@ private slots:
r.value(QStringLiteral("v")).toArray());
}
/**
Mirroring a symbol twice the same way puts it back where it was,
for every symbol of the project, including those that do not sit
on the grid. Before, the second mirror snapped such a symbol onto
the grid, a few pixels away. Which symbol mirrorBackAndSave()
picks changes from run to run, so this checks them all.
*/
void mirrorTwiceComesBack_data()
{
QTest::addColumn<bool>("vertical");
QTest::newRow("horizontal") << false;
QTest::newRow("vertical") << true;
}
void mirrorTwiceComesBack()
{
QFETCH(bool, vertical);
const QJsonObject r = run(script(QStringLiteral(
"var out = {count: 0, moved: []};\n"
"for (var f = 0; f < qet.folioCount(); f++) {\n"
" var els = qet.elementUuids(f);\n"
" for (var e = 0; e < els.length; e++) {\n"
" var before = JSON.stringify(terms(f, els[e]));\n"
" qet.mirrorElement(f, els[e], %1);\n"
" qet.mirrorElement(f, els[e], %1);\n"
" var after = JSON.stringify(terms(f, els[e]));\n"
" out.count++;\n"
" if (after !== before) out.moved.push(els[e] + ' ' + before + ' -> ' + after);\n"
" }\n"
"}\n"
"qet.log('PROBE ' + JSON.stringify(out));\n")
.arg(vertical ? QStringLiteral("true") : QStringLiteral("false"))),
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QVERIFY(r.value(QStringLiteral("count")).toInt() > 0);
const QJsonArray moved = r.value(QStringLiteral("moved")).toArray();
for (const QJsonValue &m : moved)
qWarning("moved: %s", qPrintable(m.toString()));
QCOMPARE(moved.size(), 0);
}
/**
On a turned symbol a mirror of the folio is the other mirror of the
symbol itself, and its rotation does not change: a label kept
+171
View File
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDomDocument>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTemporaryDir>
// Symbols copied in old versions can share one uuid on a folio. Since every
// terminal got a uuid (#1118), the wires on them were saved by symbol uuid
// and reopened on the first symbol carrying it (#1408).
//
// Runs the real binary on examples/Habitat-Schemas_developpes.qet, whose
// folio 1 has three lamps L1, L2, L3, and compares the nets QElectroTech
// exports with those of the unedited example:
// - the lamps given one uuid, saved twice;
// - fixtures/shared_symbol_uuid_0200.qet: that folio as a build with #1118
// saved it, its wires naming the lamps by the shared uuid.
namespace {
const QString example = QStringLiteral(QET_EXAMPLES_DIR "/Habitat-Schemas_developpes.qet");
QDomDocument load(const QString &path)
{
QDomDocument doc;
QFile file(path);
if (file.open(QIODevice::ReadOnly))
doc.setContent(&file);
return doc;
}
QDomElement firstDiagram(const QDomDocument &doc)
{
return doc.documentElement().firstChildElement(QStringLiteral("diagram"));
}
QList<QDomElement> childElements(const QDomElement &diagram, const QString &block,
const QString &tag)
{
QList<QDomElement> out;
for (QDomElement e = diagram.firstChildElement(block).firstChildElement(tag);
!e.isNull(); e = e.nextSiblingElement(tag))
out << e;
return out;
}
QList<QDomElement> lamps(const QDomDocument &doc)
{
QList<QDomElement> out;
for (const QDomElement &e : childElements(firstDiagram(doc), QStringLiteral("elements"),
QStringLiteral("element")))
if (e.attribute(QStringLiteral("type")).endsWith(QStringLiteral("lampe_pe.elmt")))
out << e;
return out;
}
} // namespace
class tst_sharedsymboluuid : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
int m_run = 0;
// Run the binary in a sandbox of its own, so a running QElectroTech
// cannot answer instead.
bool run(const QStringList &args)
{
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
QDir().mkpath(home);
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), args);
return proc.waitForFinished(120000) && proc.exitCode() == 0;
}
QString write(const QDomDocument &doc)
{
const QString path = m_dir.filePath(QStringLiteral("in%1.qet").arg(m_run));
QFile f(path);
if (f.open(QIODevice::WriteOnly))
f.write(doc.toByteArray());
return path;
}
QString resave(const QString &in)
{
const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
return run({QStringLiteral("--resave"), in, out}) ? out : QString();
}
// Folio 1's nets, each as its sorted symbol labels, sorted: the order
// the export numbers them in follows the file and does not matter.
QStringList nets(const QString &project)
{
const QString out = m_dir.filePath(QStringLiteral("nets%1.json").arg(m_run));
if (!run({QStringLiteral("--export-nets"), project, out}))
return {};
QFile f(out);
if (!f.open(QIODevice::ReadOnly))
return {};
QStringList result;
const QJsonArray list = QJsonDocument::fromJson(f.readAll())
.object().value(QStringLiteral("list")).toArray();
for (const QJsonValue &net : list) {
QStringList ends;
for (const QJsonValue &t : net.toObject().value(QStringLiteral("terminals")).toArray()) {
const QJsonObject end = t.toObject();
if (end.value(QStringLiteral("folio")).toInt() == 1)
ends << end.value(QStringLiteral("element")).toString();
}
ends.sort();
if (!ends.isEmpty())
result << ends.join(QLatin1Char(','));
}
result.sort();
return result;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
QCOMPARE(lamps(load(example)).size(), 3);
}
void sharedUuidSurvivesSaves()
{
const QStringList expected = nets(example);
QVERIFY(expected.contains(QStringLiteral("L2,L3,S2")));
QDomDocument doc = load(example);
const QList<QDomElement> l = lamps(doc);
const QString shared = l.first().attribute(QStringLiteral("uuid"));
for (QDomElement e : l)
e.setAttribute(QStringLiteral("uuid"), shared);
const QString twice = resave(resave(write(doc)));
QVERIFY(!twice.isEmpty());
QCOMPARE(nets(twice), expected);
//No wire names a lamp by the uuid they share
for (const QDomElement &c : childElements(firstDiagram(load(twice)),
QStringLiteral("conductors"),
QStringLiteral("conductor"))) {
QVERIFY(c.attribute(QStringLiteral("element1")) != shared);
QVERIFY(c.attribute(QStringLiteral("element2")) != shared);
}
}
void savedByUuidReopensRight()
{
const QString fixture = QFINDTESTDATA("fixtures/shared_symbol_uuid_0200.qet");
QVERIFY(!fixture.isEmpty());
QCOMPARE(nets(fixture), nets(example));
}
};
QTEST_GUILESS_MAIN(tst_sharedsymboluuid)
#include "tst_sharedsymboluuid.moc"
+84
View File
@@ -0,0 +1,84 @@
/*
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 <QtTest>
#include <QProcess>
#include <QTemporaryDir>
/**
A static text with anchor="alignment" has its x at the edge or centre
its Halignment selects, instead of its left edge (#1251). The symbol in
fixtures/static_text_anchor.qet has the same text five times, all at
x = 0: left, right and centre anchored, right aligned without anchor,
and anchored without Halignment. --export-dxf writes where each is drawn.
*/
class tst_statictextanchor : public QObject
{
Q_OBJECT
private slots:
void anchoredX()
{
const QString project = QFINDTESTDATA("fixtures/static_text_anchor.qet");
QVERIFY2(!project.isEmpty(), "fixture project not found");
QTemporaryDir dir;
QVERIFY(dir.isValid());
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--export-dxf"), project, dir.path()});
QVERIFY2(proc.waitForFinished(60000), "--export-dxf timed out");
QCOMPARE(proc.exitCode(), 0);
QFile file(dir.filePath(QStringLiteral("01_diagram.dxf")));
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
//x (group code 10) of each TEXT entity of the symbol, in order
QList<qreal> xs;
for (int i = 0 ; i + 1 < values.size() ; i += 2) {
if (values.at(i).trimmed() != QLatin1String("0") || values.at(i + 1).trimmed() != QLatin1String("TEXT"))
continue;
qreal x = 0;
QString text;
for (int j = i + 2 ; j + 1 < values.size() && values.at(j).trimmed() != QLatin1String("0") ; j += 2) {
if (values.at(j).trimmed() == QLatin1String("10"))
x = values.at(j + 1).trimmed().toDouble();
else if (values.at(j).trimmed() == QLatin1String("1"))
text = values.at(j + 1).trimmed();
}
if (text == QLatin1String("GPIO21 PCM_DOUT"))
xs << x;
}
QCOMPARE(xs.size(), 5);
const qreal left = xs.at(0), right = xs.at(1), centre = xs.at(2);
//anchored on the right edge: drawn one text width to the left
QVERIFY2(left - right > 20, qPrintable(QStringLiteral("left %1 right %2").arg(left).arg(right)));
//anchored on the centre: half as far
QVERIFY(qAbs(2 * (left - centre) - (left - right)) < 0.01);
//without anchor, x stays the left edge whatever the alignment
QCOMPARE(xs.at(3), left);
//anchor without Halignment is the left edge too
QCOMPARE(xs.at(4), left);
}
};
QTEST_APPLESS_MAIN(tst_statictextanchor)
#include "tst_statictextanchor.moc"
+2 -1
View File
@@ -176,6 +176,7 @@ private slots:
// and reports the change once, on release.
void dragKeepsTheOppositeCorner()
{
for (qreal size : {2.5, 5.0, 7.5, 10.0, 20.0, 30.0})
for (int c = TextResize::TopLeft ; c <= TextResize::BottomLeft ; ++c)
for (qreal angle : {0.0, 90.0, 37.0}) {
QGraphicsScene scene;
@@ -183,7 +184,7 @@ private slots:
scene.addItem(text);
text->setPos(100, 100);
text->setRotation(angle);
auto *handles = new TextResizeHandles(text);
auto *handles = new TextResizeHandles(text, size);
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
const Corner corner = Corner(c);
+6
View File
@@ -20,6 +20,7 @@
#include <QCheckBox>
#include <QComboBox>
#include <QCoreApplication>
#include <QDockWidget>
#include <QMainWindow>
#include <QSettings>
@@ -48,6 +49,11 @@ private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
//A scope of this test's own. Without an organization name,
//QSettings on Windows has no registry key: it drops every
//write and reads only defaults.
QCoreApplication::setOrganizationName(QStringLiteral("QElectroTech-tst_toolbarsettings"));
QCoreApplication::setApplicationName(QStringLiteral("tst_toolbarsettings"));
QSettings().remove(QStringLiteral("toolbars"));
m_window = new QMainWindow();