Merge remote-tracking branch 'upstream/master' into wiring-manager

Conflicts resolved in 24 files. Notable decisions:

- ico/icon-themes.qrc: upstream's icon folder restructure kept; the 36
  cable icon entries re-added under the new qet/qet-dark path scheme
  (XML valid, every referenced file exists).
- Toolbar system: upstream's DiagramToolbarSettings now owns the
  toolbar filling. The cable tool is registered as
  diagrameditor.add_cable (generic group loop) and listed on the
  "diagram" toolbar right after the auto-break conductor action,
  separated by a divider, exactly like before the merge.
- Paste: the cable lines of a copy are handed to
  PasteNumberingImport::push() as extra commands, so paste + cable
  lines + the numbering question stay ONE undo step on both paste
  paths (DiagramView::paste and DiagramEventAddPaste::commit).
- PDF links: upstream's deterministic link sorting kept together with
  the cable cross-reference injector (injectLinkTo -> links list).
- Element renumbering: upstream's ElementAutoNumSchemeCommand::renumber
  replaces the old formula-based body of
  QETProject::renumberElementsBySchemeTitle; the cable numbering rule
  API (cableAutoNum, axis priority, rule name) is kept in full.
- Auto-numbering pages: upstream's scheme management kept, the Câbles
  tab added as index 4 (changeToCableTab), both entry points
  changeToCable() and changeToElement() kept side by side.
- SelectAutonumW: dirty tracking and the contextEdited signal moved
  into upstream's insertPartRow() row API, so single-rule mode, the
  rule-removable button and "confirm without change leaves the project
  alone" keep working.
- xrefpropertieswidget: upstream's offset clamping kept; the cable
  branch (Texte field, Police button, hidden display options) kept;
  the stack-overlapping checkbox is hidden for cables.

Verified after the merge: build green (515/515, no errors), ctest
85/86 (only the pre-existing headless tst_menubarkeyboard failure),
offscreen smoke exit 124.
This commit is contained in:
Kellermorph
2026-10-09 14:09:08 +02:00
1767 changed files with 194544 additions and 72846 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright 2006 The QElectroTech Team
# Copyright 2006-2026 The QElectroTech Team
# This file is part of QElectroTech.
#
# QElectroTech is free software: you can redistribute it and/or modify
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright 2006 The QElectroTech Team
# Copyright 2006-2026 The QElectroTech Team
# This file is part of QElectroTech.
#
# QElectroTech is free software: you can redistribute it and/or modify
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright 2006 The QElectroTech Team
# Copyright 2006-2026 The QElectroTech Team
# This file is part of QElectroTech.
#
# QElectroTech is free software: you can redistribute it and/or modify
+540 -1
View File
@@ -1,4 +1,4 @@
# Copyright 2006 The QElectroTech Team
# Copyright 2006-2026 The QElectroTech Team
# This file is part of QElectroTech.
#
# QElectroTech is free software: you can redistribute it and/or modify
@@ -66,6 +66,30 @@ include(../../cmake/fetch_pugixml.cmake)
enable_testing()
add_executable(tst_darkimagelayers tst_darkimagelayers.cpp
${QET_DIR}/sources/palettegraphicsview.cpp ${QET_DIR}/sources/palettegraphicsview.h
${QET_DIR}/sources/qetpalette.cpp)
add_test(NAME tst_darkimagelayers COMMAND tst_darkimagelayers)
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_attachedplainpaste
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_attached_plain_paste_probe.py
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
--source ${CMAKE_CURRENT_SOURCE_DIR}/attached_plain_paste_probe.cpp
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
set_tests_properties(tst_attachedplainpaste PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE)
endif()
if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
add_test(NAME tst_darkimageintegration
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_image_dark_probe.py
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
--source ${CMAKE_CURRENT_SOURCE_DIR}/image_dark_integration_probe.cpp
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
set_tests_properties(tst_darkimageintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE)
endif()
add_executable(${PROJECT_NAME} tst_mytest.cpp)
add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME})
@@ -84,6 +108,13 @@ add_test(NAME tst_diagramsortkeys COMMAND tst_diagramsortkeys)
target_include_directories(tst_diagramsortkeys PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_diagramsortkeys PRIVATE Qt::Test)
# positionorder.h is header-only: the position comparisons behind element
# renumbering and terminal numbering, as strict weak orderings for std::sort.
add_executable(tst_positionorder tst_positionorder.cpp)
target_include_directories(tst_positionorder PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_positionorder PRIVATE Qt::Test Qt::Core)
add_test(NAME tst_positionorder COMMAND tst_positionorder)
# bordercelllabels.h is header-only too.
add_executable(tst_bordercelllabels tst_bordercelllabels.cpp)
add_test(NAME tst_bordercelllabels COMMAND tst_bordercelllabels)
@@ -104,6 +135,28 @@ add_test(NAME tst_textgrid COMMAND tst_textgrid)
target_include_directories(tst_textgrid PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_textgrid PRIVATE Qt::Test)
# foliogrid.h is header-only: the folio grid step as read from the
# settings, never 0 or negative, so no reader can divide by it or loop on it.
add_executable(tst_foliogrid tst_foliogrid.cpp)
target_include_directories(tst_foliogrid PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_foliogrid PRIVATE Qt::Test Qt::Core)
add_test(NAME tst_foliogrid COMMAND tst_foliogrid)
# textlines.h is header-only: the lines of a text as they are laid out,
# for the exports that write a text line by line.
add_executable(tst_textlines tst_textlines.cpp)
add_test(NAME tst_textlines COMMAND tst_textlines)
set_tests_properties(tst_textlines PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_textlines PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_textlines PRIVATE Qt::Test Qt::Gui)
# elementviewgrid.h is header-only: the symbol editor's snap step at each
# zoom (bugtracker #112), tested without an editor window.
add_executable(tst_elementviewgrid tst_elementviewgrid.cpp)
add_test(NAME tst_elementviewgrid COMMAND tst_elementviewgrid)
target_include_directories(tst_elementviewgrid PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_elementviewgrid PRIVATE Qt::Test)
# alignment.h is header-only too (it builds on textgrid.h): the geometry
# behind the align commands, tested without a scene.
add_executable(tst_alignment tst_alignment.cpp)
@@ -118,6 +171,13 @@ add_test(NAME tst_terminalnamecheck COMMAND tst_terminalnamecheck)
target_include_directories(tst_terminalnamecheck PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_terminalnamecheck PRIVATE Qt::Test)
# symbolscale.h is header-only: which factors the element editor offers
# for scaling an element without taking its terminals off the grid.
add_executable(tst_symbolscale tst_symbolscale.cpp)
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)
# 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)
@@ -132,6 +192,14 @@ add_test(NAME tst_itemgroups COMMAND tst_itemgroups)
target_include_directories(tst_itemgroups PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_itemgroups PRIVATE Qt::Test Qt::Widgets Qt::Xml)
# shownkinds.cpp keeps View > Show (bugtracker #301) apart from the item
# classes, so it is tested here on a plain scene of rectangles.
add_executable(tst_shownkinds tst_shownkinds.cpp ${QET_DIR}/sources/shownkinds.cpp)
add_test(NAME tst_shownkinds COMMAND tst_shownkinds)
set_tests_properties(tst_shownkinds PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_shownkinds PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_shownkinds PRIVATE Qt::Test Qt::Widgets)
# textanchor.h is header-only: the point of a text chosen by its alignment,
# shown as Position X/Y in the text properties (#1155).
add_executable(tst_textanchor tst_textanchor.cpp)
@@ -140,6 +208,18 @@ 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)
# 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(
tst_textresizehandles
tst_textresizehandles.cpp
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.cpp
${QET_DIR}/sources/QetGraphicsItemModeler/textresizehandles.cpp)
add_test(NAME tst_textresizehandles COMMAND tst_textresizehandles)
set_tests_properties(tst_textresizehandles PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_textresizehandles PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_textresizehandles PRIVATE Qt::Test Qt::Widgets)
# legacyelementtypes.h is header-only: the rewrite of element type names in
# nomenclature queries saved before June 2022.
add_executable(tst_legacyelementtypes tst_legacyelementtypes.cpp)
@@ -229,6 +309,109 @@ set_tests_properties(tst_commandsearch PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=o
target_include_directories(tst_commandsearch PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_commandsearch PRIVATE Qt::Test Qt::Widgets)
# ShortcutsConfigPage: icon and menu columns, category filter, finding a
# command by pressing its key, and the copied list.
add_executable(
tst_shortcutspage
tst_shortcutspage.cpp
${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.h
${QET_DIR}/sources/ui/configpage/configpage.h
${QET_DIR}/sources/shortcutmanager.cpp
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetstyle.cpp)
add_test(NAME tst_shortcutspage COMMAND tst_shortcutspage)
set_tests_properties(tst_shortcutspage PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_shortcutspage PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_shortcutspage PRIVATE Qt::Test Qt::Widgets Qt::Svg)
# ToolbarSettings and its configuration page: defaults change nothing,
# saved values reach every open window that took them, panels are left alone.
add_executable(
tst_toolbarsettings
tst_toolbarsettings.cpp
${QET_DIR}/sources/toolbarsettings.cpp
${QET_DIR}/sources/toolbarsettings.h
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.h
${QET_DIR}/sources/ui/configpage/configpage.h
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetstyle.cpp)
add_test(NAME tst_toolbarsettings COMMAND tst_toolbarsettings)
set_tests_properties(tst_toolbarsettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_toolbarsettings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_toolbarsettings PRIVATE Qt::Test Qt::Widgets Qt::Svg)
# DiagramToolbarSettings and its configuration page: unchanged toolbars
# store nothing and build as before, unknown ids and stray separators are
# skipped, the user's own toolbars are kept, a widget is in one place only.
add_executable(
tst_diagramtoolbarsettings
tst_diagramtoolbarsettings.cpp
${QET_DIR}/sources/diagramtoolbarsettings.cpp
${QET_DIR}/sources/diagramtoolbarsettings.h
${QET_DIR}/sources/shortcutmanager.cpp
${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.h
${QET_DIR}/sources/ui/configpage/configpage.h
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetstyle.cpp)
add_test(NAME tst_diagramtoolbarsettings COMMAND tst_diagramtoolbarsettings)
set_tests_properties(tst_diagramtoolbarsettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_diagramtoolbarsettings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_diagramtoolbarsettings PRIVATE Qt::Test Qt::Widgets Qt::Svg)
# GestureSettings, the gesture ring's 4 / 8 directions and empty slots, and
# its configuration page: the ring follows the shortcut bar until the user
# picks commands, then keeps each one in its direction.
add_executable(
tst_gesturesettings
tst_gesturesettings.cpp
${QET_DIR}/sources/gesturesettings.cpp
${QET_DIR}/sources/gesturesettings.h
${QET_DIR}/sources/shortcutbarsettings.cpp
${QET_DIR}/sources/diagramgestureoverlay.cpp
${QET_DIR}/sources/diagramgestureoverlay.h
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.cpp
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.h
${QET_DIR}/sources/ui/configpage/configpage.h
${QET_DIR}/sources/shortcutmanager.cpp
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetstyle.cpp)
add_test(NAME tst_gesturesettings COMMAND tst_gesturesettings)
set_tests_properties(tst_gesturesettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_gesturesettings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_gesturesettings PRIVATE Qt::Test Qt::Widgets Qt::Svg Qt::Xml pugixml::pugixml)
# CustomizeDialog: the configuration pages as tabs of one window; OK
# applies every page once, Cancel none.
add_executable(
tst_customizedialog
tst_customizedialog.cpp
${QET_DIR}/sources/ui/customizedialog.cpp
${QET_DIR}/sources/ui/customizedialog.h
${QET_DIR}/sources/ui/configpage/configpage.h)
add_test(NAME tst_customizedialog COMMAND tst_customizedialog)
set_tests_properties(tst_customizedialog PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_customizedialog PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_customizedialog PRIVATE Qt::Test Qt::Widgets)
# scriptheader.h is header-only: the // ==QETScript== block that turns a
# stored script into a button; and ShortcutManager::unregisterAction(), which
# takes a deleted script's command out of the lists again.
add_executable(
tst_scriptheader
tst_scriptheader.cpp
${QET_DIR}/sources/shortcutmanager.cpp)
add_test(NAME tst_scriptheader COMMAND tst_scriptheader)
set_tests_properties(tst_scriptheader PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_scriptheader PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_scriptheader PRIVATE Qt::Test Qt::Widgets)
# QETSql::execReadOnly() -- read-only enforcement for every project-database
# query, including the ones a .qet file carries. Compiles sqlreadonly.cpp
# alone against its own in-memory QSQLITE connection, so the security
@@ -379,6 +562,18 @@ target_link_libraries(tst_conductorselfretrace PRIVATE Qt::Test)
target_compile_definitions(tst_conductorselfretrace PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# A free text with a width keeps it when the project is saved again (the
# attribute is written only when set) and is drawn wrapped to it.
add_executable(
tst_freetextwidth
tst_freetextwidth.cpp)
add_test(NAME tst_freetextwidth COMMAND tst_freetextwidth)
set_tests_properties(tst_freetextwidth PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
add_dependencies(tst_freetextwidth qelectrotech)
target_link_libraries(tst_freetextwidth PRIVATE Qt::Test Qt::Gui Qt::Xml)
target_compile_definitions(tst_freetextwidth PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# The wiring list export (--export-cables) keeps every wire drawn to a folio
# report when a report has several, joins only one-to-one report pairs, and
# prints the folio number rather than its template. fixtures/wiring_list_arrows.qet
@@ -393,6 +588,113 @@ target_compile_definitions(tst_wiringlistexport PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# A label formula with ten or more sequential numbers gets the tenth value
# for %sequ_10 (not the first one and a "0"): the real binary's
# --export-bom on fixtures/ten_sequential_numbers.qet.
add_executable(
tst_tensequentialnumbers
tst_tensequentialnumbers.cpp)
add_test(NAME tst_tensequentialnumbers COMMAND tst_tensequentialnumbers)
add_dependencies(tst_tensequentialnumbers qelectrotech)
target_link_libraries(tst_tensequentialnumbers PRIVATE Qt::Test)
target_compile_definitions(tst_tensequentialnumbers PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# --export-dxf writes a wrapped symbol text line by line, as it is drawn.
add_executable(
tst_dxfwrappedtext
tst_dxfwrappedtext.cpp)
add_test(NAME tst_dxfwrappedtext COMMAND tst_dxfwrappedtext)
add_dependencies(tst_dxfwrappedtext qelectrotech)
target_link_libraries(tst_dxfwrappedtext PRIVATE Qt::Test)
target_compile_definitions(tst_dxfwrappedtext PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# --export-dxf on a folio with its title block at the right (issue #1339):
# a circle and an arc stay one CIRCLE and one ARC, the title block is turned.
add_executable(
tst_dxfcurves
tst_dxfcurves.cpp)
add_test(NAME tst_dxfcurves COMMAND tst_dxfcurves)
add_dependencies(tst_dxfcurves qelectrotech)
target_link_libraries(tst_dxfcurves PRIVATE Qt::Test)
target_compile_definitions(tst_dxfcurves 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(
tst_dxfblocks
tst_dxfblocks.cpp)
add_test(NAME tst_dxfblocks COMMAND tst_dxfblocks)
add_dependencies(tst_dxfblocks qelectrotech)
target_link_libraries(tst_dxfblocks PRIVATE Qt::Test)
target_compile_definitions(tst_dxfblocks PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# --export-pdf puts an A3 folio on an A3 page whichever way it is turned:
# QPageSize matches standard sheets upright only, so a wide folio is matched
# upright and turned. fixtures/pdf_page_a3.qet has one folio each way.
add_executable(
tst_pdfpagesize
tst_pdfpagesize.cpp)
add_test(NAME tst_pdfpagesize COMMAND tst_pdfpagesize)
add_dependencies(tst_pdfpagesize qelectrotech)
target_link_libraries(tst_pdfpagesize PRIVATE Qt::Test Qt::Gui)
target_compile_definitions(tst_pdfpagesize PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# --export-pdf writes the cross-reference links in the same order in every
# run, rather than in Element pointer order. Exports examples/industrial.qet
# several times, in separate processes.
add_executable(
tst_pdflinkorder
tst_pdflinkorder.cpp)
add_test(NAME tst_pdflinkorder COMMAND tst_pdflinkorder)
add_dependencies(tst_pdflinkorder qelectrotech)
target_link_libraries(tst_pdflinkorder PRIVATE Qt::Test)
target_compile_definitions(tst_pdflinkorder PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# One text per potential: the number goes on the same conductor in every
# run when two conductors tie for longest. Exports
# fixtures/one_text_per_potential_tie.qet through the real binary, several
# times, since the order it used to depend on changes only between runs.
add_executable(
tst_potentialtextcarrier
tst_potentialtextcarrier.cpp)
add_test(NAME tst_potentialtextcarrier COMMAND tst_potentialtextcarrier)
add_dependencies(tst_potentialtextcarrier qelectrotech)
target_link_libraries(tst_potentialtextcarrier PRIVATE Qt::Test)
target_compile_definitions(tst_potentialtextcarrier PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# Junction dots: a wire's corner that lies on another wire of the same
# potential gets a dot, even when the two share no terminal (issue #1280).
# Exports fixtures/junction_dot_shared_potential.qet through the real binary.
add_executable(
tst_junctiondots
tst_junctiondots.cpp)
add_test(NAME tst_junctiondots COMMAND tst_junctiondots)
add_dependencies(tst_junctiondots qelectrotech)
target_link_libraries(tst_junctiondots PRIVATE Qt::Test)
target_compile_definitions(tst_junctiondots PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# With SOURCE_DATE_EPOCH set, --export-pdf writes the same bytes for the
# same project in every run. Exports examples/741.qet several times, in
# separate processes.
add_executable(
tst_pdfreproducible
tst_pdfreproducible.cpp)
add_test(NAME tst_pdfreproducible COMMAND tst_pdfreproducible)
add_dependencies(tst_pdfreproducible qelectrotech)
target_link_libraries(tst_pdfreproducible PRIVATE Qt::Test)
target_compile_definitions(tst_pdfreproducible PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# Hops where wires cross (issue #436): the geometry in wirehops.cpp, and
# through the real binary, that --resave keeps the project setting and
# adds nothing to a project without it.
@@ -407,6 +709,31 @@ target_link_libraries(tst_wirehops PRIVATE Qt::Test Qt::Gui)
target_compile_definitions(tst_wirehops PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# Wires per terminal (discussion #1158): the rules in wiringrules.cpp, and
# through the real binary, that --resave keeps the project setting, adds
# none to a project without it, and that a wire past the limit is refused
# unless the master switch is off.
add_executable(
tst_wiringrules
tst_wiringrules.cpp
${QET_DIR}/sources/wiringrules.cpp)
add_test(NAME tst_wiringrules COMMAND tst_wiringrules)
add_dependencies(tst_wiringrules qelectrotech)
target_include_directories(tst_wiringrules PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_wiringrules PRIVATE Qt::Test Qt::Xml)
target_compile_definitions(tst_wiringrules PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# The routing behind qet.routeConductor(): conductorrouter.cpp needs only
# Qt Core, so the geometry is tested without a scene.
add_executable(
tst_conductorrouter
tst_conductorrouter.cpp
${QET_DIR}/sources/conductorrouter.cpp)
add_test(NAME tst_conductorrouter COMMAND tst_conductorrouter)
target_include_directories(tst_conductorrouter PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_conductorrouter PRIVATE Qt::Test)
# qet.conductorUuids() / qet.conductorEnds(): a script lists a folio's
# conductors by uuid and finds each one's two ends. Runs a script through
# the real binary's --run on fixtures/qet_bug_repro_resaved.qet, so only
@@ -422,6 +749,28 @@ if(QET_HAS_SCRIPTING)
target_compile_definitions(tst_scriptconductoruuid PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# qet.terminalPosition() and qet.conductorPath(), checked against each
# other on a fixture's conductors.
add_executable(
tst_scriptlayoutreads
tst_scriptlayoutreads.cpp)
add_test(NAME tst_scriptlayoutreads COMMAND tst_scriptlayoutreads)
add_dependencies(tst_scriptlayoutreads qelectrotech)
target_link_libraries(tst_scriptlayoutreads PRIVATE Qt::Test)
target_compile_definitions(tst_scriptlayoutreads PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# The project database keeps the width of a free text (independent_text),
# read with qet.query() on fixtures/free_text_width.qet.
add_executable(
tst_scriptfreetextwidth
tst_scriptfreetextwidth.cpp)
add_test(NAME tst_scriptfreetextwidth COMMAND tst_scriptfreetextwidth)
add_dependencies(tst_scriptfreetextwidth qelectrotech)
target_link_libraries(tst_scriptfreetextwidth PRIVATE Qt::Test)
target_compile_definitions(tst_scriptfreetextwidth PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# QET_SETTINGS_DIR moves the settings into an INI file there (#1178): a
# script places a symbol only the folder's settings file can resolve.
add_executable(
@@ -434,6 +783,18 @@ if(QET_HAS_SCRIPTING)
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_ELEMENTS_DIR=\"${QET_DIR}/elements\"")
# qet.addElement() on a symbol that exists but cannot be read says so,
# rather than reporting a collision with the copy already embedded.
add_executable(
tst_unreadableelement
tst_unreadableelement.cpp)
add_test(NAME tst_unreadableelement COMMAND tst_unreadableelement)
add_dependencies(tst_unreadableelement qelectrotech)
target_link_libraries(tst_unreadableelement PRIVATE Qt::Test)
target_compile_definitions(tst_unreadableelement PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_ELEMENTS_DIR=\"${QET_DIR}/elements\"")
# The project database filled from the file as it is read holds exactly
# what it holds filled from the built folios (QET_DATABASE_FROM_FOLIOS=1),
# on every example saved once; an older file falls back, saying why.
@@ -471,6 +832,68 @@ if(QET_HAS_SCRIPTING)
target_compile_definitions(tst_scriptterminaluuid PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# Mirroring a symbol on its folio (#1335): its terminals swap sides and
# face the other way, a second mirror puts it back, and the mirror is
# kept by a save. Through the real binary's --run on perceuse.qet.
add_executable(
tst_scriptmirror
tst_scriptmirror.cpp)
add_test(NAME tst_scriptmirror COMMAND tst_scriptmirror)
add_dependencies(tst_scriptmirror qelectrotech)
target_link_libraries(tst_scriptmirror PRIVATE Qt::Test Qt::Xml)
target_compile_definitions(tst_scriptmirror PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# A project can keep the texts of its turned symbols horizontal: the
# DXF of a turned folio of tremie_vibrante.qet with the setting off and on.
add_executable(
tst_uprightsymboltexts
tst_uprightsymboltexts.cpp)
add_test(NAME tst_uprightsymboltexts COMMAND tst_uprightsymboltexts)
add_dependencies(tst_uprightsymboltexts qelectrotech)
target_link_libraries(tst_uprightsymboltexts PRIVATE Qt::Test)
target_compile_definitions(tst_uprightsymboltexts PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# Undoing a crop of a picture restores its crop rectangle too, so a
# project saved after the undo does not record the undone crop.
add_executable(
tst_imagecropundo
tst_imagecropundo.cpp)
add_test(NAME tst_imagecropundo COMMAND tst_imagecropundo)
add_dependencies(tst_imagecropundo qelectrotech)
target_link_libraries(tst_imagecropundo PRIVATE Qt::Test Qt::Gui)
target_compile_definitions(tst_imagecropundo PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# The same symbol placed twice in a project whose collection does not
# start with its "import" category (examples/lmdg.qet).
add_executable(
tst_importcategory
tst_importcategory.cpp)
add_test(NAME tst_importcategory COMMAND tst_importcategory)
add_dependencies(tst_importcategory qelectrotech)
target_link_libraries(tst_importcategory PRIVATE Qt::Test)
target_compile_definitions(tst_importcategory PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\""
"QET_ELEMENTS_DIR=\"${QET_DIR}/elements\"")
# qet.currentFolio() through --run: the first folio, or -1 with none; and
# --run keeps one undo step per call (only an editor run is grouped).
add_executable(
tst_scriptcurrentfolio
tst_scriptcurrentfolio.cpp)
add_test(NAME tst_scriptcurrentfolio COMMAND tst_scriptcurrentfolio)
add_dependencies(tst_scriptcurrentfolio qelectrotech)
target_link_libraries(tst_scriptcurrentfolio PRIVATE Qt::Test)
target_compile_definitions(tst_scriptcurrentfolio PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
endif()
# A symbol saved without a uuid gets the same one on every load, and
@@ -499,6 +922,35 @@ target_compile_definitions(tst_derivedwireuuid 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.
if(QET_HAS_SCRIPTING)
add_executable(
tst_elementautonumids
tst_elementautonumids.cpp)
add_test(NAME tst_elementautonumids COMMAND tst_elementautonumids)
add_dependencies(tst_elementautonumids qelectrotech)
target_link_libraries(tst_elementautonumids PRIVATE Qt::Test Qt::Xml)
target_compile_definitions(tst_elementautonumids PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
endif()
# A picture whose saved crop does not match the picture shown (an undone
# crop, saved before #1310) loads without that crop; a valid crop is kept.
if(QET_HAS_SCRIPTING)
add_executable(
tst_imagestalecrop
tst_imagestalecrop.cpp)
add_test(NAME tst_imagestalecrop COMMAND tst_imagestalecrop)
add_dependencies(tst_imagestalecrop qelectrotech)
target_link_libraries(tst_imagestalecrop PRIVATE Qt::Test Qt::Gui)
target_compile_definitions(tst_imagestalecrop PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
endif()
# Saving a project that was just saved changes nothing: runs the real
# binary's --resave twice on every project in examples/, and on one whose
# title block holds a single-space value (#973).
@@ -513,6 +965,18 @@ target_compile_definitions(tst_resaveunchanged PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# A folio's "freeze new elements/conductors" flags, saved as the words
# "true"/"false", are read back as words: the real binary's --resave on
# fixtures/folio_freeze_flags.qet, one folio per combination.
add_executable(
tst_foliofreezeflags
tst_foliofreezeflags.cpp)
add_test(NAME tst_foliofreezeflags COMMAND tst_foliofreezeflags)
add_dependencies(tst_foliofreezeflags qelectrotech)
target_link_libraries(tst_foliofreezeflags PRIVATE Qt::Test Qt::Xml)
target_compile_definitions(tst_foliofreezeflags PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# Replacing a symbol's definition in a project keeps its terminal uuids, so
# the wires saved against them still load: TerminalUuids::keep() on its own,
# then the real binary's --info on an example whose symbols were replaced.
@@ -527,6 +991,19 @@ target_compile_definitions(tst_terminaluuids PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# Sequential numbers saved the old way, as sequ_1/.../seqhf_1 attributes,
# are all read: the real binary's --resave on
# fixtures/legacy_sequential_attributes.qet, whose element and conductor
# carry only seqhf_1.
add_executable(
tst_legacysequentialattributes
tst_legacysequentialattributes.cpp)
add_test(NAME tst_legacysequentialattributes COMMAND tst_legacysequentialattributes)
add_dependencies(tst_legacysequentialattributes qelectrotech)
target_link_libraries(tst_legacysequentialattributes PRIVATE Qt::Test Qt::Xml)
target_compile_definitions(tst_legacysequentialattributes PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# ConductorProperties::applyChanges() -- the Selection properties panel
# writes only the fields the user edited.
add_executable(
@@ -540,6 +1017,34 @@ add_test(NAME tst_conductorapplychanges COMMAND tst_conductorapplychanges)
target_include_directories(tst_conductorapplychanges PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_conductorapplychanges PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
# ConductorProperties::fromSettings() -- the default conductor width is a
# decimal and comes back as saved (1.4 used to be read as 0).
add_executable(
tst_conductorsizesetting
tst_conductorsizesetting.cpp
${QET_DIR}/sources/conductorproperties.cpp
${QET_DIR}/sources/qet.cpp
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/shortcutmanager.cpp)
add_test(NAME tst_conductorsizesetting COMMAND tst_conductorsizesetting)
target_include_directories(tst_conductorsizesetting PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_conductorsizesetting PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
# ConductorMultiEdit -- editing several wires at once in the Selection
# properties panel: order, which wires an edit reaches, one undo step for
# all of them, and fields shown blank because the wires disagree.
add_executable(
tst_conductormultiedit
tst_conductormultiedit.cpp
${QET_DIR}/sources/conductorproperties.cpp
${QET_DIR}/sources/QPropertyUndoCommand/qpropertyundocommand.cpp
${QET_DIR}/sources/qet.cpp
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/shortcutmanager.cpp)
add_test(NAME tst_conductormultiedit COMMAND tst_conductormultiedit)
target_include_directories(tst_conductormultiedit PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_conductormultiedit PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
# DiagramContext::fromXml() -- the two readers (QDom for projects, pugixml
# for element definitions in the collection) give the same values: stray
# spaces trimmed, accents kept.
@@ -553,3 +1058,37 @@ add_executable(
add_test(NAME tst_diagramcontext COMMAND tst_diagramcontext)
target_include_directories(tst_diagramcontext PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_diagramcontext PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
# XRefProperties: an empty or unknown cross-reference position, as older
# versions saved it, reads as the default instead of drawing the
# cross-reference over the element's label (issue #1238).
add_executable(
tst_xrefpos
tst_xrefpos.cpp
${QET_DIR}/sources/properties/xrefproperties.cpp
${QET_DIR}/sources/properties/propertiesinterface.cpp)
add_test(NAME tst_xrefpos COMMAND tst_xrefpos)
target_include_directories(tst_xrefpos PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_xrefpos PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
# BorderProperties::fromXml() -- the project's <border> for new folios:
# column width and row height are decimals and are read as such.
add_executable(
tst_borderpropertiesxml
tst_borderpropertiesxml.cpp
${QET_DIR}/sources/borderproperties.cpp)
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)
# 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
# hash seed makes the order stable; it changes nothing for a newer Qt.
if(Qt6_VERSION VERSION_LESS 6.5.0)
foreach(qet_test tst_resaveunchanged tst_elementautonumids)
if(TEST ${qet_test})
set_tests_properties(${qet_test} PROPERTIES ENVIRONMENT "QT_HASH_SEED=0")
endif()
endforeach()
endif()
+112
View File
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Real item/key-event regression, linked against the application's objects.
#include <QApplication>
#include <QClipboard>
#include <QDomDocument>
#include <QFile>
#include <QFontDatabase>
#include <QGraphicsView>
#include <QGlyphRun>
#include <QKeyEvent>
#include <QKeySequence>
#include <QMimeData>
#include <QRawFont>
#include <QSettings>
#include <QTemporaryDir>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextLayout>
#include <cstdio>
#include <stdexcept>
#include "../../sources/diagram.h"
#include "../../sources/qetproject.h"
#include "../../sources/qetresult.h"
#include "../../sources/qetmessagebox.h"
#include "../../sources/qetgraphicsitem/dynamicelementtextitem.h"
#include "../../sources/qetgraphicsitem/independenttextitem.h"
static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); }
static void key(Diagram *scene, QGraphicsItem *item, int code, Qt::KeyboardModifiers modifiers) {
QKeyEvent event(QEvent::KeyPress,code,modifiers); scene->sendEvent(item,&event);
}
static void redo(Diagram *scene, QGraphicsItem *item) {
// Qt uses Ctrl+Y on Windows and Ctrl+Shift+Z on Linux.
const QKeySequence shortcut(QKeySequence::Redo);
check(shortcut.count()==1,"platform redo shortcut is a single key combination");
const auto combination=shortcut[0];
QKeyEvent event(QEvent::KeyPress,combination.key(),combination.keyboardModifiers());
check(event.matches(QKeySequence::Redo),"generated key matches platform Redo");
scene->sendEvent(item,&event);
}
static void clipboard(const QString &html, const QString &plain = QString(), bool withPlain = false) {
auto *mime=new QMimeData; mime->setHtml(html); if(withPlain) mime->setText(plain); QApplication::clipboard()->setMimeData(mime);
}
static void select(QGraphicsTextItem *item,int start,int end) {
auto cursor=item->textCursor(); cursor.setPosition(start); cursor.setPosition(end,QTextCursor::KeepAnchor); item->setTextCursor(cursor);
}
static void fieldStyle(QGraphicsTextItem *item,int start,int length,const QFont &field) {
item->document()->documentLayout(); item->boundingRect();
for(int i=start;i<start+length;++i) {
if(item->toPlainText().at(i)=='\n') continue;
QTextCursor cursor(item->document()); cursor.setPosition(i); cursor.setPosition(i+1,QTextCursor::KeepAnchor);
const auto format=cursor.charFormat(); const QFont resolved=format.font().resolve(item->document()->defaultFont());
check(resolved.family()==field.family()&&qAbs(resolved.pointSizeF()-field.pointSizeF())<0.001,"inserted character resolves to field font/size");
check(!format.hasProperty(QTextFormat::ForegroundBrush)&&!format.isAnchor(),"clipboard colour/link discarded");
const auto block=cursor.block(); const auto runs=block.layout()->glyphRuns(i-block.position(),1);
check(!runs.isEmpty(),"inserted character laid out");
for(const auto &run:runs) check(qAbs(run.rawFont().pixelSize()-QRawFont::fromFont(field).pixelSize())<0.01,"actual rendered font size follows field");
}
}
int main(int argc,char **argv) {
QApplication app(argc,argv); std::freopen(argv[2],"w",stdout);
QTemporaryDir settings; QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat,QSettings::UserScope,settings.path()); QCoreApplication::setOrganizationName("QETPlainPasteRegression");
QETProject::setBackupEnabled(false); QET::QetMessageBox::setNonInteractive(true);
QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf");
try {
const QString output=QString::fromLocal8Bit(argv[3]);
QETProject project(QString::fromLocal8Bit(argv[1])); check(project.state()==QETProject::Ok,"fixture opens");
auto *scene=project.diagrams().first(); auto *text=scene->elements().first()->dynamicTextItems().first();
text->setTextFrom(DynamicElementTextItem::UserText);
const QFont field("Liberation Sans",11); text->setFont(field);
QGraphicsView view(scene); view.resize(400,300); view.show(); view.activateWindow(); QApplication::setActiveWindow(&view); view.setFocus(); QApplication::processEvents();
auto edit=[&] { text->setTextInteractionFlags(Qt::TextEditorInteraction); text->setFocus(); scene->setFocus(); QApplication::processEvents(); check(text->hasFocus(),"real editing focus"); };
auto paste=[&] { key(scene,text,Qt::Key_V,Qt::ControlModifier); };
const QString rich="<p><a href='https://example.org'><span style='font-family:Courier New;font-size:40pt;color:#ff0000'><b><i>new</i></b></span></a></p><p>line</p>";
text->setText("old"); edit(); select(text,0,3); clipboard(rich); paste();
check(text->toPlainText()=="new\nline","HTML-only clipboard converts to plain with newlines"); fieldStyle(text,0,8,field);
key(scene,text,Qt::Key_Z,Qt::ControlModifier); check(text->toPlainText()=="old","native undo is one paste step");
redo(scene,text); check(text->toPlainText()=="new\nline","native redo"); fieldStyle(text,0,8,field);
scene->clearFocus(); QApplication::processEvents(); check(text->text()=="new\nline","editing session updates stored user text");
scene->undoStack().undo(); check(text->toPlainText()=="old","project undo"); scene->undoStack().redo(); check(text->toPlainText()=="new\nline","project redo");
std::puts("PASS: HTML-only paste, newlines, field font/size, native and project undo/redo");
text->setText("AB"); text->setHtml("<p style='font-family:Courier New;font-size:36pt'><b><i>AB</i></b></p>");
edit(); select(text,2,2); clipboard("<span style='font-size:50pt'>ignored</span>","tail",true); paste();
check(text->toPlainText()=="ABtail","clipboard plain representation takes precedence"); fieldStyle(text,2,4,field);
QTextCursor prior(text->document()); prior.setPosition(0); prior.setPosition(1,QTextCursor::KeepAnchor); check(prior.charFormat().fontWeight()>=QFont::Bold,"existing preceding format untouched");
select(text,1,4); clipboard("<span style='font-size:50pt'>Z</span>"); paste(); check(text->toPlainText()=="AZil","selection replaced in formatted document"); fieldStyle(text,1,1,field);
key(scene,text,Qt::Key_Z,Qt::ControlModifier); check(text->toPlainText()=="ABtail","selection paste undo");
redo(scene,text); check(text->toPlainText()=="AZil","selection paste redo");
scene->clearFocus(); QApplication::processEvents();
std::puts("PASS: paste after formatted text, plain/HTML precedence, selection replacement and undo/redo");
text->setText("before"); edit(); select(text,0,6); clipboard("<b>ignored</b>","one\ntwo\nthree",true); paste(); fieldStyle(text,0,13,field); scene->clearFocus(); QApplication::processEvents();
project.setFilePath(output+"/plain.qet"); check(project.write().isOk(),"save project");
QDomDocument xml; QFile file(project.filePath()); check(file.open(QIODevice::ReadOnly),"read saved project"); check(bool(xml.setContent(file.readAll())),"parse saved project");
bool found=false; const auto nodes=xml.elementsByTagName("dynamic_elmt_text");
for(int i=0;i<nodes.size();++i) { auto node=nodes.at(i).toElement(); if(node.attribute("uuid")==text->uuid().toString()) { found=true; check(node.firstChildElement("html").isNull(),"no new HTML persistence"); check(node.firstChildElement("text").text()=="one\ntwo\nthree","existing plain XML text"); } }
check(found,"attached text saved"); QETProject reopened(project.filePath()); check(reopened.state()==QETProject::Ok,"reopen project");
DynamicElementTextItem *loaded=nullptr; for(auto *element:reopened.diagrams().first()->elements()) for(auto *item:element->dynamicTextItems()) if(item->uuid()==text->uuid()) loaded=item;
check(loaded&&loaded->toPlainText()=="one\ntwo\nthree"&&loaded->font()==field,"plain text and field font survive reopen"); fieldStyle(loaded,0,13,field);
std::puts("PASS: multiline clipboard, unchanged XML representation and save/reopen");
QKeyEvent origin(QEvent::KeyPress,Qt::Key_V,Qt::ControlModifier|Qt::ShiftModifier); check(!origin.matches(QKeySequence::Paste),"Ctrl+Shift+V excluded from standard Paste");
edit(); const QString before=text->toPlainText(); clipboard("<b>do not paste</b>"); scene->sendEvent(text,&origin); check(text->toPlainText()==before,"Ctrl+Shift+V not intercepted"); scene->clearFocus();
auto *free=new IndependentTextItem; scene->addItem(free); free->setPlainText("free"); free->setTextInteractionFlags(Qt::TextEditorInteraction); free->setFocus(); scene->setFocus(); QApplication::processEvents(); select(free,0,4);
clipboard("<p><b>bold</b> <i>italic</i> <span style='font-size:20pt'>large</span></p>"); key(scene,free,Qt::Key_V,Qt::ControlModifier);
check(free->document()->find("bold").charFormat().fontWeight()>=QFont::Bold&&free->document()->find("italic").charFormat().fontItalic()&&free->document()->find("large").charFormat().fontPointSize()==20,"free-text rich paste unchanged");
scene->clearFocus(); QApplication::processEvents(); project.setFilePath(output+"/free.qet"); check(project.write().isOk(),"save free text");
QETProject freeReopened(project.filePath()); check(freeReopened.state()==QETProject::Ok,"reopen free text"); bool freeFound=false;
for(auto *item:freeReopened.diagrams().first()->items()) if(auto *loadedFree=dynamic_cast<IndependentTextItem *>(item)) if(loadedFree->uuid()==free->uuid()) { freeFound=true; check(loadedFree->document()->find("bold").charFormat().fontWeight()>=QFont::Bold&&loadedFree->document()->find("italic").charFormat().fontItalic()&&loadedFree->document()->find("large").charFormat().fontPointSize()==20,"free-text formatting survives reopen"); }
check(freeFound,"free text saved"); std::puts("PASS: Ctrl+Shift+V excluded; free-text rich paste and reopen unchanged");
} catch(const std::exception &error) { std::printf("FAIL: %s\n",error.what()); return 1; }
return 0;
}
@@ -0,0 +1,270 @@
<project title="F020" uuid="{484e3ed4-bbda-5334-a526-d2ceddc55f0a}" 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">skeleton</property>
<property name="savedfilepath" show="1">/tmp/qet-mcp-new-2fmbrnvi/skeleton.qet</property>
<property name="savedtime" show="1">19:29</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="{4bc342c7-122c-4846-9b05-5ee7d6dc677b}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="X" type="embed://import/10_electric/10_allpole/130_terminals_terminal_strips/borne_2.elmt" uuid="{4c30414d-8678-47b8-b674-a1b87fe0347b}" x="200" y="200" z="10">
<terminals>
<terminal id="0" orientation="0" x="0" y="-6"/>
<terminal id="1" orientation="1" x="6" y="0"/>
<terminal id="2" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<elementInformations>
<elementInformation name="machine_manufacturer_reference_auxiliary1" show="1">MMR-AUX1</elementInformation>
<elementInformation name="machine_manufacturer_reference_auxiliary2" show="1">MMR-AUX2</elementInformation>
<elementInformation name="machine_manufacturer_reference_auxiliary3" show="1">MMR-AUX3</elementInformation>
<elementInformation name="machine_manufacturer_reference_auxiliary4" show="1">MMR-AUX4</elementInformation>
<elementInformation name="manufacturer_reference_auxiliary1" show="1">MR-AUX1</elementInformation>
<elementInformation name="manufacturer_reference_auxiliary2" show="1">MR-AUX2</elementInformation>
<elementInformation name="manufacturer_reference_auxiliary3" show="1">MR-AUX3</elementInformation>
<elementInformation name="manufacturer_reference_auxiliary4" show="1">MR-AUX4</elementInformation>
</elementInformations>
<properties>
<element_type terminal_function="generic" terminal_led="false" terminal_type="generic"/>
</properties>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{17dea734-db18-4b95-a27a-c4bb68473919}" x="3" y="-24.5">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,0,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="CompositeText" text_width="-1" uuid="{533b3793-9867-4a95-9ae9-bf945c8bb8d3}" x="30" y="10">
<text>[%{machine_manufacturer_reference_auxiliary1}|MR-AUX1]</text>
<composite_text>[%{machine_manufacturer_reference_auxiliary1}|%{manufacturer_reference_auxiliary1}]</composite_text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,0,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="CompositeText" text_width="-1" uuid="{2b1d4d47-e88f-4212-82b6-549b1565289d}" x="30" y="20">
<text>[%{machine_manufacturer_reference_auxiliary2}|MR-AUX2]</text>
<composite_text>[%{machine_manufacturer_reference_auxiliary2}|%{manufacturer_reference_auxiliary2}]</composite_text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,0,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="CompositeText" text_width="-1" uuid="{dc286aae-6c3e-4ee5-894c-205b06b538b0}" x="30" y="30">
<text>[%{machine_manufacturer_reference_auxiliary3}|MR-AUX3]</text>
<composite_text>[%{machine_manufacturer_reference_auxiliary3}|%{manufacturer_reference_auxiliary3}]</composite_text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,0,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="CompositeText" text_width="-1" uuid="{cf6e8c75-b728-46c1-b0d4-c93b84689ce6}" x="30" y="40">
<text>[%{machine_manufacturer_reference_auxiliary4}|MR-AUX4]</text>
<composite_text>[%{machine_manufacturer_reference_auxiliary4}|%{manufacturer_reference_auxiliary4}]</composite_text>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
</diagram>
<collection>
<category name="import">
<names>
<name lang="ca">Elements importats</name>
<name lang="cs">Zavedené prvky</name>
<name lang="da">Importerede elementer</name>
<name lang="de">Importierte Elemente</name>
<name lang="el">Εισηγμένα στοιχεία</name>
<name lang="en">Imported elements</name>
<name lang="es">Elementos importados</name>
<name lang="fr">Éléments importés</name>
<name lang="hr">Uvezeni elementi</name>
<name lang="it">Elementi importati</name>
<name lang="ja">インバートされた要素</name>
<name lang="ko">전기 심볼 요소</name>
<name lang="ko_KR">전기 심볼 요소</name>
<name lang="nl">Elementen geïmporteerd</name>
<name lang="nl_BE">Elementen geïmporteerd</name>
<name lang="pl">Elementy importowane</name>
<name lang="pt">Elementos importados</name>
<name lang="pt_BR">Elementos importados</name>
<name lang="ro">Elemente importate</name>
<name lang="ru">Импортированные элементы</name>
<name lang="sl">Uvoženi elementi</name>
<name lang="sv">Importerade element</name>
<name lang="tr">İthal öğeler</name>
<name lang="uk">Імпортовані елементи</name>
<name lang="zh">导入元件</name>
</names>
<category name="10_electric">
<names>
<name lang="ar">كهربائي</name>
<name lang="ca">Elèctric</name>
<name lang="cs">Elektrotechnika</name>
<name lang="da">Elektrisk</name>
<name lang="de">Elektrik</name>
<name lang="el">Ηλεκτρικά</name>
<name lang="en">Electric</name>
<name lang="es">Eléctrica</name>
<name lang="fi">Sähkö</name>
<name lang="fr">Electrique</name>
<name lang="hr">Električno</name>
<name lang="hu">Elektromos</name>
<name lang="it">Elettrica</name>
<name lang="ja">電気</name>
<name lang="ko">전기</name>
<name lang="mn">Цахилгаан</name>
<name lang="nb">Elektrisk</name>
<name lang="nl">Elektrotechniek</name>
<name lang="nl_BE">Elektrisch</name>
<name lang="pl">Elektrotechnika</name>
<name lang="pt">Elétrico</name>
<name lang="pt_BR">Elétrica</name>
<name lang="ro">Electric</name>
<name lang="ru">Электротехника</name>
<name lang="sk">Elektrické</name>
<name lang="sl">Elektrika</name>
<name lang="sr">Електрика</name>
<name lang="sv">El</name>
<name lang="tr">Elektrik</name>
<name lang="uk">Електрика</name>
<name lang="zh">10_电气</name>
<name lang="zn">电气工程</name>
</names>
<category name="10_allpole">
<names>
<name lang="ar">متعدد الأقطاب</name>
<name lang="ca">Multifilar</name>
<name lang="cs">Vícežilový</name>
<name lang="da">Flere ledere</name>
<name lang="de">Allpolig</name>
<name lang="el">Πολυγραμμικό</name>
<name lang="en">All-pole</name>
<name lang="es">Multifilar</name>
<name lang="fi">Yleisnapa</name>
<name lang="fr">Multifilaire</name>
<name lang="hr">Univerzalni</name>
<name lang="hu">Többvonalas szimbólumok</name>
<name lang="it">Multifilare</name>
<name lang="ja">複線</name>
<name lang="ko">다-극</name>
<name lang="mn">Бүх туйл</name>
<name lang="nb">Alle poler</name>
<name lang="nl">Veel polig</name>
<name lang="nl_BE">All-pole</name>
<name lang="pl">Schematy wieloliniowe</name>
<name lang="pt">Multipolar</name>
<name lang="pt_BR">Multifilar</name>
<name lang="ro">Bipolar</name>
<name lang="ru">Многополюсные</name>
<name lang="sk">Všepólové</name>
<name lang="sl">Vsi poli</name>
<name lang="sr">Свe-полни</name>
<name lang="sv">Allpolig</name>
<name lang="tr">Çok kutuplu</name>
<name lang="uk">Багатополюсні</name>
<name lang="zh">10_全极</name>
</names>
<category name="130_terminals_terminal_strips">
<names>
<name lang="ar">طرفيات توصيل</name>
<name lang="ca">Borns i regletes de borns</name>
<name lang="cs">Svorky</name>
<name lang="da">Klemmer og klemmerækker</name>
<name lang="de">Klemmen und Klemmleisten</name>
<name lang="el">Ακροδέκτες και σειρές ακροδεκτών</name>
<name lang="en">Terminals and terminal strips</name>
<name lang="es">Terminales</name>
<name lang="fi">Rimaelittimet</name>
<name lang="fr">Bornes et borniers</name>
<name lang="hr">Stezaljke i redne stezaljke</name>
<name lang="hu">Sorkapcsok</name>
<name lang="it">Terminali</name>
<name lang="ja">端子と端子台</name>
<name lang="ko">단자 및 단자대</name>
<name lang="mn">Клемм ба клеммын хавтан</name>
<name lang="nb">Rekkeklemmer og klemlister</name>
<name lang="nl">Klemmenlijst</name>
<name lang="nl_BE">Klemmen en klemmenstroken</name>
<name lang="pl">Złącza i zaciski</name>
<name lang="pt">Terminais</name>
<name lang="pt_BR">Bornes e borneiras</name>
<name lang="ro">Borne și borniere</name>
<name lang="ru">Клеммы и клеммники</name>
<name lang="sk">Svorky a svorkovnice</name>
<name lang="sl">Vrstne sponke in sponke</name>
<name lang="sr">Клеме и редне клеме</name>
<name lang="sv">Plintar och plintrader</name>
<name lang="tr">Klemensler ve klemens şeritleri</name>
<name lang="uk">Клеми та клемні колодки</name>
<name lang="zh">接线端子与端子排</name>
</names>
<element name="borne_2.elmt">
<definition height="40" hotspot_x="10" hotspot_y="20" link_type="terminal" type="element" version="0.100.0" width="30">
<uuid uuid="{3f1985d3-104c-d0fe-e025-de0b19429826}"/>
<names>
<name lang="ar">طرف توصيل</name>
<name lang="ca">Born de continuïtat</name>
<name lang="cs">Spojovací svorka</name>
<name lang="da">Klemme</name>
<name lang="de">Klemme</name>
<name lang="el">Ακροδέκτης</name>
<name lang="en">Terminal block</name>
<name lang="es">Terminal de unión</name>
<name lang="fi">Liitinrima</name>
<name lang="fr">Borne continuité</name>
<name lang="hr">Stezaljka</name>
<name lang="hu">Sorkapocs</name>
<name lang="it">Terminale di unione</name>
<name lang="ja">端子ブロック</name>
<name lang="ko">단자 블록</name>
<name lang="mn">Клеммын блок</name>
<name lang="nb">Rekkeklemme</name>
<name lang="nl">klem doorgang</name>
<name lang="nl_BE">Klemmenblok</name>
<name lang="pl">Zacisk</name>
<name lang="pt">Terminal de combinação</name>
<name lang="pt_BR">Borne de combinacao</name>
<name lang="ro">Bloc de borne</name>
<name lang="ru">Клеммное соединение</name>
<name lang="sk">Svorkovnica</name>
<name lang="sl">Vrstna sponka</name>
<name lang="sr">Клемни блок</name>
<name lang="sv">Plint</name>
<name lang="tr">Klemens bloğu</name>
<name lang="uk">Клемна колодка</name>
<name lang="zh">2号接线端子</name>
</names>
<kindInformations>
<kindInformation name="type">generic</kindInformation>
<kindInformation name="function">generic</kindInformation>
</kindInformations>
<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="10" y2="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="0" x2="0" y1="-10" y2="-3"/>
<ellipse antialias="true" height="5" style="line-style:normal;line-weight:normal;filling:none;color:black" width="5" x="-2.5" y="-2.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="3" y1="0" y2="0"/>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{8e248a1f-fe85-48a3-8940-9a66b8032f11}" x="3" y="-24.5" z="5">
<text/>
<info_name>label</info_name>
</dynamic_text>
<terminal name="" orientation="n" type="Generic" uuid="{323e68e1-514f-4e03-8ece-f22744a2d325}" x="0" y="-10"/>
<terminal name="" orientation="s" type="Generic" uuid="{c81262f0-711b-451d-a333-9039f4de25b6}" x="0" y="10"/>
<terminal name="" orientation="e" type="Generic" uuid="{a33d1699-88d4-4071-a651-977d380e6e8c}" x="10" y="0"/>
</description>
</definition>
</element>
</category>
</category>
</category>
</category>
</collection>
</project>
+79
View File
@@ -0,0 +1,79 @@
<project title="DXF symbol blocks" uuid="{7d2b8e3f-0c65-4e30-9079-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">dxf_blocks</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="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a01}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/curves.elmt" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a02}" x="300" y="300" z="10">
<terminals/>
<inputs/>
<elementInformations>
<elementInformation name="label" show="1">K1</elementInformation>
<elementInformation name="manufacturer" show="1">Schneider</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7b01}" x="0" y="25">
<text>K1</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/curves.elmt" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a03}" x="500" y="300" z="10">
<terminals/>
<inputs/>
<elementInformations>
<elementInformation name="label" show="1">K2</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7b02}" x="0" y="25">
<text>K2</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
</diagram>
<collection>
<category name="import">
<names>
<name lang="en">Imported elements</name>
</names>
<element name="curves.elmt">
<definition height="80" hotspot_x="10" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="120">
<uuid uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a05}"/>
<names>
<name lang="en">Circle and arc</name>
</names>
<description>
<ellipse antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="0" y="0" width="20" height="20"/>
<arc antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="40" y="0" width="20" height="20" start="30" angle="120"/>
</description>
</definition>
</element>
</category>
</collection>
</project>
+56
View File
@@ -0,0 +1,56 @@
<project title="DXF curves and title block" uuid="{6c1a7d2e-9b54-4d2f-8f68-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">dxf_curves</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="right" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{6c1a7d2e-9b54-4d2f-8f68-3f0d0f0a7a01}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/curves.elmt" uuid="{6c1a7d2e-9b54-4d2f-8f68-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="curves.elmt">
<definition height="80" hotspot_x="10" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="120">
<uuid uuid="{6c1a7d2e-9b54-4d2f-8f68-3f0d0f0a7a05}"/>
<names>
<name lang="en">Circle and arc</name>
</names>
<description>
<ellipse antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="0" y="0" width="20" height="20"/>
<arc antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="40" y="0" width="20" height="20" start="30" angle="120"/>
</description>
</definition>
</element>
</category>
</collection>
</project>
@@ -0,0 +1,60 @@
<project title="DXF wrapped texts" uuid="{5b0f5e1c-8a43-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">dxf_wrapped_text</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="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a01}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/wrap.elmt" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a02}" x="300" y="300" z="10">
<terminals/>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="UserText" text_width="70" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b01}" x="0" y="10"><text>WrapAlpha WrapBeta WrapGamma WrapDelta WrapEpsilon</text></dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b02}" x="0" y="60"><text>LineAlpha LineBeta LineGamma LineDelta LineEpsilon</text></dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
</diagram>
<collection>
<category name="import">
<names>
<name lang="en">Imported elements</name>
</names>
<element name="wrap.elmt">
<definition height="80" hotspot_x="10" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="120">
<uuid uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a05}"/>
<names>
<name lang="en">Wrapped symbol text</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="10" y1="0" y2="0"/>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" z="2" text_from="UserText" text_width="70" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b01}" x="0" y="10"><text>WrapAlpha WrapBeta WrapGamma WrapDelta WrapEpsilon</text></dynamic_text>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" z="3" text_from="UserText" text_width="-1" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b02}" x="0" y="60"><text>LineAlpha LineBeta LineGamma LineDelta LineEpsilon</text></dynamic_text>
</description>
</definition>
</element>
</category>
</collection>
</project>
@@ -0,0 +1,36 @@
<project title="Folio freeze flags" uuid="{7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e00}" version="0.200.1">
<properties>
<property name="savedfilename" show="1">folio_freeze_flags</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/>
<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="true" freezeNewElement="true" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="both frozen" uuid="{7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e01}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
</diagram>
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="true" height="660" indexrev="" locmach="" order="2" plant="" rows="8" rowsize="80" title="elements frozen" uuid="{7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e02}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
</diagram>
<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="3" plant="" rows="8" rowsize="80" title="not frozen" uuid="{7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e03}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
</diagram>
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" height="660" indexrev="" locmach="" order="4" plant="" rows="8" rowsize="80" title="no attributes" uuid="{7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e04}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
</diagram>
<collection>
<category name="import">
<names>
<name lang="en">Imported elements</name>
</names>
</category>
</collection>
</project>
+41
View File
@@ -0,0 +1,41 @@
<project title="Free text widths" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a00}" 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">free_text_width</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="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a01}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<inputs>
<input font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" rotation="0" text="FreeAlpha FreeBeta FreeGamma FreeDelta FreeEpsilon" text_width="70" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a03}" x="100" y="100"/>
<input font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" rotation="0" text="OpenAlpha OpenBeta OpenGamma OpenDelta OpenEpsilon" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a04}" x="100" y="400"/>
<input font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" rotation="0" text="&lt;p align=&quot;center&quot;&gt;CentAlpha CentBeta CentGamma&lt;/p&gt;" text_width="120" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a08}" x="400" y="100"/>
</inputs>
</diagram>
<collection>
<category name="import">
<names>
<name lang="en">Imported elements</name>
</names>
</category>
</collection>
</project>
@@ -0,0 +1,354 @@
<project title="i1280" uuid="{abc27554-b301-5651-8055-41e0ce4599cb}" version="0.200.1">
<properties>
<property name="saveddate" show="1">10/4/26</property>
<property name="saveddate-eu" show="1">04-10-2026</property>
<property name="saveddate-us" show="1">2026-10-04</property>
<property name="savedfilename" show="1">skeleton</property>
<property name="savedfilepath" show="1">/tmp/qet-mcp-new-jk9lz64u/skeleton.qet</property>
<property name="savedtime" show="1">10:28</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="{a94486ed-a490-4ae0-9698-b075035de54d}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" x="100" y="300" z="10">
<terminals>
<terminal id="0" orientation="0" x="0" y="-26"/>
<terminal id="1" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{1b01d6c6-86f7-44e4-8e76-eba4b3a53dea}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{65cedcc2-1a82-4168-858c-8751bc1561d9}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{91920aeb-94ef-423e-a282-206489f04e5e}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" x="200" y="300" z="10">
<terminals>
<terminal id="2" orientation="0" x="0" y="-26"/>
<terminal id="3" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{827f6a67-cf32-448a-b33a-e52fbbb9cee7}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{8fe1bb6c-bd54-429f-b8b9-2b1ca789ed2c}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{15fa6b73-1702-4e17-b821-3bd12f81e7ba}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" x="300" y="300" z="10">
<terminals>
<terminal id="4" orientation="0" x="0" y="-26"/>
<terminal id="5" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{93d83639-c2c1-4af5-9048-c0a5e0f241b8}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{c14602f0-24e8-463c-a183-3dac7f78e009}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{662e7f9d-9f6d-40b4-b517-7b584f6b3604}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" x="400" y="300" z="10">
<terminals>
<terminal id="6" orientation="0" x="0" y="-26"/>
<terminal id="7" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{3c604f1b-1498-4b2b-9f2d-0063e2bcb78c}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{dbb98cf7-9d66-4d99-91d9-2149077d6ba2}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{a8efc4c9-bfb3-4bde-a501-6b1bd5f93ae5}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
<conductors>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{f5162c59-0c59-4e94-b6a6-4cf259a52d91}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
<sequentialNumbers/>
</conductor>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{89cacce1-b12a-4ec3-aadf-d3f8feb7494d}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
<sequentialNumbers/>
</conductor>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element1_label="" element1_name="Normally open counter" element2="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{944a0069-bad6-4d92-91ad-7aedf3961c8a}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
<sequentialNumbers/>
</conductor>
</conductors>
</diagram>
<collection>
<category name="import">
<names>
<name lang="ca">Elements importats</name>
<name lang="cs">Zavedené prvky</name>
<name lang="da">Importerede elementer</name>
<name lang="de">Importierte Elemente</name>
<name lang="el">Εισηγμένα στοιχεία</name>
<name lang="en">Imported elements</name>
<name lang="es">Elementos importados</name>
<name lang="fr">Éléments importés</name>
<name lang="hr">Uvezeni elementi</name>
<name lang="it">Elementi importati</name>
<name lang="ja">インバートされた要素</name>
<name lang="ko">전기 심볼 요소</name>
<name lang="ko_KR">전기 심볼 요소</name>
<name lang="nl">Elementen geïmporteerd</name>
<name lang="nl_BE">Elementen geïmporteerd</name>
<name lang="pl">Elementy importowane</name>
<name lang="pt">Elementos importados</name>
<name lang="pt_BR">Elementos importados</name>
<name lang="ro">Elemente importate</name>
<name lang="ru">Импортированные элементы</name>
<name lang="sl">Uvoženi elementi</name>
<name lang="sv">Importerade element</name>
<name lang="tr">İthal öğeler</name>
<name lang="uk">Імпортовані елементи</name>
<name lang="zh">导入元件</name>
</names>
<category name="10_electric">
<names>
<name lang="ar">كهربائي</name>
<name lang="ca">Elèctric</name>
<name lang="cs">Elektrotechnika</name>
<name lang="da">Elektrisk</name>
<name lang="de">Elektrik</name>
<name lang="el">Ηλεκτρικά</name>
<name lang="en">Electric</name>
<name lang="es">Eléctrica</name>
<name lang="fi">Sähkö</name>
<name lang="fr">Electrique</name>
<name lang="hr">Električno</name>
<name lang="hu">Elektromos</name>
<name lang="it">Elettrica</name>
<name lang="ja">電気</name>
<name lang="ko">전기</name>
<name lang="mn">Цахилгаан</name>
<name lang="nb">Elektrisk</name>
<name lang="nl">Elektrotechniek</name>
<name lang="nl_BE">Elektrisch</name>
<name lang="pl">Elektrotechnika</name>
<name lang="pt">Elétrico</name>
<name lang="pt_BR">Elétrica</name>
<name lang="ro">Electric</name>
<name lang="ru">Электротехника</name>
<name lang="sk">Elektrické</name>
<name lang="sl">Elektrika</name>
<name lang="sr">Електрика</name>
<name lang="sv">El</name>
<name lang="tr">Elektrik</name>
<name lang="uk">Електрика</name>
<name lang="zh">10_电气</name>
<name lang="zn">电气工程</name>
</names>
<category name="10_allpole">
<names>
<name lang="ar">متعدد الأقطاب</name>
<name lang="ca">Multifilar</name>
<name lang="cs">Vícežilový</name>
<name lang="da">Flere ledere</name>
<name lang="de">Allpolig</name>
<name lang="el">Πολυγραμμικό</name>
<name lang="en">All-pole</name>
<name lang="es">Multifilar</name>
<name lang="fi">Yleisnapa</name>
<name lang="fr">Multifilaire</name>
<name lang="hr">Univerzalni</name>
<name lang="hu">Többvonalas szimbólumok</name>
<name lang="it">Multifilare</name>
<name lang="ja">複線</name>
<name lang="ko">다-극</name>
<name lang="mn">Бүх туйл</name>
<name lang="nb">Alle poler</name>
<name lang="nl">Veel polig</name>
<name lang="nl_BE">All-pole</name>
<name lang="pl">Schematy wieloliniowe</name>
<name lang="pt">Multipolar</name>
<name lang="pt_BR">Multifilar</name>
<name lang="ro">Bipolar</name>
<name lang="ru">Многополюсные</name>
<name lang="sk">Všepólové</name>
<name lang="sl">Vsi poli</name>
<name lang="sr">Свe-полни</name>
<name lang="sv">Allpolig</name>
<name lang="tr">Çok kutuplu</name>
<name lang="uk">Багатополюсні</name>
<name lang="zh">10_全极</name>
</names>
<category name="310_relays_contactors_contacts">
<names>
<name lang="ar">المرحّلات والموصّلات وجهات الاتصال</name>
<name lang="ca">Relés, contactors i contactes</name>
<name lang="cs">Relé, stykače a kontakty</name>
<name lang="da">Relæer, kontaktorer og kontakter</name>
<name lang="de">Relais, Schütze und Kontakte</name>
<name lang="el">Ηλεκτρονόμοι και επαφές</name>
<name lang="en">Relays, contactors and contacts</name>
<name lang="es">Relevadores, contactores y contactos</name>
<name lang="fi">Releet, kontaktorit ja koskettimet</name>
<name lang="fr">Relais, contacteurs et contacts</name>
<name lang="hr">Releji, kontaktori i kontakti</name>
<name lang="hu">Relék, kontaktorok és érintkezők</name>
<name lang="it">Relè, contattori e contatti</name>
<name lang="ja">継電器、接触器、接点</name>
<name lang="ko">릴레이, 커넥터및 접점</name>
<name lang="mn">Реле, контактор болон контакт</name>
<name lang="nb">Reléer, kontaktorere og kontakter</name>
<name lang="nl">Relais, contactoren en contacten</name>
<name lang="nl_BE">Relais, contacteurs en contacten</name>
<name lang="pl">Przekaźniki i styczniki</name>
<name lang="pt">Relés, contatores e contactos</name>
<name lang="pt_BR">Relés, Contatores e contatos</name>
<name lang="ro">Relee, contactoare și contacte</name>
<name lang="ru">Реле, пускатели и контакты</name>
<name lang="sk">Relé, stykače a kontakty</name>
<name lang="sl">Releji, kontaktorji in kontakti</name>
<name lang="sr">Релеји, контактори и контакти</name>
<name lang="sv">Reläer, kontaktorer och kontakter</name>
<name lang="tr">Röleler, kontaktörler ve kontaklar</name>
<name lang="uk">Реле, контактори та контакти</name>
<name lang="zh">继电器、接触器与触点</name>
</names>
<category name="03_contacts">
<names>
<name lang="ar">جهات الاتصال</name>
<name lang="ca">Contactes</name>
<name lang="cs">Kontakty</name>
<name lang="da">Kontaktpersoner</name>
<name lang="de">Kontakte</name>
<name lang="el">Επαφές</name>
<name lang="en">Contacts</name>
<name lang="es">Contactos</name>
<name lang="fi">Kosketimet</name>
<name lang="fr">Contacts</name>
<name lang="hr">Kontakti</name>
<name lang="hu">Kapcsolatok</name>
<name lang="it">Contatti</name>
<name lang="ja">連絡先</name>
<name lang="ko">접점</name>
<name lang="mn">Харилцагчид</name>
<name lang="nb">Kontakter</name>
<name lang="nl">Contacten</name>
<name lang="nl_BE">Contacten</name>
<name lang="pl">Łączność</name>
<name lang="pt">Contatos</name>
<name lang="pt_BR">Contatos</name>
<name lang="ro">Contacte</name>
<name lang="ru">Контакты</name>
<name lang="sk">Kontakty</name>
<name lang="sl">Stiki</name>
<name lang="sr">Контакти</name>
<name lang="sv">Kontakter</name>
<name lang="tr">Kişiler</name>
<name lang="uk">Контакти</name>
<name lang="zh">触点</name>
</names>
<element name="act_comptage_no.elmt">
<definition height="60" hotspot_x="22" hotspot_y="41" link_type="simple" type="element" version="0.100.0" width="30">
<uuid uuid="{19fdfdc7-c1fe-4604-cb14-da967d1a5252}"/>
<names>
<name lang="ar">عدّاد مفتوح عادة</name>
<name lang="ca">Taulell normalment obert</name>
<name lang="cs">Kontakt čítače (NO)</name>
<name lang="da">Normalt åben tæller</name>
<name lang="de">Öffner Zähler betätigt</name>
<name lang="el">Ανοικτή επαφή μετρητή</name>
<name lang="en">Normally open counter</name>
<name lang="es">Contador NA</name>
<name lang="fi">Normaalisti avoin laskuri</name>
<name lang="fr">Actionné par comptage NO</name>
<name lang="hr">Brojač normalno otvoren</name>
<name lang="hu">Alaphelyzetben nyitott számláló</name>
<name lang="it">Contatto azionato da un contatore (NO)</name>
<name lang="ja">常開カウンター</name>
<name lang="ko">평상시 열림 카운터</name>
<name lang="mn">Ихэвчлэн нээлттэй тоолуур</name>
<name lang="nb">Normalt åpen teller</name>
<name lang="nl">Teller contact (NO)</name>
<name lang="nl_BE">Normaal open teller</name>
<name lang="pl">Zestyk zwierny licznika</name>
<name lang="pt">Contador normalmente aberto</name>
<name lang="pt_BR">Contato NA acionado por contador</name>
<name lang="ro">Contor normal deschis</name>
<name lang="ru">Счётчик с нормально открытым контактом</name>
<name lang="sk">Počítadlo s kontaktom NO</name>
<name lang="sl">Števec normalno odprt</name>
<name lang="sr">Бројач нормално отворен</name>
<name lang="sv">Räknare med slutande kontakt</name>
<name lang="tr">Normalde açık sayaç</name>
<name lang="uk">Лічильник нормально розімкнений</name>
<name lang="zh">计数驱动常开触点</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<rect antialias="false" height="10" rx="0" ry="0" style="line-style:normal;line-weight:thin;filling:none;color:black" width="10" x="-20" y="-15"/>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:dashed;line-weight:thin;filling:none;color:black" x1="-10" x2="-3" y1="-10" y2="-10"/>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a3321491-1dac-42e2-a9d1-1a43604116a9}" x="-13" y="-2" z="3">
<text>14</text>
</dynamic_text>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a82a123f-f3cf-4dd7-93c0-44bbdd940429}" x="-13" y="-34" z="4">
<text>11</text>
</dynamic_text>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{960d8065-c520-4d45-aa36-4bb600c0e6db}" x="1" y="-22" z="5">
<text/>
<info_name>label</info_name>
</dynamic_text>
<polygon antialias="true" closed="false" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="-5" x2="0" x3="0" y1="-20" y2="0" y3="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="-30" y2="-20"/>
<ellipse antialias="true" height="4" style="line-style:normal;line-weight:thin;filling:none;color:black" width="4" x="-17" y="-12"/>
<terminal name="" orientation="s" type="Generic" uuid="{572018e1-9843-49dd-964f-836ff27b8240}" x="0" y="10"/>
<terminal name="" orientation="n" type="Generic" uuid="{77224246-2278-498a-88e5-1ecd84e938e8}" x="0" y="-30"/>
</description>
</definition>
</element>
</category>
</category>
</category>
</category>
</category>
</collection>
</project>
@@ -0,0 +1,351 @@
<project title="Legacy sequential attributes - i1280" uuid="{abc27554-b301-5651-8055-41e0ce4599cb}" version="0.200.1">
<properties>
<property name="saveddate" show="1">10/4/26</property>
<property name="saveddate-eu" show="1">04-10-2026</property>
<property name="saveddate-us" show="1">2026-10-04</property>
<property name="savedfilename" show="1">skeleton</property>
<property name="savedtime" show="1">10:28</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="{a94486ed-a490-4ae0-9698-b075035de54d}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" seqhf_1="5" uuid="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" x="100" y="300" z="10">
<terminals>
<terminal id="0" orientation="0" x="0" y="-26"/>
<terminal id="1" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{1b01d6c6-86f7-44e4-8e76-eba4b3a53dea}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{65cedcc2-1a82-4168-858c-8751bc1561d9}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{91920aeb-94ef-423e-a282-206489f04e5e}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" sequ_1="3" uuid="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" x="200" y="300" z="10">
<terminals>
<terminal id="2" orientation="0" x="0" y="-26"/>
<terminal id="3" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{827f6a67-cf32-448a-b33a-e52fbbb9cee7}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{8fe1bb6c-bd54-429f-b8b9-2b1ca789ed2c}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{15fa6b73-1702-4e17-b821-3bd12f81e7ba}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" x="300" y="300" z="10">
<terminals>
<terminal id="4" orientation="0" x="0" y="-26"/>
<terminal id="5" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{93d83639-c2c1-4af5-9048-c0a5e0f241b8}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{c14602f0-24e8-463c-a183-3dac7f78e009}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{662e7f9d-9f6d-40b4-b517-7b584f6b3604}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" x="400" y="300" z="10">
<terminals>
<terminal id="6" orientation="0" x="0" y="-26"/>
<terminal id="7" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{3c604f1b-1498-4b2b-9f2d-0063e2bcb78c}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{dbb98cf7-9d66-4d99-91d9-2149077d6ba2}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{a8efc4c9-bfb3-4bde-a501-6b1bd5f93ae5}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
<conductors>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" seqhf_1="7" uuid="{f5162c59-0c59-4e94-b6a6-4cf259a52d91}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
</conductor>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" sequ_1="4" uuid="{89cacce1-b12a-4ec3-aadf-d3f8feb7494d}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
</conductor>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element1_label="" element1_name="Normally open counter" element2="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{944a0069-bad6-4d92-91ad-7aedf3961c8a}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
<sequentialNumbers/>
</conductor>
</conductors>
</diagram>
<collection>
<category name="import">
<names>
<name lang="ca">Elements importats</name>
<name lang="cs">Zavedené prvky</name>
<name lang="da">Importerede elementer</name>
<name lang="de">Importierte Elemente</name>
<name lang="el">Εισηγμένα στοιχεία</name>
<name lang="en">Imported elements</name>
<name lang="es">Elementos importados</name>
<name lang="fr">Éléments importés</name>
<name lang="hr">Uvezeni elementi</name>
<name lang="it">Elementi importati</name>
<name lang="ja">インバートされた要素</name>
<name lang="ko">전기 심볼 요소</name>
<name lang="ko_KR">전기 심볼 요소</name>
<name lang="nl">Elementen geïmporteerd</name>
<name lang="nl_BE">Elementen geïmporteerd</name>
<name lang="pl">Elementy importowane</name>
<name lang="pt">Elementos importados</name>
<name lang="pt_BR">Elementos importados</name>
<name lang="ro">Elemente importate</name>
<name lang="ru">Импортированные элементы</name>
<name lang="sl">Uvoženi elementi</name>
<name lang="sv">Importerade element</name>
<name lang="tr">İthal öğeler</name>
<name lang="uk">Імпортовані елементи</name>
<name lang="zh">导入元件</name>
</names>
<category name="10_electric">
<names>
<name lang="ar">كهربائي</name>
<name lang="ca">Elèctric</name>
<name lang="cs">Elektrotechnika</name>
<name lang="da">Elektrisk</name>
<name lang="de">Elektrik</name>
<name lang="el">Ηλεκτρικά</name>
<name lang="en">Electric</name>
<name lang="es">Eléctrica</name>
<name lang="fi">Sähkö</name>
<name lang="fr">Electrique</name>
<name lang="hr">Električno</name>
<name lang="hu">Elektromos</name>
<name lang="it">Elettrica</name>
<name lang="ja">電気</name>
<name lang="ko">전기</name>
<name lang="mn">Цахилгаан</name>
<name lang="nb">Elektrisk</name>
<name lang="nl">Elektrotechniek</name>
<name lang="nl_BE">Elektrisch</name>
<name lang="pl">Elektrotechnika</name>
<name lang="pt">Elétrico</name>
<name lang="pt_BR">Elétrica</name>
<name lang="ro">Electric</name>
<name lang="ru">Электротехника</name>
<name lang="sk">Elektrické</name>
<name lang="sl">Elektrika</name>
<name lang="sr">Електрика</name>
<name lang="sv">El</name>
<name lang="tr">Elektrik</name>
<name lang="uk">Електрика</name>
<name lang="zh">10_电气</name>
<name lang="zn">电气工程</name>
</names>
<category name="10_allpole">
<names>
<name lang="ar">متعدد الأقطاب</name>
<name lang="ca">Multifilar</name>
<name lang="cs">Vícežilový</name>
<name lang="da">Flere ledere</name>
<name lang="de">Allpolig</name>
<name lang="el">Πολυγραμμικό</name>
<name lang="en">All-pole</name>
<name lang="es">Multifilar</name>
<name lang="fi">Yleisnapa</name>
<name lang="fr">Multifilaire</name>
<name lang="hr">Univerzalni</name>
<name lang="hu">Többvonalas szimbólumok</name>
<name lang="it">Multifilare</name>
<name lang="ja">複線</name>
<name lang="ko">다-극</name>
<name lang="mn">Бүх туйл</name>
<name lang="nb">Alle poler</name>
<name lang="nl">Veel polig</name>
<name lang="nl_BE">All-pole</name>
<name lang="pl">Schematy wieloliniowe</name>
<name lang="pt">Multipolar</name>
<name lang="pt_BR">Multifilar</name>
<name lang="ro">Bipolar</name>
<name lang="ru">Многополюсные</name>
<name lang="sk">Všepólové</name>
<name lang="sl">Vsi poli</name>
<name lang="sr">Свe-полни</name>
<name lang="sv">Allpolig</name>
<name lang="tr">Çok kutuplu</name>
<name lang="uk">Багатополюсні</name>
<name lang="zh">10_全极</name>
</names>
<category name="310_relays_contactors_contacts">
<names>
<name lang="ar">المرحّلات والموصّلات وجهات الاتصال</name>
<name lang="ca">Relés, contactors i contactes</name>
<name lang="cs">Relé, stykače a kontakty</name>
<name lang="da">Relæer, kontaktorer og kontakter</name>
<name lang="de">Relais, Schütze und Kontakte</name>
<name lang="el">Ηλεκτρονόμοι και επαφές</name>
<name lang="en">Relays, contactors and contacts</name>
<name lang="es">Relevadores, contactores y contactos</name>
<name lang="fi">Releet, kontaktorit ja koskettimet</name>
<name lang="fr">Relais, contacteurs et contacts</name>
<name lang="hr">Releji, kontaktori i kontakti</name>
<name lang="hu">Relék, kontaktorok és érintkezők</name>
<name lang="it">Relè, contattori e contatti</name>
<name lang="ja">継電器、接触器、接点</name>
<name lang="ko">릴레이, 커넥터및 접점</name>
<name lang="mn">Реле, контактор болон контакт</name>
<name lang="nb">Reléer, kontaktorere og kontakter</name>
<name lang="nl">Relais, contactoren en contacten</name>
<name lang="nl_BE">Relais, contacteurs en contacten</name>
<name lang="pl">Przekaźniki i styczniki</name>
<name lang="pt">Relés, contatores e contactos</name>
<name lang="pt_BR">Relés, Contatores e contatos</name>
<name lang="ro">Relee, contactoare și contacte</name>
<name lang="ru">Реле, пускатели и контакты</name>
<name lang="sk">Relé, stykače a kontakty</name>
<name lang="sl">Releji, kontaktorji in kontakti</name>
<name lang="sr">Релеји, контактори и контакти</name>
<name lang="sv">Reläer, kontaktorer och kontakter</name>
<name lang="tr">Röleler, kontaktörler ve kontaklar</name>
<name lang="uk">Реле, контактори та контакти</name>
<name lang="zh">继电器、接触器与触点</name>
</names>
<category name="03_contacts">
<names>
<name lang="ar">جهات الاتصال</name>
<name lang="ca">Contactes</name>
<name lang="cs">Kontakty</name>
<name lang="da">Kontaktpersoner</name>
<name lang="de">Kontakte</name>
<name lang="el">Επαφές</name>
<name lang="en">Contacts</name>
<name lang="es">Contactos</name>
<name lang="fi">Kosketimet</name>
<name lang="fr">Contacts</name>
<name lang="hr">Kontakti</name>
<name lang="hu">Kapcsolatok</name>
<name lang="it">Contatti</name>
<name lang="ja">連絡先</name>
<name lang="ko">접점</name>
<name lang="mn">Харилцагчид</name>
<name lang="nb">Kontakter</name>
<name lang="nl">Contacten</name>
<name lang="nl_BE">Contacten</name>
<name lang="pl">Łączność</name>
<name lang="pt">Contatos</name>
<name lang="pt_BR">Contatos</name>
<name lang="ro">Contacte</name>
<name lang="ru">Контакты</name>
<name lang="sk">Kontakty</name>
<name lang="sl">Stiki</name>
<name lang="sr">Контакти</name>
<name lang="sv">Kontakter</name>
<name lang="tr">Kişiler</name>
<name lang="uk">Контакти</name>
<name lang="zh">触点</name>
</names>
<element name="act_comptage_no.elmt">
<definition height="60" hotspot_x="22" hotspot_y="41" link_type="simple" type="element" version="0.100.0" width="30">
<uuid uuid="{19fdfdc7-c1fe-4604-cb14-da967d1a5252}"/>
<names>
<name lang="ar">عدّاد مفتوح عادة</name>
<name lang="ca">Taulell normalment obert</name>
<name lang="cs">Kontakt čítače (NO)</name>
<name lang="da">Normalt åben tæller</name>
<name lang="de">Öffner Zähler betätigt</name>
<name lang="el">Ανοικτή επαφή μετρητή</name>
<name lang="en">Normally open counter</name>
<name lang="es">Contador NA</name>
<name lang="fi">Normaalisti avoin laskuri</name>
<name lang="fr">Actionné par comptage NO</name>
<name lang="hr">Brojač normalno otvoren</name>
<name lang="hu">Alaphelyzetben nyitott számláló</name>
<name lang="it">Contatto azionato da un contatore (NO)</name>
<name lang="ja">常開カウンター</name>
<name lang="ko">평상시 열림 카운터</name>
<name lang="mn">Ихэвчлэн нээлттэй тоолуур</name>
<name lang="nb">Normalt åpen teller</name>
<name lang="nl">Teller contact (NO)</name>
<name lang="nl_BE">Normaal open teller</name>
<name lang="pl">Zestyk zwierny licznika</name>
<name lang="pt">Contador normalmente aberto</name>
<name lang="pt_BR">Contato NA acionado por contador</name>
<name lang="ro">Contor normal deschis</name>
<name lang="ru">Счётчик с нормально открытым контактом</name>
<name lang="sk">Počítadlo s kontaktom NO</name>
<name lang="sl">Števec normalno odprt</name>
<name lang="sr">Бројач нормално отворен</name>
<name lang="sv">Räknare med slutande kontakt</name>
<name lang="tr">Normalde açık sayaç</name>
<name lang="uk">Лічильник нормально розімкнений</name>
<name lang="zh">计数驱动常开触点</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<rect antialias="false" height="10" rx="0" ry="0" style="line-style:normal;line-weight:thin;filling:none;color:black" width="10" x="-20" y="-15"/>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:dashed;line-weight:thin;filling:none;color:black" x1="-10" x2="-3" y1="-10" y2="-10"/>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a3321491-1dac-42e2-a9d1-1a43604116a9}" x="-13" y="-2" z="3">
<text>14</text>
</dynamic_text>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a82a123f-f3cf-4dd7-93c0-44bbdd940429}" x="-13" y="-34" z="4">
<text>11</text>
</dynamic_text>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{960d8065-c520-4d45-aa36-4bb600c0e6db}" x="1" y="-22" z="5">
<text/>
<info_name>label</info_name>
</dynamic_text>
<polygon antialias="true" closed="false" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="-5" x2="0" x3="0" y1="-20" y2="0" y3="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="-30" y2="-20"/>
<ellipse antialias="true" height="4" style="line-style:normal;line-weight:thin;filling:none;color:black" width="4" x="-17" y="-12"/>
<terminal name="" orientation="s" type="Generic" uuid="{572018e1-9843-49dd-964f-836ff27b8240}" x="0" y="10"/>
<terminal name="" orientation="n" type="Generic" uuid="{77224246-2278-498a-88e5-1ecd84e938e8}" x="0" y="-30"/>
</description>
</definition>
</element>
</category>
</category>
</category>
</category>
</category>
</collection>
</project>
@@ -0,0 +1,320 @@
<project title="one text per potential, tied lengths" uuid="{74544bb0-3f71-58af-9052-eb46f8a03dfd}" 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">one_text_per_potential_tie</property>
<property name="savedfilepath" show="1">one_text_per_potential_tie.qet</property>
<property name="savedtime" show="1">22:07</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="{91957ff3-7b74-4012-a876-f858c68b2d60}" 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="7" onetextperfolio="1" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="R" type="embed://import/10_electric/10_allpole/395_electronics_semiconductors/01_resistors/resistance.elmt" uuid="{3817b0a1-cab7-454e-81f2-7678b242cced}" x="100" y="300" z="10">
<terminals>
<terminal id="0" orientation="0" x="0" y="-16"/>
<terminal id="1" orientation="2" x="0" y="16"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{1a7d317c-51b7-402f-807e-79aa136df233}" x="6" y="-11.5">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="R" type="embed://import/10_electric/10_allpole/395_electronics_semiconductors/01_resistors/resistance.elmt" uuid="{f571bdba-9562-4119-8a18-0d798ebb7c3d}" x="300" y="100" z="10">
<terminals>
<terminal id="2" orientation="0" x="0" y="-16"/>
<terminal id="3" orientation="2" x="0" y="16"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{79099d69-dcec-4247-bd03-cdb07a0194d6}" x="6" y="-11.5">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="R" type="embed://import/10_electric/10_allpole/395_electronics_semiconductors/01_resistors/resistance.elmt" uuid="{c05fe3f1-f4a1-4beb-843b-90aadc3ca6c0}" x="300" y="500" z="10">
<terminals>
<terminal id="4" orientation="0" x="0" y="-16"/>
<terminal id="5" orientation="2" x="0" y="16"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{075df885-82c2-49dd-817d-809137a3329d}" x="6" y="-11.5">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="R" type="embed://import/10_electric/10_allpole/395_electronics_semiconductors/01_resistors/resistance.elmt" uuid="{d0acb0f6-8c2f-4daa-8365-7ad1df6f7f86}" x="400" y="300" z="10">
<terminals>
<terminal id="6" orientation="0" x="0" y="-16"/>
<terminal id="7" orientation="2" x="0" y="16"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{11eafa99-e5a9-4baf-a2ed-8469780c7fa8}" x="6" y="-11.5">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
<conductors>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{3817b0a1-cab7-454e-81f2-7678b242cced}" element1_label="" element1_name="Resistor" element2="{f571bdba-9562-4119-8a18-0d798ebb7c3d}" element2_label="" element2_name="Resistor" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="42" numsize="7" onetextperfolio="1" tension_protocol="" terminal1="{09a3060b-a0fd-41ad-b605-f5c0ae63901e}" terminal2="{4410c57c-3796-4696-b63b-d8d5a0c9b788}" terminalname1="A2" terminalname2="A1" text_color="#000000" type="multi" uuid="{26591715-c028-44c9-bf2d-c828f8187527}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
<sequentialNumbers/>
</conductor>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{f571bdba-9562-4119-8a18-0d798ebb7c3d}" element1_label="" element1_name="Resistor" element2="{c05fe3f1-f4a1-4beb-843b-90aadc3ca6c0}" element2_label="" element2_name="Resistor" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="42" numsize="7" onetextperfolio="1" tension_protocol="" terminal1="{4410c57c-3796-4696-b63b-d8d5a0c9b788}" terminal2="{09a3060b-a0fd-41ad-b605-f5c0ae63901e}" terminalname1="A1" terminalname2="A2" text_color="#000000" type="multi" uuid="{0c9e2a9a-f402-49f4-9780-a8c72704acff}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
<sequentialNumbers/>
</conductor>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{f571bdba-9562-4119-8a18-0d798ebb7c3d}" element1_label="" element1_name="Resistor" element2="{d0acb0f6-8c2f-4daa-8365-7ad1df6f7f86}" element2_label="" element2_name="Resistor" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="42" numsize="7" onetextperfolio="1" tension_protocol="" terminal1="{4410c57c-3796-4696-b63b-d8d5a0c9b788}" terminal2="{09a3060b-a0fd-41ad-b605-f5c0ae63901e}" terminalname1="A1" terminalname2="A2" text_color="#000000" type="multi" uuid="{5ecce027-3b6b-4e2e-9d30-f5dd59b320c0}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
<sequentialNumbers/>
</conductor>
</conductors>
</diagram>
<collection>
<category name="import">
<names>
<name lang="ca">Elements importats</name>
<name lang="cs">Zavedené prvky</name>
<name lang="da">Importerede elementer</name>
<name lang="de">Importierte Elemente</name>
<name lang="el">Εισηγμένα στοιχεία</name>
<name lang="en">Imported elements</name>
<name lang="es">Elementos importados</name>
<name lang="fr">Éléments importés</name>
<name lang="hr">Uvezeni elementi</name>
<name lang="it">Elementi importati</name>
<name lang="ja">インバートされた要素</name>
<name lang="ko">전기 심볼 요소</name>
<name lang="ko_KR">전기 심볼 요소</name>
<name lang="nl">Elementen geïmporteerd</name>
<name lang="nl_BE">Elementen geïmporteerd</name>
<name lang="pl">Elementy importowane</name>
<name lang="pt">Elementos importados</name>
<name lang="pt_BR">Elementos importados</name>
<name lang="ro">Elemente importate</name>
<name lang="ru">Импортированные элементы</name>
<name lang="sl">Uvoženi elementi</name>
<name lang="sv">Importerade element</name>
<name lang="tr">İthal öğeler</name>
<name lang="uk">Імпортовані елементи</name>
<name lang="zh">导入元件</name>
</names>
<category name="10_electric">
<names>
<name lang="ar">كهربائي</name>
<name lang="ca">Elèctric</name>
<name lang="cs">Elektrotechnika</name>
<name lang="da">Elektrisk</name>
<name lang="de">Elektrik</name>
<name lang="el">Ηλεκτρικά</name>
<name lang="en">Electric</name>
<name lang="es">Eléctrica</name>
<name lang="fi">Sähkö</name>
<name lang="fr">Electrique</name>
<name lang="hr">Električno</name>
<name lang="hu">Elektromos</name>
<name lang="it">Elettrica</name>
<name lang="ja">電気</name>
<name lang="ko">전기</name>
<name lang="mn">Цахилгаан</name>
<name lang="nb">Elektrisk</name>
<name lang="nl">Elektrotechniek</name>
<name lang="nl_BE">Elektrisch</name>
<name lang="pl">Elektrotechnika</name>
<name lang="pt">Elétrico</name>
<name lang="pt_BR">Elétrica</name>
<name lang="ro">Electric</name>
<name lang="ru">Электротехника</name>
<name lang="sk">Elektrické</name>
<name lang="sl">Elektrika</name>
<name lang="sr">Електрика</name>
<name lang="sv">El</name>
<name lang="tr">Elektrik</name>
<name lang="uk">Електрика</name>
<name lang="zh">10_电气</name>
<name lang="zn">电气工程</name>
</names>
<category name="10_allpole">
<names>
<name lang="ar">متعدد الأقطاب</name>
<name lang="ca">Multifilar</name>
<name lang="cs">Vícežilový</name>
<name lang="da">Flere ledere</name>
<name lang="de">Allpolig</name>
<name lang="el">Πολυγραμμικό</name>
<name lang="en">All-pole</name>
<name lang="es">Multifilar</name>
<name lang="fi">Yleisnapa</name>
<name lang="fr">Multifilaire</name>
<name lang="hr">Univerzalni</name>
<name lang="hu">Többvonalas szimbólumok</name>
<name lang="it">Multifilare</name>
<name lang="ja">複線</name>
<name lang="ko">다-극</name>
<name lang="mn">Бүх туйл</name>
<name lang="nb">Alle poler</name>
<name lang="nl">Veel polig</name>
<name lang="nl_BE">All-pole</name>
<name lang="pl">Schematy wieloliniowe</name>
<name lang="pt">Multipolar</name>
<name lang="pt_BR">Multifilar</name>
<name lang="ro">Bipolar</name>
<name lang="ru">Многополюсные</name>
<name lang="sk">Všepólové</name>
<name lang="sl">Vsi poli</name>
<name lang="sr">Свe-полни</name>
<name lang="sv">Allpolig</name>
<name lang="tr">Çok kutuplu</name>
<name lang="uk">Багатополюсні</name>
<name lang="zh">10_全极</name>
</names>
<category name="395_electronics_semiconductors">
<names>
<name lang="ar">إلكترونيات وأشباه موصلات</name>
<name lang="ca">Electrònica i semiconductors</name>
<name lang="cs">Elektronika a polovodiče</name>
<name lang="da">Elektronik og halvledere</name>
<name lang="de">Elektronik und Halbleiter</name>
<name lang="el">Ηλεκτρονικά και ημιαγωγοί</name>
<name lang="en">Electronics and semiconductors</name>
<name lang="es">Electrónica y semiconductores</name>
<name lang="fi">Elektroniikka ja puolijohteet</name>
<name lang="fr">Electronique et semi-conducteurs</name>
<name lang="hr">Elektronika i poluvodiči</name>
<name lang="hu">Elektronika és félvezetők</name>
<name lang="it">Elettronica e semiconduttori</name>
<name lang="ja">電子と半導体</name>
<name lang="ko">전자 공학 및 반도체</name>
<name lang="mn">Электроник ба хагас дамжуулагч</name>
<name lang="nb">Elektronikk og halvledere</name>
<name lang="nl">Electronica en halfgeleiders</name>
<name lang="nl_BE">Elektronica en halfgeleiders</name>
<name lang="pl">Elektronika i półprzewodniki</name>
<name lang="pt">Eletrónica e semicondutores</name>
<name lang="pt_BR">Eletrônica e semi-condutoues</name>
<name lang="ro">Electronică și semiconductoare</name>
<name lang="ru">Электроника и полупроводники</name>
<name lang="sk">Elektronika a polovodiče</name>
<name lang="sl">Elektronika in polprevodniki</name>
<name lang="sr">Електроника и полупроводници</name>
<name lang="sv">Elektronik och halvledare</name>
<name lang="tr">Elektronik ve yarı iletkenler</name>
<name lang="uk">Електроніка та напівпровідники</name>
<name lang="zh">电子与半导体</name>
</names>
<category name="01_resistors">
<names>
<name lang="ar">مُقاومة</name>
<name lang="ca">Resistències</name>
<name lang="cs">Odpory</name>
<name lang="da">Modstande</name>
<name lang="de">Widerstände</name>
<name lang="el">Αντιστάσεις</name>
<name lang="en">Resistors</name>
<name lang="es">Resistencias</name>
<name lang="fi">Vastukset</name>
<name lang="fr">Résistances</name>
<name lang="hr">Otpornici</name>
<name lang="hu">Ellenállások</name>
<name lang="it">Resistori</name>
<name lang="ja">抵抗器</name>
<name lang="ko">저항</name>
<name lang="mn">Эсэргүүцэл</name>
<name lang="nb">Motstander</name>
<name lang="nl">Weerstand</name>
<name lang="nl_BE">Weerstanden</name>
<name lang="pl">Rezystory</name>
<name lang="pt">Resistência</name>
<name lang="pt_BR">Resistores</name>
<name lang="ro">Rezistoare</name>
<name lang="ru">Резисторы</name>
<name lang="sk">Rezistory</name>
<name lang="sl">Upori</name>
<name lang="sr">Отпорници</name>
<name lang="sv">Motstånd</name>
<name lang="tr">Dirençler</name>
<name lang="uk">Резистори</name>
<name lang="zh">电阻器</name>
</names>
<element name="resistance.elmt">
<definition height="60" hotspot_x="10" hotspot_y="30" link_type="simple" type="element" version="0.100.0" width="20">
<uuid uuid="{2becfcef-f6f6-6a63-ef49-65f12026c4f8}"/>
<names>
<name lang="ar">مُقاومة</name>
<name lang="ca">Resistència</name>
<name lang="cs">Odpor</name>
<name lang="da">Modstand</name>
<name lang="de">Widerstand</name>
<name lang="el">Αντίσταση</name>
<name lang="en">Resistor</name>
<name lang="es">Resistencia</name>
<name lang="fi">Vastus</name>
<name lang="fr">Résistance</name>
<name lang="hr">Otpornik</name>
<name lang="hu">Ellenállás</name>
<name lang="it">Resistore</name>
<name lang="ja">抵抗器</name>
<name lang="ko">저항</name>
<name lang="mn">Резистор</name>
<name lang="nb">Motstand</name>
<name lang="nl">Weerstand</name>
<name lang="nl_BE">Weerstand</name>
<name lang="pl">Rezystor</name>
<name lang="pt">Resistência</name>
<name lang="pt_BR">Resistor</name>
<name lang="ro">Rezistor</name>
<name lang="ru">Резистор</name>
<name lang="sk">Rezistor</name>
<name lang="sl">Upor</name>
<name lang="sr">Отпорник</name>
<name lang="sv">Resistor</name>
<name lang="tr">Direnç</name>
<name lang="uk">Резистор</name>
<name lang="zh">电阻</name>
</names>
<informations>EN 60617: 04-01-01</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="-20"/>
<rect antialias="false" height="20" rx="0" ry="0" style="line-style:normal;line-weight:normal;filling:white;color:black" width="8" x="-4" y="-10"/>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{570cd854-578d-4c4b-bd2a-3498353e46b6}" x="6" y="-11.5" z="7">
<text/>
<info_name>label</info_name>
</dynamic_text>
<terminal name="A2" orientation="s" type="Generic" uuid="{09a3060b-a0fd-41ad-b605-f5c0ae63901e}" x="0" y="20"/>
<terminal name="A1" orientation="n" type="Generic" uuid="{4410c57c-3796-4696-b63b-d8d5a0c9b788}" x="0" y="-20"/>
</description>
</definition>
</element>
</category>
</category>
</category>
</category>
</category>
</collection>
</project>
+63
View File
@@ -0,0 +1,63 @@
<project title="pdf page size" uuid="{63126da6-a8f2-5840-8149-a23a51406a32}" 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">pdf_page_a3</property>
<property name="savedtime" show="1">22: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="27" colsize="58" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="1070" indexrev="" locmach="" order="1" plant="" rows="14" rowsize="75" title="" uuid="{270fad7d-7d9f-4198-a0e1-dceb22c883cc}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
</diagram>
<diagram author="" auto_page_num="" cols="20" colsize="55" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="1535" indexrev="" locmach="" order="2" plant="" rows="15" rowsize="101" title="" uuid="{9cf88404-d198-4690-90f7-647fb7940f68}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
</diagram>
<collection>
<category name="import">
<names>
<name lang="ca">Elements importats</name>
<name lang="cs">Zavedené prvky</name>
<name lang="da">Importerede elementer</name>
<name lang="de">Importierte Elemente</name>
<name lang="el">Εισηγμένα στοιχεία</name>
<name lang="en">Imported elements</name>
<name lang="es">Elementos importados</name>
<name lang="fr">Éléments importés</name>
<name lang="hr">Uvezeni elementi</name>
<name lang="it">Elementi importati</name>
<name lang="ja">インバートされた要素</name>
<name lang="ko">전기 심볼 요소</name>
<name lang="ko_KR">전기 심볼 요소</name>
<name lang="nl">Elementen geïmporteerd</name>
<name lang="nl_BE">Elementen geïmporteerd</name>
<name lang="pl">Elementy importowane</name>
<name lang="pt">Elementos importados</name>
<name lang="pt_BR">Elementos importados</name>
<name lang="ro">Elemente importate</name>
<name lang="ru">Импортированные элементы</name>
<name lang="sl">Uvoženi elementi</name>
<name lang="sv">Importerade element</name>
<name lang="tr">İthal öğeler</name>
<name lang="uk">Імпортовані елементи</name>
<name lang="zh">导入元件</name>
</names>
</category>
</collection>
</project>
@@ -0,0 +1,359 @@
<project title="Ten sequential numbers - i1280" uuid="{abc27554-b301-5651-8055-41e0ce4599cb}" version="0.200.1">
<properties>
<property name="saveddate" show="1">10/4/26</property>
<property name="saveddate-eu" show="1">04-10-2026</property>
<property name="saveddate-us" show="1">2026-10-04</property>
<property name="savedfilename" show="1">skeleton</property>
<property name="savedtime" show="1">10:28</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="{a94486ed-a490-4ae0-9698-b075035de54d}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" x="100" y="300" z="10">
<terminals>
<terminal id="0" orientation="0" x="0" y="-26"/>
<terminal id="1" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<elementInformations>
<elementInformation name="formula" show="1">%sequ_10-%sequ_1</elementInformation>
</elementInformations>
<sequentialNumbers>
<unit>A;B;C;D;E;F;G;H;I;J</unit>
</sequentialNumbers>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{1b01d6c6-86f7-44e4-8e76-eba4b3a53dea}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{65cedcc2-1a82-4168-858c-8751bc1561d9}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{91920aeb-94ef-423e-a282-206489f04e5e}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" x="200" y="300" z="10">
<terminals>
<terminal id="2" orientation="0" x="0" y="-26"/>
<terminal id="3" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{827f6a67-cf32-448a-b33a-e52fbbb9cee7}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{8fe1bb6c-bd54-429f-b8b9-2b1ca789ed2c}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{15fa6b73-1702-4e17-b821-3bd12f81e7ba}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" x="300" y="300" z="10">
<terminals>
<terminal id="4" orientation="0" x="0" y="-26"/>
<terminal id="5" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{93d83639-c2c1-4af5-9048-c0a5e0f241b8}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{c14602f0-24e8-463c-a183-3dac7f78e009}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{662e7f9d-9f6d-40b4-b517-7b584f6b3604}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" x="400" y="300" z="10">
<terminals>
<terminal id="6" orientation="0" x="0" y="-26"/>
<terminal id="7" orientation="2" x="0" y="6"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{3c604f1b-1498-4b2b-9f2d-0063e2bcb78c}" x="-13" y="-2">
<text>14</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{dbb98cf7-9d66-4d99-91d9-2149077d6ba2}" x="-13" y="-34">
<text>11</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{a8efc4c9-bfb3-4bde-a501-6b1bd5f93ae5}" x="1" y="-22">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
<conductors>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{f5162c59-0c59-4e94-b6a6-4cf259a52d91}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
<sequentialNumbers/>
</conductor>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{89cacce1-b12a-4ec3-aadf-d3f8feb7494d}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
<sequentialNumbers/>
</conductor>
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element1_label="" element1_name="Normally open counter" element2="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{944a0069-bad6-4d92-91ad-7aedf3961c8a}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
<sequentialNumbers/>
</conductor>
</conductors>
</diagram>
<collection>
<category name="import">
<names>
<name lang="ca">Elements importats</name>
<name lang="cs">Zavedené prvky</name>
<name lang="da">Importerede elementer</name>
<name lang="de">Importierte Elemente</name>
<name lang="el">Εισηγμένα στοιχεία</name>
<name lang="en">Imported elements</name>
<name lang="es">Elementos importados</name>
<name lang="fr">Éléments importés</name>
<name lang="hr">Uvezeni elementi</name>
<name lang="it">Elementi importati</name>
<name lang="ja">インバートされた要素</name>
<name lang="ko">전기 심볼 요소</name>
<name lang="ko_KR">전기 심볼 요소</name>
<name lang="nl">Elementen geïmporteerd</name>
<name lang="nl_BE">Elementen geïmporteerd</name>
<name lang="pl">Elementy importowane</name>
<name lang="pt">Elementos importados</name>
<name lang="pt_BR">Elementos importados</name>
<name lang="ro">Elemente importate</name>
<name lang="ru">Импортированные элементы</name>
<name lang="sl">Uvoženi elementi</name>
<name lang="sv">Importerade element</name>
<name lang="tr">İthal öğeler</name>
<name lang="uk">Імпортовані елементи</name>
<name lang="zh">导入元件</name>
</names>
<category name="10_electric">
<names>
<name lang="ar">كهربائي</name>
<name lang="ca">Elèctric</name>
<name lang="cs">Elektrotechnika</name>
<name lang="da">Elektrisk</name>
<name lang="de">Elektrik</name>
<name lang="el">Ηλεκτρικά</name>
<name lang="en">Electric</name>
<name lang="es">Eléctrica</name>
<name lang="fi">Sähkö</name>
<name lang="fr">Electrique</name>
<name lang="hr">Električno</name>
<name lang="hu">Elektromos</name>
<name lang="it">Elettrica</name>
<name lang="ja">電気</name>
<name lang="ko">전기</name>
<name lang="mn">Цахилгаан</name>
<name lang="nb">Elektrisk</name>
<name lang="nl">Elektrotechniek</name>
<name lang="nl_BE">Elektrisch</name>
<name lang="pl">Elektrotechnika</name>
<name lang="pt">Elétrico</name>
<name lang="pt_BR">Elétrica</name>
<name lang="ro">Electric</name>
<name lang="ru">Электротехника</name>
<name lang="sk">Elektrické</name>
<name lang="sl">Elektrika</name>
<name lang="sr">Електрика</name>
<name lang="sv">El</name>
<name lang="tr">Elektrik</name>
<name lang="uk">Електрика</name>
<name lang="zh">10_电气</name>
<name lang="zn">电气工程</name>
</names>
<category name="10_allpole">
<names>
<name lang="ar">متعدد الأقطاب</name>
<name lang="ca">Multifilar</name>
<name lang="cs">Vícežilový</name>
<name lang="da">Flere ledere</name>
<name lang="de">Allpolig</name>
<name lang="el">Πολυγραμμικό</name>
<name lang="en">All-pole</name>
<name lang="es">Multifilar</name>
<name lang="fi">Yleisnapa</name>
<name lang="fr">Multifilaire</name>
<name lang="hr">Univerzalni</name>
<name lang="hu">Többvonalas szimbólumok</name>
<name lang="it">Multifilare</name>
<name lang="ja">複線</name>
<name lang="ko">다-극</name>
<name lang="mn">Бүх туйл</name>
<name lang="nb">Alle poler</name>
<name lang="nl">Veel polig</name>
<name lang="nl_BE">All-pole</name>
<name lang="pl">Schematy wieloliniowe</name>
<name lang="pt">Multipolar</name>
<name lang="pt_BR">Multifilar</name>
<name lang="ro">Bipolar</name>
<name lang="ru">Многополюсные</name>
<name lang="sk">Všepólové</name>
<name lang="sl">Vsi poli</name>
<name lang="sr">Свe-полни</name>
<name lang="sv">Allpolig</name>
<name lang="tr">Çok kutuplu</name>
<name lang="uk">Багатополюсні</name>
<name lang="zh">10_全极</name>
</names>
<category name="310_relays_contactors_contacts">
<names>
<name lang="ar">المرحّلات والموصّلات وجهات الاتصال</name>
<name lang="ca">Relés, contactors i contactes</name>
<name lang="cs">Relé, stykače a kontakty</name>
<name lang="da">Relæer, kontaktorer og kontakter</name>
<name lang="de">Relais, Schütze und Kontakte</name>
<name lang="el">Ηλεκτρονόμοι και επαφές</name>
<name lang="en">Relays, contactors and contacts</name>
<name lang="es">Relevadores, contactores y contactos</name>
<name lang="fi">Releet, kontaktorit ja koskettimet</name>
<name lang="fr">Relais, contacteurs et contacts</name>
<name lang="hr">Releji, kontaktori i kontakti</name>
<name lang="hu">Relék, kontaktorok és érintkezők</name>
<name lang="it">Relè, contattori e contatti</name>
<name lang="ja">継電器、接触器、接点</name>
<name lang="ko">릴레이, 커넥터및 접점</name>
<name lang="mn">Реле, контактор болон контакт</name>
<name lang="nb">Reléer, kontaktorere og kontakter</name>
<name lang="nl">Relais, contactoren en contacten</name>
<name lang="nl_BE">Relais, contacteurs en contacten</name>
<name lang="pl">Przekaźniki i styczniki</name>
<name lang="pt">Relés, contatores e contactos</name>
<name lang="pt_BR">Relés, Contatores e contatos</name>
<name lang="ro">Relee, contactoare și contacte</name>
<name lang="ru">Реле, пускатели и контакты</name>
<name lang="sk">Relé, stykače a kontakty</name>
<name lang="sl">Releji, kontaktorji in kontakti</name>
<name lang="sr">Релеји, контактори и контакти</name>
<name lang="sv">Reläer, kontaktorer och kontakter</name>
<name lang="tr">Röleler, kontaktörler ve kontaklar</name>
<name lang="uk">Реле, контактори та контакти</name>
<name lang="zh">继电器、接触器与触点</name>
</names>
<category name="03_contacts">
<names>
<name lang="ar">جهات الاتصال</name>
<name lang="ca">Contactes</name>
<name lang="cs">Kontakty</name>
<name lang="da">Kontaktpersoner</name>
<name lang="de">Kontakte</name>
<name lang="el">Επαφές</name>
<name lang="en">Contacts</name>
<name lang="es">Contactos</name>
<name lang="fi">Kosketimet</name>
<name lang="fr">Contacts</name>
<name lang="hr">Kontakti</name>
<name lang="hu">Kapcsolatok</name>
<name lang="it">Contatti</name>
<name lang="ja">連絡先</name>
<name lang="ko">접점</name>
<name lang="mn">Харилцагчид</name>
<name lang="nb">Kontakter</name>
<name lang="nl">Contacten</name>
<name lang="nl_BE">Contacten</name>
<name lang="pl">Łączność</name>
<name lang="pt">Contatos</name>
<name lang="pt_BR">Contatos</name>
<name lang="ro">Contacte</name>
<name lang="ru">Контакты</name>
<name lang="sk">Kontakty</name>
<name lang="sl">Stiki</name>
<name lang="sr">Контакти</name>
<name lang="sv">Kontakter</name>
<name lang="tr">Kişiler</name>
<name lang="uk">Контакти</name>
<name lang="zh">触点</name>
</names>
<element name="act_comptage_no.elmt">
<definition height="60" hotspot_x="22" hotspot_y="41" link_type="simple" type="element" version="0.100.0" width="30">
<uuid uuid="{19fdfdc7-c1fe-4604-cb14-da967d1a5252}"/>
<names>
<name lang="ar">عدّاد مفتوح عادة</name>
<name lang="ca">Taulell normalment obert</name>
<name lang="cs">Kontakt čítače (NO)</name>
<name lang="da">Normalt åben tæller</name>
<name lang="de">Öffner Zähler betätigt</name>
<name lang="el">Ανοικτή επαφή μετρητή</name>
<name lang="en">Normally open counter</name>
<name lang="es">Contador NA</name>
<name lang="fi">Normaalisti avoin laskuri</name>
<name lang="fr">Actionné par comptage NO</name>
<name lang="hr">Brojač normalno otvoren</name>
<name lang="hu">Alaphelyzetben nyitott számláló</name>
<name lang="it">Contatto azionato da un contatore (NO)</name>
<name lang="ja">常開カウンター</name>
<name lang="ko">평상시 열림 카운터</name>
<name lang="mn">Ихэвчлэн нээлттэй тоолуур</name>
<name lang="nb">Normalt åpen teller</name>
<name lang="nl">Teller contact (NO)</name>
<name lang="nl_BE">Normaal open teller</name>
<name lang="pl">Zestyk zwierny licznika</name>
<name lang="pt">Contador normalmente aberto</name>
<name lang="pt_BR">Contato NA acionado por contador</name>
<name lang="ro">Contor normal deschis</name>
<name lang="ru">Счётчик с нормально открытым контактом</name>
<name lang="sk">Počítadlo s kontaktom NO</name>
<name lang="sl">Števec normalno odprt</name>
<name lang="sr">Бројач нормално отворен</name>
<name lang="sv">Räknare med slutande kontakt</name>
<name lang="tr">Normalde açık sayaç</name>
<name lang="uk">Лічильник нормально розімкнений</name>
<name lang="zh">计数驱动常开触点</name>
</names>
<informations>Author: The QElectroTech team
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
<description>
<rect antialias="false" height="10" rx="0" ry="0" style="line-style:normal;line-weight:thin;filling:none;color:black" width="10" x="-20" y="-15"/>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:dashed;line-weight:thin;filling:none;color:black" x1="-10" x2="-3" y1="-10" y2="-10"/>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a3321491-1dac-42e2-a9d1-1a43604116a9}" x="-13" y="-2" z="3">
<text>14</text>
</dynamic_text>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a82a123f-f3cf-4dd7-93c0-44bbdd940429}" x="-13" y="-34" z="4">
<text>11</text>
</dynamic_text>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{960d8065-c520-4d45-aa36-4bb600c0e6db}" x="1" y="-22" z="5">
<text/>
<info_name>label</info_name>
</dynamic_text>
<polygon antialias="true" closed="false" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="-5" x2="0" x3="0" y1="-20" y2="0" y3="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="-30" y2="-20"/>
<ellipse antialias="true" height="4" style="line-style:normal;line-weight:thin;filling:none;color:black" width="4" x="-17" y="-12"/>
<terminal name="" orientation="s" type="Generic" uuid="{572018e1-9843-49dd-964f-836ff27b8240}" x="0" y="10"/>
<terminal name="" orientation="n" type="Generic" uuid="{77224246-2278-498a-88e5-1ecd84e938e8}" x="0" y="-30"/>
</description>
</definition>
</element>
</category>
</category>
</category>
</category>
</category>
</collection>
</project>
@@ -0,0 +1,315 @@
<project title="Bugtracker 345" uuid="{55b219d1-12d1-5179-aef2-1e303429ecc6}" 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">skeleton</property>
<property name="savedfilepath" show="1">/tmp/qet-mcp-new-k41dmnyo/skeleton.qet</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="{18d804b7-0e2c-4858-b529-33d0eeb63249}" 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="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/02_contacts_cross_referencing/15_protection_contacts/contact_relais_nf_esclave.elmt" uuid="{8987e487-0140-4cb9-b68d-7a99be379ab5}" x="200" y="200" z="10">
<terminals>
<terminal id="0" orientation="0" x="0" y="-16"/>
<terminal id="1" orientation="2" x="0" y="16"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{7e7f129d-fefa-41e6-9cf2-aa665d302dcc}" x="5.75" y="-9.5">
<text></text>
<info_name>label</info_name>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,0,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="CompositeText" text_width="-1" uuid="{ea65f2b8-dec9-4968-a9a2-349ae1e66a9d}" x="20" y="0">
<text></text>
<composite_text>%{label}</composite_text>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
</diagram>
<collection>
<category name="import">
<names>
<name lang="ca">Elements importats</name>
<name lang="cs">Zavedené prvky</name>
<name lang="da">Importerede elementer</name>
<name lang="de">Importierte Elemente</name>
<name lang="el">Εισηγμένα στοιχεία</name>
<name lang="en">Imported elements</name>
<name lang="es">Elementos importados</name>
<name lang="fr">Éléments importés</name>
<name lang="hr">Uvezeni elementi</name>
<name lang="it">Elementi importati</name>
<name lang="ja">インバートされた要素</name>
<name lang="ko">전기 심볼 요소</name>
<name lang="ko_KR">전기 심볼 요소</name>
<name lang="nl">Elementen geïmporteerd</name>
<name lang="nl_BE">Elementen geïmporteerd</name>
<name lang="pl">Elementy importowane</name>
<name lang="pt">Elementos importados</name>
<name lang="pt_BR">Elementos importados</name>
<name lang="ro">Elemente importate</name>
<name lang="ru">Импортированные элементы</name>
<name lang="sl">Uvoženi elementi</name>
<name lang="sv">Importerade element</name>
<name lang="tr">İthal öğeler</name>
<name lang="uk">Імпортовані елементи</name>
<name lang="zh">导入元件</name>
</names>
<category name="10_electric">
<names>
<name lang="ar">كهربائي</name>
<name lang="ca">Elèctric</name>
<name lang="cs">Elektrotechnika</name>
<name lang="da">Elektrisk</name>
<name lang="de">Elektrik</name>
<name lang="el">Ηλεκτρικά</name>
<name lang="en">Electric</name>
<name lang="es">Eléctrica</name>
<name lang="fi">Sähkö</name>
<name lang="fr">Electrique</name>
<name lang="hr">Električno</name>
<name lang="hu">Elektromos</name>
<name lang="it">Elettrica</name>
<name lang="ja">電気</name>
<name lang="ko">전기</name>
<name lang="mn">Цахилгаан</name>
<name lang="nb">Elektrisk</name>
<name lang="nl">Elektrotechniek</name>
<name lang="nl_BE">Elektrisch</name>
<name lang="pl">Elektrotechnika</name>
<name lang="pt">Elétrico</name>
<name lang="pt_BR">Elétrica</name>
<name lang="ro">Electric</name>
<name lang="ru">Электротехника</name>
<name lang="sk">Elektrické</name>
<name lang="sl">Elektrika</name>
<name lang="sr">Електрика</name>
<name lang="sv">El</name>
<name lang="tr">Elektrik</name>
<name lang="uk">Електрика</name>
<name lang="zh">10_电气</name>
<name lang="zn">电气工程</name>
</names>
<category name="10_allpole">
<names>
<name lang="ar">متعدد الأقطاب</name>
<name lang="ca">Multifilar</name>
<name lang="cs">Vícežilový</name>
<name lang="da">Flere ledere</name>
<name lang="de">Allpolig</name>
<name lang="el">Πολυγραμμικό</name>
<name lang="en">All-pole</name>
<name lang="es">Multifilar</name>
<name lang="fi">Yleisnapa</name>
<name lang="fr">Multifilaire</name>
<name lang="hr">Univerzalni</name>
<name lang="hu">Többvonalas szimbólumok</name>
<name lang="it">Multifilare</name>
<name lang="ja">複線</name>
<name lang="ko">다-극</name>
<name lang="mn">Бүх туйл</name>
<name lang="nb">Alle poler</name>
<name lang="nl">Veel polig</name>
<name lang="nl_BE">All-pole</name>
<name lang="pl">Schematy wieloliniowe</name>
<name lang="pt">Multipolar</name>
<name lang="pt_BR">Multifilar</name>
<name lang="ro">Bipolar</name>
<name lang="ru">Многополюсные</name>
<name lang="sk">Všepólové</name>
<name lang="sl">Vsi poli</name>
<name lang="sr">Свe-полни</name>
<name lang="sv">Allpolig</name>
<name lang="tr">Çok kutuplu</name>
<name lang="uk">Багатополюсні</name>
<name lang="zh">10_全极</name>
</names>
<category name="310_relays_contactors_contacts">
<names>
<name lang="ar">المرحّلات والموصّلات وجهات الاتصال</name>
<name lang="ca">Relés, contactors i contactes</name>
<name lang="cs">Relé, stykače a kontakty</name>
<name lang="da">Relæer, kontaktorer og kontakter</name>
<name lang="de">Relais, Schütze und Kontakte</name>
<name lang="el">Ηλεκτρονόμοι και επαφές</name>
<name lang="en">Relays, contactors and contacts</name>
<name lang="es">Relevadores, contactores y contactos</name>
<name lang="fi">Releet, kontaktorit ja koskettimet</name>
<name lang="fr">Relais, contacteurs et contacts</name>
<name lang="hr">Releji, kontaktori i kontakti</name>
<name lang="hu">Relék, kontaktorok és érintkezők</name>
<name lang="it">Relè, contattori e contatti</name>
<name lang="ja">継電器、接触器、接点</name>
<name lang="ko">릴레이, 커넥터및 접점</name>
<name lang="mn">Реле, контактор болон контакт</name>
<name lang="nb">Reléer, kontaktorere og kontakter</name>
<name lang="nl">Relais, contactoren en contacten</name>
<name lang="nl_BE">Relais, contacteurs en contacten</name>
<name lang="pl">Przekaźniki i styczniki</name>
<name lang="pt">Relés, contatores e contactos</name>
<name lang="pt_BR">Relés, Contatores e contatos</name>
<name lang="ro">Relee, contactoare și contacte</name>
<name lang="ru">Реле, пускатели и контакты</name>
<name lang="sk">Relé, stykače a kontakty</name>
<name lang="sl">Releji, kontaktorji in kontakti</name>
<name lang="sr">Релеји, контактори и контакти</name>
<name lang="sv">Reläer, kontaktorer och kontakter</name>
<name lang="tr">Röleler, kontaktörler ve kontaklar</name>
<name lang="uk">Реле, контактори та контакти</name>
<name lang="zh">继电器、接触器与触点</name>
</names>
<category name="02_contacts_cross_referencing">
<names>
<name lang="ar">تماسات مع إحالات</name>
<name lang="ca">Contactes amb referències creuades</name>
<name lang="cs">Kontakty s křížovými odkazy</name>
<name lang="da">Kontaker med krydsreference</name>
<name lang="de">Kontakte mit Querverweis</name>
<name lang="el">Επαφές με παραπομπές</name>
<name lang="en">Contacts with cross referencing</name>
<name lang="es">Contactos con referencia cruzada</name>
<name lang="fi">Kosketimet lehtiviittauksella</name>
<name lang="fr">Contacts avec référence croisée</name>
<name lang="hr">Kontakti s unakrsnim referenciranjem</name>
<name lang="hu">Érintkezők kereszthivatkozásokkal</name>
<name lang="it">Contatti con riferimenti incrociati</name>
<name lang="ja">相互参照付き接点</name>
<name lang="ko">교차 참조 접점</name>
<name lang="mn">Харилцан холбоос бүхий контакт</name>
<name lang="nb">Kontakter med kryssreferanse</name>
<name lang="nl">Contacten met kruisreferentie</name>
<name lang="nl_BE">Contacten met kruisverwijzing</name>
<name lang="pl">Zestyki (automatyczne oznaczenia)</name>
<name lang="pt">Contactos com renvoi de folio</name>
<name lang="pt_BR">Contatos com referencia cruzada</name>
<name lang="ro">Contacte cu renvoi de folio</name>
<name lang="ru">Контакты с перекрестными ссылками</name>
<name lang="sk">Kontakty s krížovým odkazom</name>
<name lang="sl">Kontakti s sklici</name>
<name lang="sr">Контакти са унакрсним референцирањем</name>
<name lang="sv">Kontakter med bladhänvisning</name>
<name lang="tr">Çapraz referanslı kontaklar</name>
<name lang="uk">Контакти з перехресними посиланнями</name>
<name lang="zh">触点交叉引用</name>
</names>
<category name="15_protection_contacts">
<names>
<name lang="ar">حمايات</name>
<name lang="ca">Proteccions</name>
<name lang="cs">Ochrany</name>
<name lang="da">Beskyttelsesfunktioner</name>
<name lang="de">Schutzfunktion</name>
<name lang="el">Προστασία</name>
<name lang="en">Protections</name>
<name lang="es">Protecciones</name>
<name lang="fi">Suojaukset</name>
<name lang="fr">Protections</name>
<name lang="hr">Zaštite</name>
<name lang="hu">Védelmek</name>
<name lang="it">Protezioni</name>
<name lang="ja">保護</name>
<name lang="ko">보호 접점</name>
<name lang="mn">Хамгаалалт</name>
<name lang="nb">Beskyttelser</name>
<name lang="nl">Beveiliging</name>
<name lang="nl_BE">Beveiligingen</name>
<name lang="pl">Zabezpieczenia</name>
<name lang="pt">Protecções</name>
<name lang="pt_BR">Protecções</name>
<name lang="ro">Protecții</name>
<name lang="ru">Защитные уст-ва</name>
<name lang="sk">Ochrany</name>
<name lang="sl">Zaščite</name>
<name lang="sr">Заштите</name>
<name lang="sv">Skydd</name>
<name lang="tr">Korumalar</name>
<name lang="uk">Захисти</name>
<name lang="zh">保护触点</name>
</names>
<element name="contact_relais_nf_esclave.elmt">
<definition height="60" hotspot_x="9" hotspot_y="30" link_type="slave" type="element" version="0.100.0" width="20">
<uuid uuid="{94d56f06-6814-5561-9148-2da3a9c4f846}"/>
<names>
<name lang="ar">مُلامس مُرحّل حراري (NC)</name>
<name lang="ca">Relé tèrmic de contacte (NC)</name>
<name lang="cs">Kontakt (NC) tepelného jističe</name>
<name lang="da">Kontakt termisk relæ (NC)</name>
<name lang="de">Relaisöffner</name>
<name lang="el">Επαφή θερμικού κλειστή</name>
<name lang="en">Contact thermal relay (NC)</name>
<name lang="es">Contacto relé térmico NC</name>
<name lang="fi">Kosketin lämpörele (NC)</name>
<name lang="fr">Contact relais thermique (NF)</name>
<name lang="hr">Kontakt termičkog releja (NC)</name>
<name lang="hu">Hőrelé érintkező (NC)</name>
<name lang="it">Contatto relé térmico (NC)</name>
<name lang="ja">接点 熱動継電器 (NC)</name>
<name lang="ko">접점 열 릴레이 (NC)</name>
<name lang="mn">Дулааны релений контакт (NC)</name>
<name lang="nb">Kontakt termisk relé (NC)</name>
<name lang="nl">Thermisch bediend contact (NC)</name>
<name lang="nl_BE">Contact thermisch relais (NC)</name>
<name lang="pl">Zestyk rozwierny przekaźnika cieplnego</name>
<name lang="pt">Contacto relé térmico (NC)</name>
<name lang="pt_BR">Contato de relé térmico (NF)</name>
<name lang="ro">Contact releu termic (NC)</name>
<name lang="ru">Контакт теплового реле (НЗ)</name>
<name lang="sk">Kontakt tepelného relé (NC)</name>
<name lang="sl">Kontakt termičnega releja (NC)</name>
<name lang="sr">Контакт термичког релеја (NC)</name>
<name lang="sv">Kontakt termiskt relä (NC)</name>
<name lang="tr">Termik röle kontağı (NC)</name>
<name lang="uk">Контакт теплового реле (NC)</name>
<name lang="zh">继电器常闭从属触点</name>
</names>
<kindInformations>
<kindInformation name="type">simple</kindInformation>
<kindInformation name="state">NC</kindInformation>
<kindInformation name="number">1</kindInformation>
</kindInformations>
<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="6" y1="-9" y2="-9"/>
<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="-9"/>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{4a7b5a49-c49c-452b-8755-35b048dafc8e}" x="5.75" y="-9.5" z="4">
<text/>
<info_name>label</info_name>
</dynamic_text>
<polygon antialias="false" closed="false" style="line-style:normal;line-weight:thin;filling:none;color:black" x1="-4" x2="-4" x3="2" y1="4" y2="0" y3="0"/>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:thin;filling:none;color:black" x1="-2" x2="-2" y1="0" y2="3"/>
<terminal name="" orientation="n" type="Generic" uuid="{b69eb17b-2ec8-47c4-bf92-0065d7e4cd7b}" x="0" y="-20"/>
<terminal name="" orientation="s" type="Generic" uuid="{759c2015-c0d8-4059-8be1-cfbd8ce1bc76}" x="0" y="20"/>
</description>
</definition>
</element>
</category>
</category>
</category>
</category>
</category>
</category>
</collection>
</project>
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Linked by run_image_dark_probe.py against the application's own objects.
#include <QApplication>
#include <QBuffer>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QDomDocument>
#include <QFile>
#include <QFontDatabase>
#include <QPrinter>
#include <QPushButton>
#include <QSettings>
#include <QSvgGenerator>
#include <QTemporaryDir>
#include <cstdio>
#include <stdexcept>
#include "../../sources/diagram.h"
#include "../../sources/qetproject.h"
#include "../../sources/qetresult.h"
#include "../../sources/qetmessagebox.h"
#include "../../sources/qetpalette.h"
#include "../../sources/palettegraphicsview.h"
#include "../../sources/qetgraphicsitem/diagramimageitem.h"
#include "../../sources/ui/imagepropertieswidget.h"
#include "../../sources/PropertiesEditor/propertieseditordialog.h"
static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); }
static QByteArray png(const QPixmap &pixmap) {
QByteArray bytes; QBuffer buffer(&bytes); buffer.open(QIODevice::WriteOnly); pixmap.save(&buffer,"PNG"); return bytes;
}
static QByteArray read(const QString &path) { QFile file(path); check(file.open(QIODevice::ReadOnly),"read export"); return file.readAll(); }
static DiagramImageItem *findImage(Diagram *diagram) {
for (auto *item : diagram->items()) if (auto *image = dynamic_cast<DiagramImageItem *>(item)) return image;
return nullptr;
}
int main(int argc, char **argv)
{
QApplication app(argc,argv);
std::freopen(argv[2],"w",stdout); std::freopen(argv[2],"a",stderr);
QTemporaryDir settings;
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat,QSettings::UserScope,settings.path());
QCoreApplication::setOrganizationName("QETImageDarkRegression");
QETProject::setBackupEnabled(false); QET::QetMessageBox::setNonInteractive(true);
QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf");
try {
const QString output=QString::fromLocal8Bit(argv[3]);
QETProject project(QString::fromLocal8Bit(argv[1])); check(project.state()==QETProject::Ok,"load fixture");
auto *diagram=project.diagrams().first();
QImage original(40,30,QImage::Format_ARGB32); original.fill(QColor(197,81,33));
for(int y=0;y<30;++y) for(int x=10;x<20;++x) original.setPixelColor(x,y,QColor(197,81,33,128));
for(int y=10;y<16;++y) for(int x=24;x<30;++x) original.setPixelColor(x,y,Qt::transparent);
QPixmap source=QPixmap::fromImage(original);
auto *image=new DiagramImageItem(source); diagram->addItem(image);
image->setPos(55,55); image->setLabel("Image test"); image->setRotationAngle(17); image->setScaleFactorX(1.3); image->setSkewX(8);
check(!image->adaptToDarkTheme(),"new image defaults to original colours");
QDomDocument xml; auto legacy=image->toXml(xml);
check(!legacy.hasAttribute("adapt_to_dark_theme"),"default does not add an attribute to old files");
const QRect crop(4,3,24,20);
legacy.firstChild().setNodeValue(QString::fromLatin1(png(source.copy(crop)).toBase64()));
auto base=xml.createElement("image_base"); base.appendChild(xml.createTextNode(QString::fromLatin1(png(source).toBase64()))); legacy.appendChild(base);
auto cropElement=xml.createElement("crop"); cropElement.setAttribute("x",4); cropElement.setAttribute("y",3); cropElement.setAttribute("w",24); cropElement.setAttribute("h",20); legacy.appendChild(cropElement);
image->setAdaptToDarkTheme(true); check(image->fromXml(legacy),"load cropped legacy image");
check(!image->adaptToDarkTheme(),"missing attribute resets an existing image to original colours");
check(image->pixmap().size()==crop.size(),"crop survives");
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");
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");
diagram->undoStack().redo(); check(image->adaptToDarkTheme()&&box->isChecked(),"redo updates checkbox");
widget.setLiveEdit(true); box->click(); check(!image->adaptToDarkTheme(),"live edit");
check(diagram->undoStack().count()==2,"deliberate toggles do not merge");
diagram->undoStack().undo(); check(image->adaptToDarkTheme(),"live undo");
diagram->undoStack().redo(); check(!image->adaptToDarkTheme(),"live redo");
box->click(); check(image->adaptToDarkTheme()&&diagram->undoStack().count()==3,"edit after undo has current baseline");
for (auto role : {QDialogButtonBox::Cancel,QDialogButtonBox::Reset,QDialogButtonBox::Apply}) {
image->setAdaptToDarkTheme(false);
auto *editor=new ImagePropertiesWidget(image); PropertiesEditorDialog dialog(editor);
editor->findChild<QCheckBox *>("m_adapt_to_dark_theme_cb")->setChecked(true);
dialog.findChild<QDialogButtonBox *>()->button(role)->click();
check(image->adaptToDarkTheme()==(role==QDialogButtonBox::Apply),"actual dialog button semantics");
}
check(png(image->pixmap())==pristine && image->transform()==transform && image->boundingRect()==bounds && image->label()=="Image test","choice cannot modify pixels, transform, crop or caption");
image->setAdaptToDarkTheme(false);
{
auto *editor=new ImagePropertiesWidget(image); PropertiesEditorDialog dialog(editor);
editor->findChild<QCheckBox *>("m_adapt_to_dark_theme_cb")->setChecked(true);
dialog.reject(); check(!image->adaptToDarkTheme(),"Escape/title-bar rejection cancels preview");
}
PaletteGraphicsView view(diagram); view.setSceneRect(0,0,180,180); view.setFrameShape(QFrame::NoFrame);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setAlignment(Qt::AlignLeft|Qt::AlignTop); view.resize(180,180); view.setAttribute(Qt::WA_DontShowOnScreen); view.show();
for (bool dark : {false,true}) for(bool adapt : {false,true}) {
const auto palette=dark?QET::Palette::fusionDark():QET::Palette::fusionLight(); QApplication::setPalette(palette);
image->setAdaptToDarkTheme(adapt); QApplication::processEvents(); const auto shown=view.viewport()->grab().toImage();
shown.save(output+QString("/screen-%1-%2.png").arg(dark).arg(adapt));
const auto point=view.mapFromScene(image->mapToScene(QPointF(2,2))); const auto ratio=shown.devicePixelRatio();
QImage expected(1,1,QImage::Format_RGB32); expected.fill(QColor(197,81,33));
if(dark&&adapt) QET::Palette::invertLightness(expected,palette.color(QPalette::Base),palette.color(QPalette::Text));
check(shown.pixelColor(qRound(point.x()*ratio),qRound(point.y()*ratio))==expected.pixelColor(0,0),"actual rotated/cropped image display colour");
project.setFilePath(output+QString("/project-%1-%2.qet").arg(dark).arg(adapt)); check(project.write().isOk(),"project save");
QETProject reopened(project.filePath()); check(reopened.state()==QETProject::Ok,"project reopen");
auto *loaded=findImage(reopened.diagrams().first()); check(loaded && loaded->adaptToDarkTheme()==adapt,"per-image persisted choice");
check(png(loaded->pixmap())==pristine && loaded->transform()==transform && loaded->label()=="Image test","reopen keeps original image/crop/transform/caption");
}
QApplication::setPalette(QET::Palette::fusionDark()); QImage exports[2];
for(int i=0;i<2;++i) {
image->setAdaptToDarkTheme(i); exports[i]=QImage(180,180,QImage::Format_RGB32); exports[i].fill(Qt::white);
{QPainter painter(&exports[i]); diagram->render(&painter,QRectF(0,0,180,180),QRectF(0,0,180,180));}
exports[i].save(output+QString("/export-%1.png").arg(i));
QSvgGenerator svg; svg.setFileName(output+QString("/export-%1.svg").arg(i)); svg.setSize(QSize(180,180)); svg.setViewBox(QRect(0,0,180,180));
{QPainter painter(&svg); diagram->render(&painter,QRectF(0,0,180,180),QRectF(0,0,180,180));}
QPrinter printer; printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(output+QString("/print-%1.pdf").arg(i)); printer.setResolution(72);
{QPainter painter(&printer); check(painter.isActive(),"PDF print painter"); diagram->render(&painter,QRectF(0,0,180,180),QRectF(0,0,180,180));}
}
check(exports[0]==exports[1],"raster export unchanged"); check(read(output+"/export-0.svg")==read(output+"/export-1.svg"),"SVG export unchanged");
std::puts("PASS: image defaults, XML crop/alpha/transform/label, properties preview/apply/reset/cancel, undo/redo and live edit, four display modes, project save/reopen, raster/SVG export, PDF print generation");
} catch(const std::exception &error) {std::fprintf(stderr,"FAIL: %s\n",error.what());return 1;}
return 0;
}
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Link the attached plain-paste 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.', 'attached_plain_paste_probe.cpp.')
probe_binary = binary.with_name('attached_plain_paste_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 / 'attached-plain-paste-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 / 'attached-plain-paste'
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)
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Link the real image/widget 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
import zlib
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.', 'image_dark_integration_probe.cpp.')
probe_binary = binary.with_name('image_dark_integration_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 / 'image-dark-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 / 'image-dark-integration'
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'))
if result.returncode == 0:
def print_streams(path):
streams = re.findall(rb'\nstream\r?\n(.*?)\nendstream', path.read_bytes(), re.DOTALL)
decoded = []
for stream in streams:
try:
data = zlib.decompress(stream)
except zlib.error:
data = stream
# Qt assigns every PDF a fresh XMP DocumentID (and dates).
# Metadata is not printing content and must not be compared
# as if it were a graphics/image/font stream.
if b'<x:xmpmeta' not in data:
decoded.append(data)
return decoded
first = print_streams(output / 'print-0.pdf')
second = print_streams(output / 'print-1.pdf')
if not first or first != second:
sys.exit('PDF printing changed the decoded graphics/image/font streams.')
print('PASS: PDF print graphics/image/font streams identical for both choices.')
try:
import pymupdf
except ImportError:
print('PDF raster comparison not run: optional PyMuPDF is unavailable.')
else:
documents = [pymupdf.open(output / f'print-{i}.pdf') for i in range(2)]
if len(documents[0]) != len(documents[1]):
sys.exit('PDF page counts differ.')
for page in range(len(documents[0])):
pixels = [doc[page].get_pixmap(dpi=144, alpha=False) for doc in documents]
if (pixels[0].width, pixels[0].height, pixels[0].samples) != (pixels[1].width, pixels[1].height, pixels[1].samples):
sys.exit('Rasterized PDF print pages differ.')
for i, image in enumerate(pixels):
image.save(output / f'print-{i}-page-{page}.png')
print('PASS: PDF print pages rendered at 144 dpi are pixel-identical.')
sys.exit(result.returncode)
+119
View File
@@ -0,0 +1,119 @@
/*
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 "borderproperties.h"
#include <QtTest>
#include <QDomDocument>
/**
BorderProperties::fromXml() -- the <border> element a project keeps for
its new folios. Its column width and row height are decimals, written
by toXml() as "%1"; they used to be read with toInt(), which is 0 for
"60.5", and 0 is then clamped to the 5 px minimum by the folio.
*/
class tst_borderpropertiesxml : public QObject
{
Q_OBJECT
static QDomElement border(const QString &attributes)
{
static QDomDocument doc;
doc.setContent(QStringLiteral("<border %1/>").arg(attributes));
return doc.documentElement();
}
static BorderProperties base()
{
BorderProperties p;
p.columns_count = 17;
p.columns_width = 60;
p.rows_count = 8;
p.rows_height = 80;
p.display_columns = true;
p.display_rows = true;
return p;
}
private slots:
void sizesAreReadAsDecimals_data()
{
QTest::addColumn<QString>("colsize");
QTest::addColumn<QString>("rowsize");
QTest::addColumn<qreal>("width");
QTest::addColumn<qreal>("height");
QTest::newRow("whole numbers") << "50" << "100" << 50.0 << 100.0;
QTest::newRow("decimals") << "60.5" << "80.25" << 60.5 << 80.25;
QTest::newRow("rounds down") << "61.3" << "61.3" << 61.3 << 61.3;
QTest::newRow("rounds up") << "61.7" << "61.7" << 61.7 << 61.7;
}
void sizesAreReadAsDecimals()
{
QFETCH(QString, colsize);
QFETCH(QString, rowsize);
QFETCH(qreal, width);
QFETCH(qreal, height);
BorderProperties p = base();
QDomElement e = border(QStringLiteral("cols=\"17\" colsize=\"%1\" rows=\"8\" rowsize=\"%2\"")
.arg(colsize, rowsize));
p.fromXml(e);
QCOMPARE(p.columns_width, width);
QCOMPARE(p.rows_height, height);
QCOMPARE(p.columns_count, 17);
QCOMPARE(p.rows_count, 8);
}
void savedSizesComeBack()
{
BorderProperties saved = base();
saved.columns_width = 60.5;
saved.rows_height = 80.25;
QDomDocument doc;
QDomElement e = doc.createElement(QStringLiteral("border"));
saved.toXml(e);
BorderProperties loaded = base();
loaded.fromXml(e);
QCOMPARE(loaded.columns_width, 60.5);
QCOMPARE(loaded.rows_height, 80.25);
}
// An unreadable size leaves the previous value, as a missing one does.
void unreadableSizeIsLeftAlone_data()
{
QTest::addColumn<QString>("attributes");
QTest::newRow("missing") << "cols=\"17\" rows=\"8\"";
QTest::newRow("text") << "colsize=\"wide\" rowsize=\"tall\"";
QTest::newRow("nan") << "colsize=\"nan\" rowsize=\"nan\"";
QTest::newRow("inf") << "colsize=\"inf\" rowsize=\"-inf\"";
}
void unreadableSizeIsLeftAlone()
{
QFETCH(QString, attributes);
BorderProperties p = base();
QDomElement e = border(attributes);
p.fromXml(e);
QCOMPARE(p.columns_width, 60.0);
QCOMPARE(p.rows_height, 80.0);
}
};
QTEST_GUILESS_MAIN(tst_borderpropertiesxml)
#include "tst_borderpropertiesxml.moc"
+9 -5
View File
@@ -102,9 +102,11 @@ class tst_conductorapplychanges : public QObject
QTest::newRow(list.at(i).first) << i;
}
// The shown wire is all variant 0, the user changes one field
// to variant 1, another selected wire is all variant 2: it
// takes that one field and keeps every other one.
// The shown wire is all variant 0 and the user changes one
// field to variant 1. The other wire holds variant 1 in every
// other field, so copying one of them by mistake (they are
// variant 0 in the edit) shows, booleans included; and variant
// 2 in the edited field, so not copying it shows too.
void eachFieldAlone()
{
QFETCH(int, index);
@@ -115,9 +117,11 @@ class tst_conductorapplychanges : public QObject
set(after, 1);
QVERIFY(after != before);
ConductorProperties other = all(2);
ConductorProperties other = all(1);
set(other, 2);
ConductorProperties expected = other;
set(expected, 1);
QVERIFY(expected != other);
other.applyChanges(before, after);
QVERIFY(other == expected);
@@ -127,7 +131,7 @@ class tst_conductorapplychanges : public QObject
void noEditChangesNothing()
{
const ConductorProperties shown = all(0);
ConductorProperties other = all(2);
ConductorProperties other = all(1);
const ConductorProperties kept = other;
other.applyChanges(shown, shown);
QVERIFY(other == kept);
+189
View File
@@ -0,0 +1,189 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QUndoStack>
#include "conductormultiedit.h"
#include "qetapp.h"
// qet.cpp needs it; the application is not linked
QString QETApp::m_interface_language;
// A wire as ConductorMultiEdit sees one: properties through the same
// Q_PROPERTY the undo command writes, a potential, and a position.
class FakeWire : public QObject
{
Q_OBJECT
Q_PROPERTY(ConductorProperties properties READ properties WRITE setProperties)
public:
FakeWire(const QString &num, const QString &function,
const QString &cable, QPointF pos) :
m_pos(pos)
{
m_properties.text = num;
m_properties.m_function = function;
m_properties.m_cable = cable;
}
ConductorProperties properties() const { return m_properties; }
void setProperties(const ConductorProperties &p) { m_properties = p; }
QSet<FakeWire *> relatedPotentialConductors() const { return m_potential; }
QPointF pos() const { return m_pos; }
// Joins wires into one potential; like the real one, a wire's
// potential lists the others, not itself.
static void join(const QList<FakeWire *> &wires)
{
for (FakeWire *w : wires)
for (FakeWire *o : wires)
if (o != w)
w->m_potential.insert(o);
}
ConductorProperties m_properties;
private:
QSet<FakeWire *> m_potential;
QPointF m_pos;
};
// The rules for editing several wires at once in the Selection properties
// panel (#500): which wire is shown first, which wires an edit reaches,
// what one undo step does to all of them, and the fields shown blank
// because the wires do not agree on them.
class tst_conductormultiedit : public QObject
{
Q_OBJECT
using Wires = QList<FakeWire *>;
static ConductorProperties edited(ConductorProperties p,
QString ConductorProperties::*field,
const QString &value)
{
p.*field = value;
return p;
}
private slots:
// Same wires, any selection order: same first wire
void firstWireIsStable()
{
FakeWire a("1", "", "", {50, 10}), b("2", "", "", {10, 10}),
c("3", "", "", {0, 90});
const auto pos = [](FakeWire *w) { return w->pos(); };
Wires one {&c, &a, &b}, two {&a, &b, &c};
ConductorMultiEdit::sortByPosition(one, pos);
ConductorMultiEdit::sortByPosition(two, pos);
QCOMPARE(one, (Wires {&b, &a, &c}));
QCOMPARE(two, one);
}
void targetsWithoutPotential()
{
FakeWire a("1", "", "", {}), b("2", "", "", {}), c("3", "", "", {});
FakeWire::join({&a, &c});
QCOMPARE(ConductorMultiEdit::targets(Wires {&a, &b, &a}, false),
(Wires {&a, &b}));
}
// Each wire once: a selected wire already reached through
// another one's potential is not added again
void targetsWithPotential()
{
FakeWire a("1", "", "", {}), b("1", "", "", {}), c("1", "", "", {}),
d("2", "", "", {});
FakeWire::join({&a, &b, &c});
const Wires t = ConductorMultiEdit::targets(Wires {&a, &d, &c}, true);
QCOMPARE(t.size(), 4);
QCOMPARE(QSet<FakeWire *>(t.begin(), t.end()),
(QSet<FakeWire *> {&a, &b, &c, &d}));
}
// One edit of three wires: each takes the function, keeps its
// own number and cable; one undo restores all three exactly
void undoRedoThreeWires()
{
FakeWire a("101", "fnA", "W1", {}), b("102", "fnB", "W2", {}),
c("103", "fnC", "W3", {});
const Wires wires {&a, &b, &c};
const ConductorProperties before_a = a.properties(),
before_b = b.properties(), before_c = c.properties();
const ConductorProperties shown = a.properties();
QUndoStack stack;
stack.push(ConductorMultiEdit::undo(wires, shown,
edited(shown, &ConductorProperties::m_function, "PWR")));
QCOMPARE(stack.count(), 1);
for (FakeWire *w : wires)
QCOMPARE(w->m_properties.m_function, QStringLiteral("PWR"));
QCOMPARE(b.m_properties.text, QStringLiteral("102"));
QCOMPARE(c.m_properties.m_cable, QStringLiteral("W3"));
stack.undo();
QVERIFY(a.properties() == before_a);
QVERIFY(b.properties() == before_b);
QVERIFY(c.properties() == before_c);
stack.redo();
QCOMPARE(c.m_properties.m_function, QStringLiteral("PWR"));
QCOMPARE(c.m_properties.text, QStringLiteral("103"));
}
void noUndoWhenNothingChanges()
{
FakeWire a("1", "PWR", "", {}), b("2", "PWR", "", {});
const ConductorProperties shown = a.properties();
QCOMPARE(ConductorMultiEdit::undo(Wires {&a, &b}, shown, shown),
static_cast<QUndoCommand *>(nullptr));
// An edit every wire already has
QCOMPARE(ConductorMultiEdit::undo(Wires {&a, &b}, edited(shown,
&ConductorProperties::m_function, "X"), shown),
static_cast<QUndoCommand *>(nullptr));
}
// Wires that disagree on a text field show it blank; typing the
// first wire's own value then reaches every wire (1.5 mm² on
// a 1.5 and a 2.5 wire)
void mixedFieldTakesFirstWiresValue()
{
FakeWire a("1", "fn", "", {}), b("2", "fn", "", {});
a.m_properties.m_wire_section = "1.5";
b.m_properties.m_wire_section = "2.5";
const QList<ConductorProperties> list {a.properties(), b.properties()};
const auto mixed = ConductorMultiEdit::mixedTextFields(list);
QCOMPARE(mixed, (QList<ConductorMultiEdit::TextField> {
ConductorMultiEdit::Text, ConductorMultiEdit::WireSection}));
const ConductorProperties shown = ConductorMultiEdit::shown(list, mixed);
QVERIFY(shown.m_wire_section.isEmpty());
QCOMPARE(shown.m_function, QStringLiteral("fn"));
QUndoStack stack;
stack.push(ConductorMultiEdit::undo(Wires {&a, &b}, shown,
edited(shown, &ConductorProperties::m_wire_section, "1.5")));
QCOMPARE(a.m_properties.m_wire_section, QStringLiteral("1.5"));
QCOMPARE(b.m_properties.m_wire_section, QStringLiteral("1.5"));
// Another field edited while the section is mixed leaves
// each wire's section alone
a.m_properties.m_wire_section = "1.5";
b.m_properties.m_wire_section = "2.5";
stack.push(ConductorMultiEdit::undo(Wires {&a, &b}, shown,
edited(shown, &ConductorProperties::m_function, "PWR")));
QCOMPARE(a.m_properties.m_wire_section, QStringLiteral("1.5"));
QCOMPARE(b.m_properties.m_wire_section, QStringLiteral("2.5"));
}
void oneWireIsNeverMixed()
{
FakeWire a("1", "fn", "", {});
QVERIFY(ConductorMultiEdit::mixedTextFields({a.properties()}).isEmpty());
}
};
QTEST_GUILESS_MAIN(tst_conductormultiedit)
#include "tst_conductormultiedit.moc"
+305
View File
@@ -0,0 +1,305 @@
/*
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/>.
*/
// SPDX-License-Identifier: GPL-2.0-or-later
#include "conductorrouter.h"
#include <QtTest>
/**
The geometry of ConductorRouter on its own: that a route is made of
horizontal and vertical segments, leaves and enters each terminal in
the terminal's direction, and goes around obstacles rather than
through them.
*/
class TestConductorRouter : public QObject
{
Q_OBJECT
using Direction = ConductorRouter::Direction;
static bool crosses(const QList<QPointF> &points, const QRectF &r)
{
for (int i = 0; i + 1 < points.size(); ++i) {
const QPointF a = points.at(i), b = points.at(i + 1);
const QRectF seg = QRectF(a, b).normalized();
const qreal left = qMax(seg.left(), r.left()), right = qMin(seg.right(), r.right());
const qreal top = qMax(seg.top(), r.top()), bottom = qMin(seg.bottom(), r.bottom());
// a segment is a degenerate rectangle: it crosses the
// interior when it overlaps it on one axis and lies
// strictly inside it on the other
if (seg.width() == 0 && seg.left() > r.left() && seg.left() < r.right() && bottom > top)
return true;
if (seg.height() == 0 && seg.top() > r.top() && seg.top() < r.bottom() && right > left)
return true;
}
return false;
}
static void checkShape(const QList<QPointF> &p, const ConductorRouter::Request &r)
{
QVERIFY(p.size() >= 3);
QCOMPARE(p.first(), r.start);
QCOMPARE(p.last(), r.end);
for (int i = 0; i + 1 < p.size(); ++i)
QVERIFY2(p.at(i).x() == p.at(i + 1).x() || p.at(i).y() == p.at(i + 1).y(),
"a segment is neither horizontal nor vertical");
}
static QPointF unit(Direction d)
{
switch (d) {
case Direction::North: return {0, -1};
case Direction::East: return {1, 0};
case Direction::South: return {0, 1};
case Direction::West: return {-1, 0};
}
return {};
}
static QPointF direction(QPointF from, QPointF to)
{
const QPointF d = to - from;
return {d.x() > 0 ? 1. : d.x() < 0 ? -1. : 0., d.y() > 0 ? 1. : d.y() < 0 ? -1. : 0.};
}
private slots:
void straightWhenNothingIsInTheWay()
{
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction::East;
r.end = {300, 100};
r.end_direction = Direction::West;
const auto result = ConductorRouter::route(r);
checkShape(result.points, r);
// no bend at all: start, the two exit points, end
for (const QPointF &p : result.points) QCOMPARE(p.y(), 100.);
}
void goesAroundASymbol()
{
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction::East;
r.end = {300, 100};
r.end_direction = Direction::West;
const QRectF symbol(170, 60, 60, 80);
r.obstacles << symbol;
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
checkShape(result.points, r);
QVERIFY(!crosses(result.points, symbol.adjusted(-r.margin, -r.margin, r.margin, r.margin)));
}
// The test that the check above can fail: a straight line through
// the symbol is reported as crossing it.
void crossingCheckCanFail()
{
QVERIFY(crosses({{100, 100}, {300, 100}}, QRectF(170, 60, 60, 80)));
}
void leavesAndEntersInTheTerminalsDirections_data()
{
QTest::addColumn<int>("from");
QTest::addColumn<int>("to");
for (int a = 0; a < 4; ++a)
for (int b = 0; b < 4; ++b)
QTest::addRow("%d-%d", a, b) << a << b;
}
void leavesAndEntersInTheTerminalsDirections()
{
QFETCH(int, from);
QFETCH(int, to);
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction(from);
r.end = {250, 180};
r.end_direction = Direction(to);
// each terminal's own symbol, on the side opposite the way
// the terminal points
const QPointF s = unit(r.start_direction), e = unit(r.end_direction);
const QRectF own_start(r.start - s * 40 - QPointF(20, 20), QSizeF(40, 40));
const QRectF own_end(r.end - e * 40 - QPointF(20, 20), QSizeF(40, 40));
r.obstacles << own_start << own_end;
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
checkShape(result.points, r);
const auto &p = result.points;
QCOMPARE(direction(p.at(0), p.at(1)), s);
QCOMPARE(direction(p.at(p.size() - 1), p.at(p.size() - 2)), e);
QVERIFY(!crosses(p, own_start));
QVERIFY(!crosses(p, own_end));
}
void prefersNotToRunAlongAnotherWire()
{
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction::East;
r.end = {300, 100};
r.end_direction = Direction::West;
// a wire lying exactly on the straight route
r.wires << QVector<QPointF>{{110, 100}, {290, 100}};
const auto result = ConductorRouter::route(r);
checkShape(result.points, r);
bool on_it = false;
for (int i = 1; i + 2 < result.points.size(); ++i)
if (result.points.at(i).y() == 100 && result.points.at(i + 1).y() == 100)
on_it = true;
QVERIFY(!on_it);
}
void staysOnTheFolio()
{
ConductorRouter::Request r;
r.start = {100, 30};
r.start_direction = Direction::East;
r.end = {300, 30};
r.end_direction = Direction::West;
r.obstacles << QRectF(170, 0, 60, 200);
r.bounds = QRectF(0, 0, 500, 400);
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
for (const QPointF &p : result.points) QVERIFY(r.bounds.contains(p));
}
void reportsWhenThereIsNoWay()
{
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction::East;
r.end = {300, 100};
r.end_direction = Direction::West;
// a wall from the top of the folio to the bottom
r.obstacles << QRectF(170, -10, 60, 520);
r.bounds = QRectF(0, 0, 500, 500);
const auto result = ConductorRouter::route(r);
QVERIFY(result.points.isEmpty());
QVERIFY(!result.error.isEmpty());
}
void terminalsOffTheGrid()
{
ConductorRouter::Request r;
r.start = {103, 97};
r.start_direction = Direction::South;
r.end = {287, 213};
r.end_direction = Direction::North;
r.obstacles << QRectF(120, 140, 200, 30);
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
checkShape(result.points, r);
}
// #1178: two contacts one above the other, the upper one's bottom
// terminal wired to the lower one's top terminal. Each docking
// point is 10 px inside its symbol, as in con_simple.elmt. Close
// together, the route went down through the lower contact, back
// up past the upper one and down again.
void closeFacingTerminalsGoStraight_data()
{
QTest::addColumn<int>("gap");
for (int gap : {2, 8, 10, 18, 20, 28, 30, 38, 40, 60})
QTest::addRow("%d px", gap) << gap;
}
void closeFacingTerminalsGoStraight()
{
QFETCH(int, gap);
ConductorRouter::Request r;
r.start = {100, 120};
r.start_direction = Direction::South;
r.end = {100, 120. + gap};
r.end_direction = Direction::North;
r.start_symbol = QRectF(90, 70, 20, 60);
r.end_symbol = QRectF(90, r.end.y() - 10, 20, 60);
r.obstacles << r.start_symbol << r.end_symbol;
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
checkShape(result.points, r);
for (int i = 0; i + 1 < result.points.size(); ++i) {
QCOMPARE(result.points.at(i).x(), 100.);
QVERIFY2(result.points.at(i + 1).y() > result.points.at(i).y(),
"the route turns back on itself");
}
}
// The straight line is taken only when nothing is in its way.
void facingTerminalsGoAroundWhatIsBetweenThem()
{
ConductorRouter::Request r;
r.start = {100, 120};
r.start_direction = Direction::South;
r.end = {100, 250};
r.end_direction = Direction::North;
r.start_symbol = QRectF(90, 70, 20, 60);
r.end_symbol = QRectF(90, 240, 20, 60);
const QRectF between(80, 170, 40, 30);
r.obstacles << r.start_symbol << r.end_symbol << between;
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
checkShape(result.points, r);
QVERIFY(!crosses(result.points, between.adjusted(-r.margin, -r.margin, r.margin, r.margin)));
}
// A terminal pointing straight into a symbol that is not the other
// end: no route, rather than one that runs through that symbol.
void noRouteThroughASymbolInFrontOfATerminal()
{
ConductorRouter::Request r;
r.start = {100, 120};
r.start_direction = Direction::South;
r.end = {300, 300};
r.end_direction = Direction::West;
r.start_symbol = QRectF(90, 70, 20, 60);
r.end_symbol = QRectF(300, 280, 40, 40);
const QRectF in_front(60, 132, 80, 40);
r.obstacles << r.start_symbol << r.end_symbol << in_front;
const auto result = ConductorRouter::route(r);
QVERIFY(result.points.isEmpty());
QVERIFY(!result.error.isEmpty());
}
// A symbol drawn around others (a cabinet made as one element):
// the wire between two symbols inside it stays inside it, going
// round what lies between them.
void routesInsideAnEnclosingSymbol()
{
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction::East;
r.end = {300, 100};
r.end_direction = Direction::West;
r.start_symbol = QRectF(60, 80, 40, 40);
r.end_symbol = QRectF(300, 80, 40, 40);
const QRectF frame(20, 20, 360, 200);
const QRectF between(170, 60, 60, 80);
r.obstacles << frame << r.start_symbol << r.end_symbol << between;
r.bounds = QRectF(0, 0, 500, 400);
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
checkShape(result.points, r);
QVERIFY(!crosses(result.points, between.adjusted(-r.margin, -r.margin, r.margin, r.margin)));
for (const QPointF &p : result.points)
QVERIFY2(frame.contains(p), "the route leaves the frame");
}
};
QTEST_MAIN(TestConductorRouter)
#include "tst_conductorrouter.moc"
+113
View File
@@ -0,0 +1,113 @@
/*
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 "conductorproperties.h"
#include "qetapp.h"
#include <QtTest>
#include <QSettings>
// qet.cpp, compiled in for ConductorProperties, refers to this static.
QString QETApp::m_interface_language;
/**
ConductorProperties::toSettings()/fromSettings() keep the conductor
width (cond_size) as it was saved. The width is a decimal, written with
QString::number(); reading it with toInt() gave 0 for every value that
was not a whole number, so a default width of 1.4 came back as 0 after
a restart.
This test owns its own QSettings scope (organization + application
name), so it cannot read or write the configuration of whoever runs it.
*/
class tst_conductorsizesetting : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
QCoreApplication::setOrganizationName(
QStringLiteral("QElectroTech-tst_conductorsizesetting"));
QCoreApplication::setApplicationName(QStringLiteral("tst_conductorsizesetting"));
QSettings settings;
settings.clear();
}
void init()
{
QSettings settings;
settings.clear();
settings.sync();
}
void widthSurvivesARoundTrip_data()
{
QTest::addColumn<qreal>("width");
QTest::newRow("whole number") << 2.0;
QTest::newRow("decimal") << 1.4;
QTest::newRow("minimum") << 0.4;
QTest::newRow("rounds down") << 61.3;
QTest::newRow("rounds up") << 61.7;
}
void widthSurvivesARoundTrip()
{
QFETCH(qreal, width);
QSettings settings;
ConductorProperties saved;
saved.cond_size = width;
saved.toSettings(settings, QStringLiteral("test/"));
ConductorProperties loaded;
loaded.fromSettings(settings, QStringLiteral("test/"));
QCOMPARE(loaded.cond_size, width);
}
// A hand-edited or truncated settings file must not give a width of 0.
void unusableValueFallsBackToOne_data()
{
QTest::addColumn<QString>("value");
QTest::newRow("text") << QStringLiteral("wide");
QTest::newRow("zero") << QStringLiteral("0");
QTest::newRow("negative") << QStringLiteral("-1");
QTest::newRow("nan") << QStringLiteral("nan");
QTest::newRow("inf") << QStringLiteral("inf");
}
void unusableValueFallsBackToOne()
{
QFETCH(QString, value);
QSettings settings;
settings.setValue(QStringLiteral("test/size"), value);
ConductorProperties loaded;
loaded.fromSettings(settings, QStringLiteral("test/"));
QCOMPARE(loaded.cond_size, 1.0);
}
void missingValueIsOne()
{
QSettings settings;
ConductorProperties loaded;
loaded.fromSettings(settings, QStringLiteral("test/"));
QCOMPARE(loaded.cond_size, 1.0);
}
};
QTEST_GUILESS_MAIN(tst_conductorsizesetting)
#include "tst_conductorsizesetting.moc"
+93
View File
@@ -0,0 +1,93 @@
/*
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 "ui/customizedialog.h"
#include "ui/configpage/configpage.h"
#include <QDialogButtonBox>
#include <QIcon>
#include <QPushButton>
#include <QTabWidget>
#include <QTest>
/// A configuration page that counts how often it was applied
class CountingPage : public ConfigPage
{
public:
CountingPage(const QString &title) : ConfigPage(nullptr), m_title(title) {}
void applyConf() override { ++applied; }
QString title() const override { return m_title; }
QIcon icon() const override { return QIcon(); }
int applied = 0;
private:
QString m_title;
};
/**
CustomizeDialog: one tab per page in the order added, OK applies every
page once, Cancel applies none.
*/
class tst_customizedialog : public QObject
{
Q_OBJECT
private slots:
void tabsInOrder()
{
CustomizeDialog dialog;
auto *a = new CountingPage(QStringLiteral("Barres d'outils"));
auto *b = new CountingPage(QStringLiteral("Clavier"));
dialog.addPage(a);
dialog.addPage(b);
dialog.addPage(a); // twice: ignored
dialog.addPage(nullptr);
auto *tabs = dialog.findChild<QTabWidget *>(QStringLiteral("customizeTabs"));
QVERIFY(tabs);
QCOMPARE(tabs->count(), 2);
QCOMPARE(tabs->tabText(0), QStringLiteral("Barres d'outils"));
QCOMPARE(tabs->tabText(1), QStringLiteral("Clavier"));
QCOMPARE(tabs->widget(1), b);
}
void okAppliesEveryPage()
{
CustomizeDialog dialog;
auto *a = new CountingPage(QStringLiteral("a"));
auto *b = new CountingPage(QStringLiteral("b"));
dialog.addPage(a);
dialog.addPage(b);
dialog.show();
dialog.findChild<QDialogButtonBox *>()->button(QDialogButtonBox::Ok)->click();
QCOMPARE(a->applied, 1);
QCOMPARE(b->applied, 1);
QCOMPARE(dialog.result(), int(QDialog::Accepted));
}
void cancelAppliesNothing()
{
CustomizeDialog dialog;
auto *a = new CountingPage(QStringLiteral("a"));
dialog.addPage(a);
dialog.show();
dialog.findChild<QDialogButtonBox *>()->button(QDialogButtonBox::Cancel)->click();
QCOMPARE(a->applied, 0);
QCOMPARE(dialog.result(), int(QDialog::Rejected));
}
};
QTEST_MAIN(tst_customizedialog)
#include "tst_customizedialog.moc"
+129
View File
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QGraphicsRectItem>
#include <QGraphicsScene>
#include "darkimagerendering.h"
#include "palettegraphicsview.h"
#include "qetpalette.h"
class Raster : public QGraphicsItem {
public:
QPixmap pixmap;
bool adapt = false;
explicit Raster(QGraphicsItem *parent = nullptr) : QGraphicsItem(parent) {
QImage image(40, 30, QImage::Format_ARGB32);
image.fill(QColor(197, 81, 33));
for (int y = 0; y < 30; ++y)
for (int x = 10; x < 20; ++x) image.setPixelColor(x, y, QColor(197,81,33,128));
for (int y = 0; y < 30; ++y)
for (int x = 30; x < 40; ++x) image.setPixelColor(x, y, Qt::transparent);
pixmap = QPixmap::fromImage(image);
}
QRectF boundingRect() const override { return QRectF(0,0,40,40); }
void paint(QPainter *p, const QStyleOptionGraphicsItem *, QWidget *) override {
DarkImageRendering::paintPixmap(p, pixmap, adapt);
p->fillRect(QRectF(0,34,8,4), Qt::black); // caption/decorations remain adapted
}
};
class tst_darkimagelayers : public QObject {
Q_OBJECT
static QColor sample(const QImage &image, const PaletteGraphicsView &view, QPointF scene) {
const QPoint point = view.mapFromScene(scene);
const qreal dpr = image.devicePixelRatio();
return image.pixelColor(qRound(point.x()*dpr), qRound(point.y()*dpr));
}
static void setup(PaletteGraphicsView &view) {
view.setFrameShape(QFrame::NoFrame);
view.setAlignment(Qt::AlignLeft | Qt::AlignTop);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.resize(100,80); view.show();
}
private slots:
void cleanup() {
QApplication::setPalette(QET::Palette::fusionLight());
PaletteGraphicsView::setCustomBackgroundColor(false);
}
void modes_data() {
QTest::addColumn<bool>("dark"); QTest::addColumn<bool>("adapt"); QTest::addColumn<bool>("custom");
QTest::newRow("light-original") << false << false << false;
QTest::newRow("light-adapt") << false << true << false;
QTest::newRow("dark-original") << true << false << false;
QTest::newRow("dark-adapt") << true << true << false;
QTest::newRow("light-custom-original") << false << false << true;
QTest::newRow("light-custom-adapt") << false << true << true;
QTest::newRow("dark-custom-original") << true << false << true;
QTest::newRow("dark-custom-adapt") << true << true << true;
}
void modes() {
QFETCH(bool,dark); QFETCH(bool,adapt); QFETCH(bool,custom);
PaletteGraphicsView::setCustomBackgroundColor(custom);
const QPalette palette = dark ? QET::Palette::fusionDark() : QET::Palette::fusionLight();
QApplication::setPalette(palette);
const QColor background = custom ? QColor(80,90,100) : QColor(Qt::white);
QGraphicsScene scene(0,0,100,80); scene.setBackgroundBrush(background);
auto *raster = new Raster; raster->adapt = adapt; raster->setPos(10,10); scene.addItem(raster);
scene.addRect(32,10,5,20,QPen(Qt::NoPen),QBrush(Qt::black))->setZValue(1);
PaletteGraphicsView view(&scene); setup(view);
QVERIFY(QTest::qWaitForWindowExposed(&view));
const QImage shown = view.viewport()->grab().toImage();
QImage expected(100,80,QImage::Format_RGB32);
expected.fill(custom ? background : (dark && !adapt ? palette.color(QPalette::Base) : QColor(Qt::white)));
QPainter p(&expected);
if (dark && custom && adapt) {
QImage adapted = raster->pixmap.toImage();
QET::Palette::invertLightnessLayer(adapted,palette.color(QPalette::Base),palette.color(QPalette::Text));
p.drawImage(QPoint(10,10),adapted);
} else p.drawPixmap(QPoint(10,10),raster->pixmap);
p.fillRect(QRect(32,10,5,20), dark && !adapt && !custom ? palette.color(QPalette::Text) : QColor(Qt::black));
p.end();
if (dark && adapt && !custom) QET::Palette::invertLightness(expected,palette.color(QPalette::Base),palette.color(QPalette::Text));
for (const QPoint point : {QPoint(15,15),QPoint(25,15),QPoint(45,15),QPoint(34,15)})
QCOMPARE(sample(shown,view,point), expected.pixelColor(point));
QCOMPARE(sample(shown,view,QPoint(13,45)), dark && !custom ? palette.color(QPalette::Text) : QColor(Qt::black));
}
void parentClippingRotationOpacityAndOverlap() {
const auto palette = QET::Palette::fusionDark(); QApplication::setPalette(palette);
QGraphicsScene scene(0,0,100,80); scene.setBackgroundBrush(Qt::white);
auto *parent = scene.addRect(0,0,25,40,QPen(Qt::NoPen),QBrush(Qt::NoBrush));
parent->setFlag(QGraphicsItem::ItemClipsChildrenToShape); parent->setPos(35,15); parent->setRotation(12);
auto *first = new Raster(parent); first->setOpacity(0.5);
auto *second = new Raster; second->setPos(36,30); scene.addItem(second); second->setZValue(2);
QImage blue(40,30,QImage::Format_ARGB32); blue.fill(QColor(20,80,210,128)); second->pixmap=QPixmap::fromImage(blue);
PaletteGraphicsView view(&scene); setup(view); QVERIFY(QTest::qWaitForWindowExposed(&view));
const auto shown = view.viewport()->grab().toImage();
// Original pixels blend once over the dark sheet, then the upper
// original image blends once over those pixels. No inverse transform.
QImage reference(1,1,QImage::Format_RGB32); reference.fill(palette.color(QPalette::Base));
QPainter p(&reference); p.setOpacity(0.5); p.fillRect(reference.rect(),QColor(197,81,33)); p.end();
QCOMPARE(sample(shown,view,first->mapToScene(QPointF(5,5))),reference.pixelColor(0,0));
QPainter upper(&reference); upper.fillRect(reference.rect(),QColor(20,80,210,128)); upper.end();
QCOMPARE(sample(shown,view,first->mapToScene(QPointF(5,20))),reference.pixelColor(0,0));
// Pixel in the child's transparent stripe / beyond the parent's clip.
QCOMPARE(sample(shown,view,first->mapToScene(QPointF(28,5))),palette.color(QPalette::Base));
first->setPos(3,0); scene.update(); QApplication::processEvents();
QVERIFY(!view.viewport()->grab().isNull());
}
void alphaMapping() {
QImage image(2,1,QImage::Format_ARGB32_Premultiplied);
image.setPixelColor(0,0,QColor(0,0,0,128)); image.setPixelColor(1,0,Qt::transparent);
QET::Palette::invertLightnessLayer(image,QColor(30,30,30),QColor(220,220,220));
QCOMPARE(image.pixelColor(0,0),QColor(220,220,220,128));
QCOMPARE(image.pixelColor(1,0).alpha(),0);
}
void exportUnaffected() {
QApplication::setPalette(QET::Palette::fusionDark());
QGraphicsScene scene(0,0,100,80); scene.setBackgroundBrush(Qt::white);
auto *raster=new Raster; raster->setPos(10,10); scene.addItem(raster);
QImage results[2];
for(int i=0;i<2;++i) {
raster->adapt=i; results[i]=QImage(100,80,QImage::Format_RGB32); results[i].fill(Qt::white);
QPainter p(&results[i]); scene.render(&p,QRectF(0,0,100,80),scene.sceneRect());
}
QCOMPARE(results[0],results[1]);
QCOMPARE(results[0].pixelColor(15,15),QColor(197,81,33));
}
};
QTEST_MAIN(tst_darkimagelayers)
#include "tst_darkimagelayers.moc"
+18
View File
@@ -65,6 +65,24 @@ private slots:
QCOMPARE(fromDom(xml(value)), expected);
QCOMPARE(fromPugi(xml(value)), expected);
}
// remove() forgets the key entirely: a context with a key added
// then removed equals one that never had it. remove() used to keep
// the key's show flag, so the two never compared equal, and
// Project properties > OK saw a change where there was none.
void removeForgetsTheKey()
{
DiagramContext edited;
edited.addValue(QStringLiteral("author"), QStringLiteral("N.V."));
edited.addValue(QStringLiteral("01-numprojet"), QString());
edited.remove(QStringLiteral("01-numprojet"));
DiagramContext stored;
stored.addValue(QStringLiteral("author"), QStringLiteral("N.V."));
QCOMPARE(edited.keys(), stored.keys());
QVERIFY(edited == stored);
}
};
QTEST_APPLESS_MAIN(tst_diagramcontext)
+245
View File
@@ -0,0 +1,245 @@
/*
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 "diagramtoolbarsettings.h"
#include "shortcutmanager.h"
#include "ui/configpage/toolbarcommandsconfigpage.h"
#include <QAction>
#include <QComboBox>
#include <QListWidget>
#include <QPushButton>
#include <QSettings>
#include <QStandardPaths>
#include <QTest>
#include <QToolBar>
#include <QWidget>
/**
A top-level window standing in for the diagram editor: counts the
calls applyToAll() makes.
*/
class RebuildCounter : public QWidget
{
Q_OBJECT
public:
int rebuilds = 0;
public slots:
void rebuildToolBars() { ++rebuilds; }
};
/**
DiagramToolbarSettings and its configuration page: an unchanged
toolbar stores nothing and is built as before, unknown ids and stray
separators are skipped, the user's own toolbars are kept and removed
with their contents, and the page saves what it shows.
*/
class tst_diagramtoolbarsettings : public QObject
{
Q_OBJECT
QObject m_owner;
QHash<QString, QAction *> m_actions;
/// The action for @a id, made on first use, as the editor's resolver
QAction *resolve(const QString &id)
{
if (id.startsWith(QLatin1String("missing."))) return nullptr;
if (!m_actions.contains(id)) {
auto *action = new QAction(id, &m_owner);
m_actions.insert(id, action);
}
return m_actions.value(id);
}
static QStringList shape(QToolBar *toolbar)
{
QStringList list;
for (QAction *action : toolbar->actions()) {
list << (action->isSeparator() ? QStringLiteral("|") : action->text());
}
return list;
}
static void clearSettings()
{
QSettings settings;
settings.remove(QStringLiteral("diagrameditor/toolbars"));
settings.remove(QStringLiteral("diagrameditor/custom_toolbars"));
}
private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
clearSettings();
//A few of the diagram editor's commands, as it registers them
for (const QString &id : {QStringLiteral("diagrameditor.copy"),
QStringLiteral("diagrameditor.paste"),
QStringLiteral("diagrameditor.zoom_fit"),
QStringLiteral("depth.raise"),
QStringLiteral("elementeditor.copy")}) {
ShortcutManager::instance().registerAction(
new QAction(id, &m_owner), id, QStringLiteral("test"), QKeySequence());
}
}
void init() { clearSettings(); }
void defaultsStoreNothing()
{
for (const QString &name : DiagramToolbarSettings::builtInNames()) {
QVERIFY(!DiagramToolbarSettings::defaultIds(name).isEmpty());
QCOMPARE(DiagramToolbarSettings::ids(name), DiagramToolbarSettings::defaultIds(name));
DiagramToolbarSettings::setIds(name, DiagramToolbarSettings::defaultIds(name));
}
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/toolbar")));
DiagramToolbarSettings::setIds(QStringLiteral("toolbar"), {QStringLiteral("diagrameditor.copy")});
QCOMPARE(DiagramToolbarSettings::ids(QStringLiteral("toolbar")),
QStringList{QStringLiteral("diagrameditor.copy")});
//Saving an empty toolbar keeps it empty, not back to the defaults
DiagramToolbarSettings::setIds(QStringLiteral("toolbar"), {});
QVERIFY(DiagramToolbarSettings::ids(QStringLiteral("toolbar")).isEmpty());
}
/// The defaults are built exactly as the toolbars were hand-built
/// before: the main toolbar's three separators in place
void defaultsBuildAsBefore()
{
QToolBar toolbar;
DiagramToolbarSettings::fill(&toolbar, DiagramToolbarSettings::defaultIds(QStringLiteral("toolbar")),
[this](const QString &id) { return resolve(id); });
QCOMPARE(toolbar.actions().size(), 18);
QCOMPARE(shape(&toolbar).mid(6, 2),
(QStringList{QStringLiteral("diagrameditor.export_to_pdf"), QStringLiteral("|")}));
QCOMPARE(shape(&toolbar).count(QStringLiteral("|")), 3);
}
void fillSkipsUnknownAndStraySeparators()
{
const QString sep = DiagramToolbarSettings::separatorId();
QToolBar toolbar;
toolbar.addAction(QStringLiteral("left over"));
DiagramToolbarSettings::fill(&toolbar,
{sep, QStringLiteral("a"), sep, sep, QStringLiteral("missing.b"),
QStringLiteral("c"), sep, QStringLiteral("missing.d"), sep},
[this](const QString &id) { return resolve(id); });
QCOMPARE(shape(&toolbar), (QStringList{QStringLiteral("a"), QStringLiteral("|"), QStringLiteral("c")}));
DiagramToolbarSettings::fill(&toolbar, {}, [this](const QString &id) { return resolve(id); });
QVERIFY(toolbar.actions().isEmpty());
}
void customToolbarsKeptAndRemoved()
{
using T = DiagramToolbarSettings::Toolbar;
const QString first = DiagramToolbarSettings::newCustomName({});
QCOMPARE(first, QStringLiteral("custom_1"));
DiagramToolbarSettings::setCustomToolbars({T{first, QStringLiteral("Mine"), true},
T{QStringLiteral("custom_2"), QStringLiteral("Other"), true}});
DiagramToolbarSettings::setIds(first, {QStringLiteral("diagrameditor.copy")});
QCOMPARE(DiagramToolbarSettings::customToolbars().size(), 2);
QCOMPARE(DiagramToolbarSettings::customToolbars().first().title, QStringLiteral("Mine"));
QCOMPARE(DiagramToolbarSettings::toolbars().size(),
DiagramToolbarSettings::builtInNames().size() + 2);
QCOMPARE(DiagramToolbarSettings::newCustomName({}), QStringLiteral("custom_3"));
//A new toolbar starts empty
QVERIFY(DiagramToolbarSettings::ids(QStringLiteral("custom_2")).isEmpty());
DiagramToolbarSettings::setCustomToolbars({T{QStringLiteral("custom_2"), QStringLiteral("Other"), true}});
QCOMPARE(DiagramToolbarSettings::customToolbars().size(), 1);
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/") + first));
DiagramToolbarSettings::setCustomToolbars({});
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/custom_toolbars/names")));
}
void availableCommandsAreTheDiagramEditors()
{
const QStringList ids = DiagramToolbarSettings::availableCommandIds();
QVERIFY(ids.contains(QStringLiteral("diagrameditor.copy")));
QVERIFY(ids.contains(QStringLiteral("depth.raise")));
QVERIFY(!ids.contains(QStringLiteral("elementeditor.copy")));
}
void pageSavesAndRebuilds()
{
RebuildCounter marked, unmarked;
DiagramToolbarSettings::markWindow(&marked);
ToolbarCommandsConfigPage page;
auto *combo = page.findChild<QComboBox *>(QStringLiteral("toolbarCombo"));
auto *available = page.findChild<QListWidget *>(QStringLiteral("availableList"));
auto *chosen = page.findChild<QListWidget *>(QStringLiteral("chosenList"));
QVERIFY(combo && available && chosen);
QCOMPARE(combo->count(), DiagramToolbarSettings::builtInNames().size());
QCOMPARE(chosen->count(), DiagramToolbarSettings::defaultIds(QStringLiteral("toolbar")).size());
//OK without a change stores nothing
page.applyConf();
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/toolbar")));
QCOMPARE(marked.rebuilds, 1);
QCOMPARE(unmarked.rebuilds, 0);
//Remove everything but the first command, add a separator
while (chosen->count() > 1) delete chosen->takeItem(1);
chosen->setCurrentRow(0);
page.findChild<QPushButton *>(QStringLiteral("separatorButton"))->click();
page.applyConf();
QCOMPARE(DiagramToolbarSettings::ids(QStringLiteral("toolbar")),
(QStringList{QStringLiteral("diagrameditor.new_file"), DiagramToolbarSettings::separatorId()}));
QCOMPARE(marked.rebuilds, 2);
}
/// A widget button can be in one place only: once moved to another
/// toolbar it is no longer offered, and resetting its own toolbar
/// takes it back
void widgetInOnePlace()
{
ToolbarCommandsConfigPage page;
auto *combo = page.findChild<QComboBox *>(QStringLiteral("toolbarCombo"));
auto *available = page.findChild<QListWidget *>(QStringLiteral("availableList"));
auto *chosen = page.findChild<QListWidget *>(QStringLiteral("chosenList"));
auto offered = [available](const QString &id) {
for (int i = 0 ; i < available->count() ; ++i)
if (available->item(i)->data(Qt::UserRole).toString() == id) return true;
return false;
};
const QString colour = QStringLiteral("widget:conductor_color");
//On "diagram" by default, so not offered for "toolbar"
QVERIFY(!offered(colour));
//Taken off "diagram" (switching toolbars keeps the change)
combo->setCurrentIndex(combo->findData(QStringLiteral("diagram")));
for (int i = chosen->count() - 1 ; i >= 0 ; --i) {
if (chosen->item(i)->data(Qt::UserRole).toString() == colour) delete chosen->takeItem(i);
}
combo->setCurrentIndex(combo->findData(QStringLiteral("toolbar")));
QVERIFY(offered(colour));
//Reset "diagram": the colour button goes back there
combo->setCurrentIndex(combo->findData(QStringLiteral("diagram")));
page.findChild<QPushButton *>(QStringLiteral("resetButton"))->click();
QCOMPARE(chosen->count(), DiagramToolbarSettings::defaultIds(QStringLiteral("diagram")).size());
combo->setCurrentIndex(combo->findData(QStringLiteral("toolbar")));
QVERIFY(!offered(colour));
}
};
QTEST_MAIN(tst_diagramtoolbarsettings)
#include "tst_diagramtoolbarsettings.moc"
+147
View File
@@ -0,0 +1,147 @@
/*
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>
/**
--export-dxf --dxf-blocks (issue #1339): a symbol placed twice is one
BLOCK and two INSERTs, the second turned a quarter, and each symbol's
information is a hidden attribute of it; with --dxf-attributes its
label is a visible one. Without the switches there is neither.
*/
class tst_dxfblocks : public QObject
{
Q_OBJECT
/// The group code / value pairs of the exported folio
static QList<QPair<QString, QString>> exportPairs(const QStringList &extra)
{
const QString project = QFINDTESTDATA("fixtures/dxf_blocks.qet");
QTemporaryDir dir;
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
QStringList{QStringLiteral("--export-dxf"), project, dir.path()} + extra);
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
return {};
QFile file(dir.filePath(QStringLiteral("01_diagram.dxf")));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return {};
const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
QList<QPair<QString, QString>> pairs;
for (int i = 0 ; i + 1 < values.size() ; i += 2)
pairs << qMakePair(values.at(i).trimmed(), values.at(i + 1).trimmed());
return pairs;
}
/// The values of code @a code in each entity of type @a type
static QStringList values(const QList<QPair<QString, QString>> &pairs,
const QString &type, const QString &code)
{
QStringList found;
for (int i = 0 ; i < pairs.size() ; ++i) {
if (pairs.at(i).first != QLatin1String("0") || pairs.at(i).second != type)
continue;
for (int j = i + 1 ; j < pairs.size() && pairs.at(j).first != QLatin1String("0") ; ++j)
if (pairs.at(j).first == code)
found << pairs.at(j).second;
}
return found;
}
private slots:
void blocks()
{
const auto pairs = exportPairs({QStringLiteral("--dxf-blocks")});
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-blocks failed");
QCOMPARE(values(pairs, QStringLiteral("BLOCK"), QStringLiteral("2")),
QStringList{QStringLiteral("QET_CURVES")});
QCOMPARE(values(pairs, QStringLiteral("INSERT"), QStringLiteral("2")),
(QStringList{QStringLiteral("QET_CURVES"), QStringLiteral("QET_CURVES")}));
QCOMPARE(values(pairs, QStringLiteral("INSERT"), QStringLiteral("50")),
(QStringList{QStringLiteral("0"), QStringLiteral("270")}));
//The circle is drawn once, inside the block
QCOMPARE(values(pairs, QStringLiteral("CIRCLE"), QStringLiteral("8")).size(), 1);
}
void labelsAsAttributes()
{
const auto pairs = exportPairs({QStringLiteral("--dxf-attributes")});
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-attributes failed");
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("2"))
.count(QStringLiteral("LABEL")), 2);
const QStringList texts = values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("1"));
QVERIFY(texts.contains(QStringLiteral("K1")));
QVERIFY(texts.contains(QStringLiteral("K2")));
//Defined once in the block, and no longer a loose text
QCOMPARE(values(pairs, QStringLiteral("ATTDEF"), QStringLiteral("2"))
.count(QStringLiteral("LABEL")), 1);
QVERIFY(!values(pairs, QStringLiteral("TEXT"), QStringLiteral("1")).contains(QStringLiteral("K1")));
}
void labelsStayTextsWithBlocksOnly()
{
const auto pairs = exportPairs({QStringLiteral("--dxf-blocks")});
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-blocks failed");
//Only hidden attributes (70 = 1); the labels are texts
QVERIFY(!values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("70")).contains(QStringLiteral("0")));
QVERIFY(values(pairs, QStringLiteral("TEXT"), QStringLiteral("1")).contains(QStringLiteral("K1")));
}
void dataAsHiddenAttributes()
{
//The label is a text, so it is part data too: K1's label and
//manufacturer, K2's label, all hidden (70 = 1)
auto pairs = exportPairs({QStringLiteral("--dxf-blocks")});
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-blocks failed");
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("2")),
(QStringList{QStringLiteral("LABEL"), QStringLiteral("MANUFACTURER"),
QStringLiteral("LABEL")}));
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("70")),
(QStringList{QStringLiteral("1"), QStringLiteral("1"), QStringLiteral("1")}));
QVERIFY(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("1"))
.contains(QStringLiteral("Schneider")));
//With the labels as attributes, the label is not repeated
pairs = exportPairs({QStringLiteral("--dxf-attributes")});
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-attributes failed");
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("2")),
(QStringList{QStringLiteral("LABEL"), QStringLiteral("MANUFACTURER"),
QStringLiteral("LABEL")}));
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("70")),
(QStringList{QStringLiteral("0"), QStringLiteral("1"), QStringLiteral("0")}));
}
void flatWithoutSwitch()
{
const auto pairs = exportPairs({});
QVERIFY2(!pairs.isEmpty(), "--export-dxf failed");
QVERIFY(values(pairs, QStringLiteral("BLOCK"), QStringLiteral("2")).isEmpty());
QVERIFY(values(pairs, QStringLiteral("INSERT"), QStringLiteral("2")).isEmpty());
QCOMPARE(values(pairs, QStringLiteral("CIRCLE"), QStringLiteral("8")).size(), 2);
QVERIFY(values(pairs, QStringLiteral("TEXT"), QStringLiteral("1")).contains(QStringLiteral("K1")));
}
};
QTEST_APPLESS_MAIN(tst_dxfblocks)
#include "tst_dxfblocks.moc"
+95
View File
@@ -0,0 +1,95 @@
/*
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>
/**
--export-dxf on a folio whose title block is at the right (issue #1339):
a symbol's circle is one CIRCLE and its arc one ARC with the symbol's own
angles, which needs the same scale on both axes; the title block is
turned a quarter, as it is on screen.
*/
class tst_dxfcurves : public QObject
{
Q_OBJECT
/// The entities on @a layer, each as its group codes and values.
static QList<QMultiHash<int, QString>> entities(const QStringList &values,
const QString &type,
const QString &layer)
{
QList<QMultiHash<int, QString>> found;
for (int i = 0 ; i + 1 < values.size() ; i += 2)
{
if (values.at(i).trimmed() != QLatin1String("0")
|| values.at(i + 1).trimmed() != type)
continue;
QMultiHash<int, QString> entity;
for (int j = i + 2 ; j + 1 < values.size()
&& values.at(j).trimmed() != QLatin1String("0") ; j += 2)
entity.insert(values.at(j).trimmed().toInt(), values.at(j + 1).trimmed());
if (entity.value(8) == layer)
found << entity;
}
return found;
}
private slots:
void curvesAndTitleBlock()
{
const QString project = QFINDTESTDATA("fixtures/dxf_curves.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'));
const QString symbols = QStringLiteral("QET_SYMBOLS");
//The 20 px circle: one CIRCLE, nothing approximated
const auto circles = entities(values, QStringLiteral("CIRCLE"), symbols);
QCOMPARE(circles.size(), 1);
QVERIFY(entities(values, QStringLiteral("POLYLINE"), symbols).isEmpty());
//The arc from 30 to 150 degrees, same radius as the circle
const auto arcs = entities(values, QStringLiteral("ARC"), symbols);
QCOMPARE(arcs.size(), 1);
QCOMPARE(arcs.first().value(50).toDouble(), 30.0);
QCOMPARE(arcs.first().value(51).toDouble(), 150.0);
QCOMPARE(arcs.first().value(40).toDouble(), circles.first().value(40).toDouble());
//Every title block text turned a quarter, as drawn on screen
const auto texts = entities(values, QStringLiteral("TEXT"), QStringLiteral("QET_TITLEBLOCK"));
QVERIFY(!texts.isEmpty());
for (const auto &text : texts)
QCOMPARE(text.value(50).toDouble(), 90.0);
}
};
QTEST_APPLESS_MAIN(tst_dxfcurves)
#include "tst_dxfcurves.moc"
+60
View File
@@ -0,0 +1,60 @@
/*
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>
/**
--export-dxf writes a symbol text wrapped to its width line by line,
as it is drawn, rather than as one long line. A text without a width
is still written on one line.
*/
class tst_dxfwrappedtext : public QObject
{
Q_OBJECT
private slots:
void wrappedLines()
{
const QString project = QFINDTESTDATA("fixtures/dxf_wrapped_text.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'));
//One text entity per word of the text 70 px wide...
for (const QString &word : {QStringLiteral("WrapAlpha"), QStringLiteral("WrapGamma"), QStringLiteral("WrapEpsilon")})
QVERIFY2(values.contains(word), qPrintable(word));
//...one for the whole text without a width
QVERIFY(values.contains(QStringLiteral("LineAlpha LineBeta LineGamma LineDelta LineEpsilon")));
}
};
QTEST_APPLESS_MAIN(tst_dxfwrappedtext)
#include "tst_dxfwrappedtext.moc"
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include "editor/elementviewgrid.h"
// The symbol editor's snap step at each zoom (bugtracker #112): below
// 100 % the grid is hidden but parts must still snap every 10 units.
class tst_elementviewgrid : public QObject
{
Q_OBJECT
private slots:
void stepAtZoom_data()
{
QTest::addColumn<qreal>("zoom");
QTest::addColumn<int>("step");
QTest::addColumn<bool>("drawGrid");
QTest::addColumn<bool>("drawCross");
QTest::newRow("10 % (fit to a big symbol)") << 0.10 << 10 << false << false;
QTest::newRow("62.5 %, the #112 case") << 0.625 << 10 << false << false;
QTest::newRow("100 %") << 1.0 << 10 << true << false;
QTest::newRow("200 %") << 2.0 << 10 << true << false;
QTest::newRow("400 %") << 4.0 << 5 << true << true;
QTest::newRow("800 %") << 8.0 << 2 << true << true;
QTest::newRow("1000 %") << 10.0 << 1 << true << true;
// Just below each threshold: the comparisons are strict (<), so a
// change to <= would move these rows into the next band.
QTest::newRow("99.9 %") << 0.999 << 10 << false << false;
QTest::newRow("399.9 %") << 3.999 << 10 << true << false;
QTest::newRow("401 %") << 4.01 << 5 << true << true;
QTest::newRow("799.9 %") << 7.999 << 5 << true << true;
QTest::newRow("999.9 %") << 9.999 << 2 << true << true;
}
void stepAtZoom()
{
QFETCH(qreal, zoom);
QFETCH(int, step);
QFETCH(bool, drawGrid);
QFETCH(bool, drawCross);
const ElementViewGrid grid = ElementViewGrid::forZoom(zoom);
QCOMPARE(grid.step, step);
QCOMPARE(grid.draw_grid, drawGrid);
QCOMPARE(grid.draw_cross, drawCross);
}
};
QTEST_APPLESS_MAIN(tst_elementviewgrid)
#include "tst_elementviewgrid.moc"
+103
View File
@@ -0,0 +1,103 @@
/*
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 <QDomDocument>
#include <QProcess>
#include <QTemporaryDir>
/**
A folio's "freeze new elements" and "freeze new conductors" flags are
saved as freezeNewElement="true" and freezeNewConductor="true" on its
<diagram>. Loading must read the words back: the flags used to be read
with toInt(), which is 0 for "true" as well as for "false", so both were
off again after every reload. Runs the real binary's --resave on a
project with one folio per combination and reads the saved attributes.
*/
class tst_foliofreezeflags : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
static bool runQet(const QStringList &arguments)
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
return proc.waitForFinished(60000)
&& proc.exitStatus() == QProcess::NormalExit
&& proc.exitCode() == 0;
}
/// The <diagram> elements of a saved project, in file order.
static QList<QDomElement> diagrams(const QString &path)
{
QFile file(path);
QDomDocument doc;
if (!file.open(QIODevice::ReadOnly) || !doc.setContent(&file)) return {};
QList<QDomElement> list;
for (QDomElement e = doc.documentElement().firstChildElement(QStringLiteral("diagram"));
!e.isNull();
e = e.nextSiblingElement(QStringLiteral("diagram")))
list << e;
return list;
}
private slots:
void initTestCase()
{
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
QVERIFY(m_dir.isValid());
}
// Folio 1 has both flags, folio 2 only the element one, folio 3 none,
// and folio 4 is an older file without the attributes at all.
void flagsSurviveASave_data()
{
QTest::addColumn<int>("folio");
QTest::addColumn<QString>("element");
QTest::addColumn<QString>("conductor");
QTest::newRow("both frozen") << 0 << "true" << "true";
QTest::newRow("elements frozen") << 1 << "true" << "false";
QTest::newRow("not frozen") << 2 << "false" << "false";
QTest::newRow("no attributes") << 3 << "false" << "false";
}
void flagsSurviveASave()
{
QFETCH(int, folio);
QFETCH(QString, element);
QFETCH(QString, conductor);
const QString fixture = QFINDTESTDATA("fixtures/folio_freeze_flags.qet");
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
const QString out = m_dir.filePath(QStringLiteral("resaved%1.qet").arg(folio));
QVERIFY2(runQet({QStringLiteral("--resave"), fixture, out}), "--resave failed");
const QList<QDomElement> list = diagrams(out);
QCOMPARE(list.size(), 4);
const QDomElement e = list.at(folio);
QCOMPARE(e.attribute(QStringLiteral("freezeNewElement")), element);
QCOMPARE(e.attribute(QStringLiteral("freezeNewConductor")), conductor);
}
};
QTEST_MAIN(tst_foliofreezeflags)
#include "tst_foliofreezeflags.moc"
+99
View File
@@ -0,0 +1,99 @@
/*
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 "foliogrid.h"
#include <QtTest>
#include <QSettings>
/**
FolioGrid::step() -- the folio grid step as read from the settings.
Every reader of diagrameditor/Xgrid and Ygrid goes through it, so a
settings file holding 0 (a division by zero in Diagram::snapToGrid()
and an endless "% 0" in drawBackground()), a negative number (an
endless loop) or text can no longer reach the code that uses the step.
*/
class tst_foliogrid : public QObject
{
Q_OBJECT
private slots:
void usableValuesAreKept_data()
{
QTest::addColumn<QVariant>("value");
QTest::addColumn<int>("expected");
QTest::newRow("one") << QVariant(1) << 1;
QTest::newRow("default") << QVariant(10) << 10;
QTest::newRow("text number") << QVariant(QStringLiteral("7")) << 7;
QTest::newRow("large") << QVariant(100000) << 100000;
QTest::newRow("int max") << QVariant(2147483647) << 2147483647;
}
void usableValuesAreKept()
{
QFETCH(QVariant, value);
QFETCH(int, expected);
QCOMPARE(FolioGrid::step(value, 10), expected);
}
void unusableValuesFallBack_data()
{
QTest::addColumn<QVariant>("value");
QTest::newRow("missing") << QVariant();
QTest::newRow("zero") << QVariant(0);
QTest::newRow("negative") << QVariant(-5);
QTest::newRow("text") << QVariant(QStringLiteral("ten"));
QTest::newRow("empty") << QVariant(QString());
QTest::newRow("nan") << QVariant(QStringLiteral("nan"));
QTest::newRow("too large") << QVariant(QStringLiteral("99999999999"));
QTest::newRow("too large number") << QVariant(qlonglong(99999999999));
QTest::newRow("int max + 1") << QVariant(qlonglong(2147483648));
}
void unusableValuesFallBack()
{
QFETCH(QVariant, value);
QCOMPARE(FolioGrid::step(value, 10), 10);
QCOMPARE(FolioGrid::step(value, 20), 20);
}
// A decimal in the file is rounded to a whole number, as the plain
// QVariant::toInt() read did before.
void decimalIsRoundedLikeBefore()
{
QCOMPARE(FolioGrid::step(QVariant(7.9), 10), 8);
QCOMPARE(FolioGrid::step(QVariant(0.4), 10), 10);
}
// Through a real settings file, in a scope of this test's own.
void readsTheSettingsKeys()
{
QCoreApplication::setOrganizationName(QStringLiteral("QElectroTech-tst_foliogrid"));
QCoreApplication::setApplicationName(QStringLiteral("tst_foliogrid"));
QSettings settings;
settings.clear();
QCOMPARE(FolioGrid::step(settings.value(FolioGrid::x_key), 10), 10);
settings.setValue(FolioGrid::x_key, 0);
settings.setValue(FolioGrid::y_key, 15);
QCOMPARE(FolioGrid::step(settings.value(FolioGrid::x_key), 10), 10);
QCOMPARE(FolioGrid::step(settings.value(FolioGrid::y_key), 10), 15);
settings.clear();
}
};
QTEST_GUILESS_MAIN(tst_foliogrid)
#include "tst_foliogrid.moc"
+113
View File
@@ -0,0 +1,113 @@
/*
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 <QDomDocument>
#include <QProcess>
#include <QRegularExpression>
#include <QTemporaryDir>
#include <QTextDocument>
/// Run the qelectrotech binary with arguments, without a display.
static bool runQet(const QStringList &arguments)
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
return proc.waitForFinished(60000)
&& proc.exitStatus() == QProcess::NormalExit
&& proc.exitCode() == 0;
}
/// The number of lines the words starting with prefix are drawn on.
static int drawnLines(const QString &svg, const QString &prefix)
{
const QRegularExpression text_re(
QStringLiteral("<text[^>]*\\by=\"([^\"]+)\"[^>]*>\\s*%1").arg(prefix));
QSet<QString> lines;
auto it = text_re.globalMatch(svg);
while (it.hasNext())
lines.insert(it.next().captured(1));
return lines.size();
}
/**
A free text with a width (text_width on its <input>) keeps it when the
project is saved again, and wraps to it. A text without a width is saved
without the attribute, as before, so older versions read it unchanged.
*/
class tst_freetextwidth : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
m_fixture = QFINDTESTDATA("fixtures/free_text_width.qet");
QVERIFY2(!m_fixture.isEmpty(), "fixture project not found");
QVERIFY(m_dir.isValid());
}
void widthIsSavedOnlyWhenSet()
{
const QString out = m_dir.filePath(QStringLiteral("resaved.qet"));
QVERIFY2(runQet({QStringLiteral("--resave"), m_fixture, out}), "--resave failed");
QFile file(out);
QVERIFY(file.open(QIODevice::ReadOnly));
QDomDocument document;
QVERIFY(document.setContent(&file));
QMap<QString, QString> widths; // first word -> text_width
const QDomNodeList inputs = document.elementsByTagName(QStringLiteral("input"));
for (int i = 0 ; i < inputs.size() ; ++i) {
const QDomElement input = inputs.at(i).toElement();
QTextDocument text;
text.setHtml(input.attribute(QStringLiteral("text")));
const QString first_word = text.toPlainText().section(QLatin1Char(' '), 0, 0);
widths.insert(first_word, input.hasAttribute(QStringLiteral("text_width"))
? input.attribute(QStringLiteral("text_width"))
: QStringLiteral("none"));
}
QCOMPARE(widths.value(QStringLiteral("FreeAlpha")), QStringLiteral("70"));
QCOMPARE(widths.value(QStringLiteral("OpenAlpha")), QStringLiteral("none"));
//Centred lines: the width of the user, not the one setHtml() gives
QCOMPARE(widths.value(QStringLiteral("CentAlpha")), QStringLiteral("120"));
}
void textWrapsToItsWidth()
{
QVERIFY2(runQet({QStringLiteral("--export-svg"), m_fixture, m_dir.path()}), "--export-svg failed");
QFile file(m_dir.filePath(QStringLiteral("01_diagram.svg")));
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
const QString svg = QString::fromUtf8(file.readAll());
QVERIFY(drawnLines(svg, QStringLiteral("Free")) > 1);
QCOMPARE(drawnLines(svg, QStringLiteral("Open")), 1);
}
private:
QString m_fixture;
QTemporaryDir m_dir;
};
QTEST_MAIN(tst_freetextwidth)
#include "tst_freetextwidth.moc"
+214
View File
@@ -0,0 +1,214 @@
/*
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 "diagramgestureoverlay.h"
#include "gesturesettings.h"
#include "shortcutmanager.h"
#include "ui/configpage/gesturesconfigpage.h"
#include <QAction>
#include <QComboBox>
#include <QDropEvent>
#include <QListWidget>
#include <QMainWindow>
#include <QMimeData>
#include <QPushButton>
#include <QSettings>
#include <QStandardPaths>
#include <QTest>
/**
The mouse gesture ring's own command list: until the user picks one it
follows the shortcut bar as before; a picked list keeps its directions;
4 or 8 directions; and the configuration page that edits it.
*/
class tst_gesturesettings : public QObject
{
Q_OBJECT
QMainWindow m_window;
QAction *addCommand(const QString &id, const QString &text)
{
auto *action = new QAction(text, &m_window);
ShortcutManager::instance().registerAction(
action, id, QStringLiteral("Éditeur de schémas"), QKeySequence());
return action;
}
QListWidgetItem *listItem(QListWidget *list, const QString &id)
{
for (int i = 0 ; i < list->count() ; ++i) {
if (list->item(i)->data(Qt::UserRole).toString() == id) {
return list->item(i);
}
}
return nullptr;
}
private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
QSettings().remove(QStringLiteral("diagrameditor"));
addCommand(QStringLiteral("diagrameditor.copy"), QStringLiteral("Copier"));
addCommand(QStringLiteral("diagrameditor.cut"), QStringLiteral("Couper"));
addCommand(QStringLiteral("diagrameditor.paste"), QStringLiteral("Coller"));
}
void cleanup()
{
QSettings().remove(QStringLiteral("diagrameditor"));
}
void defaultsFollowTheBar()
{
QCOMPARE(GestureSettings::directions(), 8);
for (const auto c : ShortcutBarSettings::contexts()) {
QVERIFY(!GestureSettings::isCustom(c));
QCOMPARE(GestureSettings::ids(c), ShortcutBarSettings::ids(c));
}
//A changed bar still reaches the ring, without its elements
ShortcutBarSettings::setIds(ShortcutBarSettings::Selection,
{QStringLiteral("diagrameditor.copy"),
QStringLiteral("common://10_electric/relay.elmt"),
QStringLiteral("diagrameditor.cut")});
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Selection),
QStringList({QStringLiteral("diagrameditor.copy"),
QStringLiteral("diagrameditor.cut")}));
}
void pickedListKeepsDirections()
{
GestureSettings::setIds(ShortcutBarSettings::Canvas,
{QStringLiteral("diagrameditor.copy"), QString(),
QStringLiteral("diagrameditor.paste"), QString(), QString()});
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Canvas));
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas),
QStringList({QStringLiteral("diagrameditor.copy"), QString(),
QStringLiteral("diagrameditor.paste")}));
//The bar no longer drives it
ShortcutBarSettings::setIds(ShortcutBarSettings::Canvas,
{QStringLiteral("diagrameditor.cut")});
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas).first(),
QStringLiteral("diagrameditor.copy"));
//Other contexts still follow the bar
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Selection));
}
void savingTheDefaultsFollowsTheBarAgain()
{
GestureSettings::setIds(ShortcutBarSettings::Conductor, {QStringLiteral("diagrameditor.cut")});
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Conductor));
GestureSettings::setIds(ShortcutBarSettings::Conductor,
GestureSettings::defaultIds(ShortcutBarSettings::Conductor));
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Conductor));
}
void directions()
{
GestureSettings::setDirections(4);
QCOMPARE(GestureSettings::directions(), 4);
GestureSettings::setDirections(8);
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/gestures/directions")));
QSettings().setValue(QStringLiteral("diagrameditor/gestures/directions"), 5);
QCOMPARE(GestureSettings::directions(), 8);
}
void overlayDirectionsAndEmptySlots()
{
QWidget viewport;
viewport.resize(400, 400);
DiagramGestureOverlay overlay(&viewport);
QAction *copy = ShortcutManager::instance().action(QStringLiteral("diagrameditor.copy"), &m_window);
QAction *paste = ShortcutManager::instance().action(QStringLiteral("diagrameditor.paste"), &m_window);
const QPoint c(200, 200);
const QPoint right(260, 200), down(200, 260);
overlay.showAt(c, {copy, nullptr, paste}, 4);
QCOMPARE(overlay.sectors(), 4);
QCOMPARE(overlay.sectorAt(right), 1);
QCOMPARE(overlay.actionAt(right), nullptr);
QCOMPARE(overlay.actionAt(down), paste);
overlay.setPointer(right);
QVERIFY(!overlay.grab().isNull());
//Today's ring: eight directions
overlay.showAt(c, {copy, nullptr, paste});
QCOMPARE(overlay.sectors(), 8);
QCOMPARE(overlay.sectorAt(right), 2);
QCOMPARE(overlay.actionAt(right), paste);
}
void pagePlacesAndSaves()
{
{
GesturesConfigPage page;
auto *directions = page.findChild<QComboBox *>(QStringLiteral("directionsCombo"));
auto *list = page.findChild<QListWidget *>(QStringLiteral("availableList"));
auto *ring = page.findChild<GestureRingEditor *>(QStringLiteral("ringEditor"));
QCOMPARE(directions->currentData().toInt(), 8);
QCOMPARE(ring->ids(), GestureSettings::defaultIds(ShortcutBarSettings::Canvas));
directions->setCurrentIndex(directions->findData(4));
ring->setCurrentSlot(1);
list->setCurrentItem(listItem(list, QStringLiteral("diagrameditor.cut")));
page.findChild<QPushButton *>(QStringLiteral("placeButton"))->click();
page.applyConf();
}
QCOMPARE(GestureSettings::directions(), 4);
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Canvas));
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas).at(1),
QStringLiteral("diagrameditor.cut"));
//Contexts left alone still follow the bar
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Selection));
{
GesturesConfigPage page;
page.findChild<QPushButton *>(QStringLiteral("followBarButton"))->click();
page.applyConf();
}
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Canvas));
}
void dropOnASlot()
{
GesturesConfigPage page;
auto *list = page.findChild<QListWidget *>(QStringLiteral("availableList"));
auto *ring = page.findChild<GestureRingEditor *>(QStringLiteral("ringEditor"));
ring->resize(ring->sizeHint());
const QModelIndex index = list->model()->index(
list->row(listItem(list, QStringLiteral("diagrameditor.paste"))), 0);
QMimeData *mime = list->model()->mimeData({index});
//The right-hand slot of eight
const QPointF target = QRectF(ring->rect()).center() + QPointF(68, 0);
QCOMPARE(ring->slotAt(target.toPoint()), 2);
QDropEvent drop(target, Qt::CopyAction, mime, Qt::LeftButton, Qt::NoModifier);
//Straight to the widget: QApplication drops drag events when
//no real drag is running
QVERIFY(static_cast<QObject *>(ring)->event(&drop));
QVERIFY(drop.isAccepted());
QCOMPARE(ring->currentSlot(), 2);
QCOMPARE(ring->ids().value(2), QStringLiteral("diagrameditor.paste"));
//Moved, not copied: paste was already on the default canvas ring
QCOMPARE(ring->ids().count(QStringLiteral("diagrameditor.paste")), 1);
delete mime;
}
};
QTEST_MAIN(tst_gesturesettings)
#include "tst_gesturesettings.moc"
+205
View File
@@ -0,0 +1,205 @@
/*
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 <QDir>
#include <QFile>
#include <QImage>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QSize>
#include <QTemporaryDir>
/**
Undoing a crop restores the crop rectangle too, not only the pixels
shown: before, a project saved after Ctrl+Z still recorded the undone
crop, and the picture came back cropped once reopened. Runs the real
binary on a script: add a picture, crop it, undo, save; redo, save.
It also restores whether the pivot was placed by hand: a crop moves
the pivot to the centre of the kept region and marks it as default,
and before, Ctrl+Z brought the hand-placed pivot back without its
mark, so the next save dropped it.
*/
class tst_imagecropundo : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
QJsonObject run(const QString &script, const QString &project)
{
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write(script.toUtf8());
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--run"), path, project});
if (!proc.waitForFinished(120000)) return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 0)
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
}
return {};
}
static QByteArray read(const QString &path)
{
QFile f(path);
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
}
// The size of the picture as shown, from the <image> element's own
// text (its first text node: a cropped picture carries <image_base>
// as a child too).
static QSize shownSize(const QByteArray &xml)
{
const QRegularExpression image(QStringLiteral("<image\\b[^>]*>\\s*([A-Za-z0-9+/=]+)"));
const QRegularExpressionMatch m = image.match(QString::fromUtf8(xml));
if (!m.hasMatch()) return {};
return QImage::fromData(QByteArray::fromBase64(m.captured(1).toLatin1())).size();
}
// qet.imageCrop()'s {x, y, width, height}, as logged by the script.
static QRect rect(const QJsonValue &crop)
{
const QJsonObject o = crop.toObject();
return QRect(o.value("x").toInt(-1), o.value("y").toInt(-1),
o.value("width").toInt(-1), o.value("height").toInt(-1));
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
QImage img(40, 30, QImage::Format_RGB32);
img.fill(Qt::darkGreen);
QVERIFY(img.save(m_dir.filePath(QStringLiteral("pic.png"))));
}
void undoneCropIsNotSaved()
{
const QString undone = m_dir.filePath(QStringLiteral("undone.qet"));
const QString redone = m_dir.filePath(QStringLiteral("redone.qet"));
const QString script = QStringLiteral(R"JS(
var i = qet.addImage(0, '%1', 100, 100);
var r = {full: qet.imageCrop(0, i)};
r.cropped_ok = qet.cropImage(0, i, 10, 5, 20, 10);
r.cropped = qet.imageCrop(0, i);
r.same_ok = qet.cropImage(0, i, 10, 5, 20, 10);
r.empty_ok = qet.cropImage(0, i, 10, 5, 0, 10);
r.outside_ok = qet.cropImage(0, i, 100, 100, 20, 10);
qet.undo();
r.undone = qet.imageCrop(0, i);
qet.save('%2');
qet.redo();
r.redone = qet.imageCrop(0, i);
qet.save('%3');
qet.log('PROBE ' + JSON.stringify(r));
)JS").arg(m_dir.filePath(QStringLiteral("pic.png")), undone, redone);
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/741.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QCOMPARE(rect(r.value("full")), QRect(0, 0, 40, 30));
QVERIFY(r.value("cropped_ok").toBool());
QCOMPARE(rect(r.value("cropped")), QRect(10, 5, 20, 10));
// Crops that change nothing report it, and push no undo step:
// the undo below still undoes the real crop.
QCOMPARE(r.value("same_ok").toBool(true), false);
QCOMPARE(r.value("empty_ok").toBool(true), false);
QCOMPARE(r.value("outside_ok").toBool(true), false);
QCOMPARE(rect(r.value("undone")), QRect(0, 0, 40, 30));
QCOMPARE(rect(r.value("redone")), QRect(10, 5, 20, 10));
const QByteArray undone_xml = read(undone);
QVERIFY(undone_xml.contains("<image "));
QVERIFY2(!undone_xml.contains("<crop "), "an undone crop was saved");
// The picture shown follows: whole after undo, the kept region after redo.
QCOMPARE(shownSize(undone_xml), QSize(40, 30));
QCOMPARE(shownSize(read(redone)), QSize(20, 10));
const QRegularExpressionMatch crop =
QRegularExpression(QStringLiteral("<crop ([^>]*)/>")).match(QString::fromUtf8(read(redone)));
QVERIFY2(crop.hasMatch(), "the redone crop was not saved");
for (const char *attribute : {R"(x="10")", R"(y="5")", R"(w="20")", R"(h="10")"})
QVERIFY2(crop.captured(1).contains(QLatin1String(attribute)), attribute);
}
void undoneCropKeepsHandPlacedPivot()
{
// A picture whose pivot was placed by hand: saved as <transform>
// with pivotX/pivotY, which no script call writes, so the test
// adds it to a saved project itself.
const QString plain = m_dir.filePath(QStringLiteral("plain.qet"));
const QString pivot = m_dir.filePath(QStringLiteral("pivot.qet"));
QJsonObject r = run(QStringLiteral(R"JS(
var i = qet.addImage(0, '%1', 100, 100);
qet.save('%2');
qet.log('PROBE ' + JSON.stringify({added: i >= 0}));
)JS").arg(m_dir.filePath(QStringLiteral("pic.png")), plain),
QStringLiteral(QET_EXAMPLES_DIR "/741.qet"));
QVERIFY(r.value("added").toBool());
QByteArray xml = read(plain);
QCOMPARE(xml.count("</image>"), 1);
xml.replace("</image>", "<transform rotation=\"0\" skewX=\"0\" skewY=\"0\" "
"scaleX=\"1\" scaleY=\"1\" pivotX=\"5\" pivotY=\"7\"/></image>");
QFile f(pivot);
QVERIFY(f.open(QIODevice::WriteOnly));
f.write(xml);
f.close();
const QString before = m_dir.filePath(QStringLiteral("before.qet"));
const QString undone = m_dir.filePath(QStringLiteral("pivot-undone.qet"));
r = run(QStringLiteral(R"JS(
var r = {};
qet.save('%1');
r.cropped_ok = qet.cropImage(0, 0, 10, 5, 20, 10);
qet.undo();
qet.save('%2');
qet.log('PROBE ' + JSON.stringify(r));
)JS").arg(before, undone), pivot);
QVERIFY(r.value("cropped_ok").toBool());
// The fixture is read as intended: saved untouched, it keeps the pivot.
const QByteArray saved = read(before);
QVERIFY(saved.contains("pivotX=\"5\"") && saved.contains("pivotY=\"7\""));
const QByteArray afterUndo = read(undone);
QVERIFY2(afterUndo.contains("pivotX=\"5\"") && afterUndo.contains("pivotY=\"7\""),
"the hand-placed pivot was dropped after undoing the crop");
}
};
QTEST_GUILESS_MAIN(tst_imagecropundo)
#include "tst_imagecropundo.moc"
+196
View File
@@ -0,0 +1,196 @@
/*
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 <QBuffer>
#include <QDir>
#include <QFile>
#include <QImage>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTemporaryDir>
/**
Before #1310, a project saved after a crop was undone kept the undone
<crop>, with the uncropped picture shown. Loading such a file has to
drop that crop: the picture shown is always exactly the crop's size,
so a picture of another size means the crop is stale. Otherwise the
next edit of the picture applies the crop again.
Runs the real binary: a script adds and crops a picture and saves;
the test then puts another picture where the shown one is, and lets
the binary load and save that file again.
*/
class tst_imagestalecrop : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
// Runs the binary on @p project with a script that saves it to @p out.
bool run(const QString &script, const QString &project)
{
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return false;
f.write(script.toUtf8());
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--run"), path, project});
return proc.waitForFinished(120000) && proc.exitStatus() == QProcess::NormalExit;
}
// Opens @p in and saves it to @p out.
bool resave(const QString &in, const QString &out)
{
return run(QStringLiteral("qet.save('%1');\n").arg(out), in) && QFile::exists(out);
}
static QByteArray read(const QString &path)
{
QFile f(path);
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
}
static QByteArray png(const QSize &size)
{
QImage img(size, QImage::Format_RGB32);
img.fill(Qt::darkGreen);
QByteArray bytes;
QBuffer buffer(&bytes);
buffer.open(QIODevice::WriteOnly);
img.save(&buffer, "PNG");
return bytes.toBase64();
}
// The size of the saved original (<image_base>), or an empty size.
static QSize baseSize(const QByteArray &xml)
{
const QRegularExpression base(QStringLiteral("<image_base>\\s*([A-Za-z0-9+/=]+)"));
const QRegularExpressionMatch m = base.match(QString::fromLatin1(xml));
if (!m.hasMatch()) return {};
return QImage::fromData(QByteArray::fromBase64(m.captured(1).toLatin1())).size();
}
// @p xml with the shown picture (the <image> element's first text)
// replaced by a PNG of @p size.
static QByteArray withShown(QByteArray xml, const QSize &size)
{
const QRegularExpression shown(QStringLiteral("(<image\\b[^>]*>\\s*)([A-Za-z0-9+/=]+)"));
const QRegularExpressionMatch m = shown.match(QString::fromLatin1(xml));
if (!m.hasMatch()) return {};
return xml.replace(m.capturedStart(2), m.capturedLength(2), png(size));
}
QString m_cropped; // a picture of 40 x 30, cropped to 10,5,20,10
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
QImage img(40, 30, QImage::Format_RGB32);
img.fill(Qt::darkGreen);
const QString pic = m_dir.filePath(QStringLiteral("pic.png"));
QVERIFY(img.save(pic));
m_cropped = m_dir.filePath(QStringLiteral("cropped.qet"));
QVERIFY(run(QStringLiteral(R"JS(
var i = qet.addImage(0, '%1', 100, 100);
qet.cropImage(0, i, 10, 5, 20, 10);
qet.save('%2');
)JS").arg(pic, m_cropped), QStringLiteral(QET_EXAMPLES_DIR "/741.qet")));
const QByteArray xml = read(m_cropped);
QCOMPARE(xml.count("<image "), 1);
QVERIFY(xml.contains("<crop "));
QVERIFY(xml.contains("<image_base>"));
}
// A valid crop is kept, byte for byte.
void validCropIsKept()
{
const QString out = m_dir.filePath(QStringLiteral("valid-out.qet"));
QVERIFY(resave(m_cropped, out));
const QByteArray xml = read(out);
// Attributes in any order: Qt before 6.5 writes them unordered.
const QRegularExpression crop(QStringLiteral("<crop (?=[^>]*\\bx=\"10\")(?=[^>]*\\by=\"5\")"
"(?=[^>]*\\bw=\"20\")(?=[^>]*\\bh=\"10\")"));
QVERIFY2(crop.match(QString::fromLatin1(xml)).hasMatch(), "the valid crop was lost");
QVERIFY(xml.contains("<image_base>"));
}
// The shown picture is the whole original (40 x 30): the crop was
// undone before the file was saved. It is dropped, and with nothing
// else to remember the original is not saved twice any more.
void staleCropOfWholeOriginalIsDropped()
{
const QString stale = m_dir.filePath(QStringLiteral("stale.qet"));
const QByteArray xml = withShown(read(m_cropped), QSize(40, 30));
QVERIFY(!xml.isEmpty());
QFile f(stale);
QVERIFY(f.open(QIODevice::WriteOnly));
f.write(xml);
f.close();
const QString out = m_dir.filePath(QStringLiteral("stale-out.qet"));
QVERIFY(resave(stale, out));
const QByteArray saved = read(out);
QCOMPARE(saved.count("<image "), 1);
QVERIFY2(!saved.contains("<crop "), "the stale crop was kept");
QVERIFY(!saved.contains("<image_base>"));
}
// The shown picture matches neither the crop nor the original: it is
// what the user saw, so it becomes the original -- a crop made after
// loading cuts it, not the old original.
void shownPictureOfOtherSizeBecomesTheOriginal()
{
const QString odd = m_dir.filePath(QStringLiteral("odd.qet"));
const QByteArray xml = withShown(read(m_cropped), QSize(25, 25));
QVERIFY(!xml.isEmpty());
QFile f(odd);
QVERIFY(f.open(QIODevice::WriteOnly));
f.write(xml);
f.close();
const QString out = m_dir.filePath(QStringLiteral("odd-out.qet"));
QVERIFY(resave(odd, out));
const QByteArray saved = read(out);
QVERIFY2(!saved.contains("<crop "), "the stale crop was kept");
QVERIFY(!saved.contains("<image_base>"));
const QString recropped = m_dir.filePath(QStringLiteral("odd-recropped.qet"));
QVERIFY(run(QStringLiteral("qet.cropImage(0, 0, 0, 0, 10, 10);\nqet.save('%1');\n")
.arg(recropped), odd));
QCOMPARE(baseSize(read(recropped)), QSize(25, 25));
}
};
QTEST_MAIN(tst_imagestalecrop)
#include "tst_imagestalecrop.moc"
+90
View File
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTemporaryDir>
// Placing the same symbol twice in lmdg.qet: its embedded collection has a
// "k_elem" category before "import". Imported symbols were filed under the
// first category while every lookup is under "import/", so the second
// placement could not find the first copy, tried to import it again, and
// failed -- in a script (qet.addElement() returned "") and in the editor,
// where the drop silently placed nothing.
class tst_importcategory : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
// Run @p script on @p project in a sandbox of its own and return the
// JSON object it logged.
QJsonObject run(const QString &script, const QString &project)
{
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
const QString home = m_dir.filePath(QStringLiteral("home"));
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write(script.toUtf8());
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
QProcess proc;
proc.setProcessEnvironment(env);
//The shipped collection, so common:// paths resolve. Given
//on the command line: macOS does not read a settings file
//under XDG_CONFIG_HOME.
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--common-elements-dir=" QET_ELEMENTS_DIR "/"),
QStringLiteral("--run"), path, project});
if (!proc.waitForFinished(120000)) return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 0)
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
}
return {};
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void sameSymbolTwiceInLmdg()
{
const QString project = m_dir.filePath(QStringLiteral("lmdg.qet"));
QVERIFY(QFile::copy(QStringLiteral(QET_EXAMPLES_DIR "/lmdg.qet"), project));
const QJsonObject r = run(QStringLiteral(
"var p = 'common://10_electric/10_allpole/310_relays_contactors_contacts/"
"01_coils/bobine_ka_a_remanence.elmt';\n"
"var a = qet.addElement(0, p, 100, 100);\n"
"var b = qet.addElement(0, p, 300, 100);\n"
"qet.log('PROBE ' + JSON.stringify({first: a, second: b}));\n"),
project);
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QVERIFY2(!r.value(QStringLiteral("first")).toString().isEmpty(),
"the first placement failed");
QVERIFY2(!r.value(QStringLiteral("second")).toString().isEmpty(),
"the second placement of the same symbol failed");
}
};
QTEST_GUILESS_MAIN(tst_importcategory)
#include "tst_importcategory.moc"
+81
View File
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QSet>
#include <QTemporaryDir>
// fixtures/junction_dot_shared_potential.qet: four contacts K1..K4 in a row,
// wired K1-K2, K1-K4 and K2-K3. All three wires run along one line above the
// contacts, so there is a T above K2 and another above K3. The corner above
// K3 belongs to K2-K3 and lies on K1-K4, which shares no terminal with it:
// that dot used to be missing (issue #1280).
class tst_junctiondots : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
// Export the fixture to SVG and return the distinct positions of the
// junction dots drawn on it.
QSet<QString> dots()
{
const QString home = m_dir.filePath(QStringLiteral("home"));
const QString out = m_dir.filePath(QStringLiteral("svg"));
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"));
env.insert(QStringLiteral("TMPDIR"), home);
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--export-svg"),
QFINDTESTDATA("fixtures/junction_dot_shared_potential.qet"), out});
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
return {};
const QStringList svgs = QDir(out).entryList({QStringLiteral("*.svg")});
if (svgs.size() != 1)
return {};
QFile f(QDir(out).filePath(svgs.first()));
if (!f.open(QIODevice::ReadOnly))
return {};
const QString svg = QString::fromUtf8(f.readAll());
// A junction dot on a wire of the default width is a circle
// 3.0 across; the symbols in the fixture draw none that size.
static const QRegularExpression re(
QStringLiteral("<circle cx=\"([^\"]*)\" cy=\"([^\"]*)\" r=\"1.5\"/>"));
QSet<QString> found;
auto it = re.globalMatch(svg);
while (it.hasNext()) {
const auto m = it.next();
found << m.captured(1) + QLatin1Char(',') + m.captured(2);
}
return found;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void dotAtEveryT()
{
const QSet<QString> expected{QStringLiteral("200,260"),
QStringLiteral("300,260")};
QCOMPARE(dots(), expected);
}
};
QTEST_GUILESS_MAIN(tst_junctiondots)
#include "tst_junctiondots.moc"
@@ -0,0 +1,126 @@
/*
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 <QDomDocument>
#include <QProcess>
#include <QTemporaryDir>
/**
Projects saved before the <sequentialNumbers> element keep an element's
or conductor's sequential numbers as attributes: sequ_1, sequf_1, seqt_1,
seqtf_1, seqh_1 and seqhf_1. The loaders look for one of them to take
the old route; the list named sequf_1 twice and seqhf_1 never, so a
file whose only sequence was the hundred-folio one lost it. Runs the
real binary's --resave on fixtures/legacy_sequential_attributes.qet
and reads the <sequentialNumbers> it writes.
*/
class tst_legacysequentialattributes : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
QDomDocument m_resaved;
static bool runQet(const QStringList &arguments)
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
return proc.waitForFinished(60000)
&& proc.exitStatus() == QProcess::NormalExit
&& proc.exitCode() == 0;
}
/// The first @p tag element of the saved project with this uuid.
QDomElement item(const QString &tag, const QString &uuid) const
{
const QDomNodeList list = m_resaved.elementsByTagName(tag);
for (int i = 0; i < list.size(); ++i) {
const QDomElement e = list.at(i).toElement();
if (e.attribute(QStringLiteral("uuid")) == uuid) return e;
}
return QDomElement();
}
/// The text of <sequentialNumbers><@p part> under @p e.
static QString sequence(const QDomElement &e, const QString &part)
{
return e.firstChildElement(QStringLiteral("sequentialNumbers"))
.firstChildElement(part).text();
}
private slots:
void initTestCase()
{
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
QVERIFY(m_dir.isValid());
const QString fixture = QFINDTESTDATA("fixtures/legacy_sequential_attributes.qet");
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
const QString out = m_dir.filePath(QStringLiteral("resaved.qet"));
QVERIFY2(runQet({QStringLiteral("--resave"), fixture, out}), "--resave failed");
QFile file(out);
QVERIFY(file.open(QIODevice::ReadOnly));
QVERIFY(m_resaved.setContent(&file));
}
// The element and the conductor whose only old attribute is seqhf_1.
void hundredFolioSequenceIsKept_data()
{
QTest::addColumn<QString>("tag");
QTest::addColumn<QString>("uuid");
QTest::addColumn<QString>("value");
QTest::newRow("element") << "element" << "{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" << "5";
QTest::newRow("conductor") << "conductor" << "{f5162c59-0c59-4e94-b6a6-4cf259a52d91}" << "7";
}
void hundredFolioSequenceIsKept()
{
QFETCH(QString, tag);
QFETCH(QString, uuid);
QFETCH(QString, value);
const QDomElement e = item(tag, uuid);
QVERIFY2(!e.isNull(), "item not found in the resaved project");
QVERIFY2(!e.hasAttribute(QStringLiteral("seqhf_1")), "the old attribute is not written back");
QCOMPARE(sequence(e, QStringLiteral("hundredFolio")), value);
}
// The unit sequence, whose attribute was already in the list, as before.
void unitSequenceIsKept_data()
{
QTest::addColumn<QString>("tag");
QTest::addColumn<QString>("uuid");
QTest::addColumn<QString>("value");
QTest::newRow("element") << "element" << "{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" << "3";
QTest::newRow("conductor") << "conductor" << "{89cacce1-b12a-4ec3-aadf-d3f8feb7494d}" << "4";
}
void unitSequenceIsKept()
{
QFETCH(QString, tag);
QFETCH(QString, uuid);
QFETCH(QString, value);
const QDomElement e = item(tag, uuid);
QVERIFY2(!e.isNull(), "item not found in the resaved project");
QCOMPARE(sequence(e, QStringLiteral("unit")), value);
}
};
QTEST_MAIN(tst_legacysequentialattributes)
#include "tst_legacysequentialattributes.moc"
+80
View File
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
--export-pdf writes a project's cross-reference links in the same order
every time. They used to come out in the order of Element pointers, so
the same project gave a different PDF in each run. Exported in separate
processes, since pointers only change between runs.
*/
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QRegularExpression>
#include <QTemporaryDir>
#include <QtTest>
class tst_pdflinkorder : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
// The link annotations' rectangles and destinations, in file order.
QStringList exportLinks(const QString &project, int run)
{
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(run));
const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(run));
const QString out = m_dir.filePath(QStringLiteral("out%1.pdf").arg(run));
QDir().mkpath(home);
QDir().mkpath(tmp);
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("/.local/share"));
env.insert(QStringLiteral("TMPDIR"), tmp);
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--export-pdf"), project, out});
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
return {};
QFile file(out);
if (!file.open(QIODevice::ReadOnly))
return {};
const QString pdf = QString::fromLatin1(file.readAll());
static const QRegularExpression link(QStringLiteral(
R"(/Subtype /Link\s*/Rect \[([^\]]*)\][\s\S]*?/D \[\d+ 0 R ([^\]]*)\])"));
QStringList links;
auto it = link.globalMatch(pdf);
while (it.hasNext()) {
const auto m = it.next();
links << m.captured(1).simplified() + QStringLiteral(" -> ")
+ m.captured(2).simplified();
}
return links;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void sameOrderEveryRun()
{
const QString project =
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/industrial.qet");
QVERIFY2(QFile::exists(project), "examples/industrial.qet not found");
const QStringList first = exportLinks(project, 0);
QVERIFY2(first.size() >= 2, "expected several cross-reference links");
for (int run = 1; run < 5; ++run)
QCOMPARE(exportLinks(project, run), first);
}
};
QTEST_MAIN(tst_pdflinkorder)
#include "tst_pdflinkorder.moc"
+84
View File
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDir>
#include <QFile>
#include <QPageSize>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTemporaryDir>
// --export-pdf sizes each page to its folio, and QPageSize rounds a size
// within 3 pt of a standard sheet to the sheet. It knows the sheets upright
// only, so a wide folio used to miss: the fixture's A3 landscape folio
// (frame 1190.25 x 840.75 pt) came out 1190 x 841 pt while its A3 portrait
// folio came out 842 x 1191, the sheet. Both must land on A3.
class tst_pdfpagesize : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
// The MediaBox of each page, in page order. Qt writes one uncompressed
// "/MediaBox [0 0 w h]" per page object.
QList<QSizeF> exportPages(const QString &project)
{
const QString home = m_dir.filePath(QStringLiteral("home"));
const QString tmp = m_dir.filePath(QStringLiteral("tmp"));
const QString out = m_dir.filePath(QStringLiteral("out.pdf"));
QDir().mkpath(home);
QDir().mkpath(tmp);
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("/.local/share"));
env.insert(QStringLiteral("TMPDIR"), tmp);
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--export-pdf"), project, out});
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
return {};
QFile file(out);
if (!file.open(QIODevice::ReadOnly))
return {};
const QString pdf = QString::fromLatin1(file.readAll());
static const QRegularExpression box(
QStringLiteral(R"(/MediaBox \[0 0 ([0-9.]+) ([0-9.]+)\])"));
QList<QSizeF> pages;
auto it = box.globalMatch(pdf);
while (it.hasNext()) {
const auto m = it.next();
pages << QSizeF(m.captured(1).toDouble(), m.captured(2).toDouble());
}
return pages;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void a3BothWays()
{
const QString fixture = QFINDTESTDATA("fixtures/pdf_page_a3.qet");
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
const QList<QSizeF> pages = exportPages(fixture);
QCOMPARE(pages.size(), 2);
// Qt writes the sheet's whole points: 842 x 1191 for A3.
const QSize a3 = QPageSize(QPageSize::A3).sizePoints();
QCOMPARE(pages.at(0), QSizeF(a3.height(), a3.width())); // landscape
QCOMPARE(pages.at(1), QSizeF(a3)); // portrait
}
};
QTEST_MAIN(tst_pdfpagesize)
#include "tst_pdfpagesize.moc"
+183
View File
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
With SOURCE_DATE_EPOCH set, --export-pdf writes the same bytes for the
same project in every run: the dates are the ones it names, the document
id comes from the content of the PDF, and the fonts come in a fixed order.
Without it the dates are the time of the export, as before. Exported in
separate processes, since what used to differ changed between runs.
*/
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QRegularExpression>
#include <QTemporaryDir>
#include <QtTest>
#include <QUuid>
class tst_pdfreproducible : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
int m_run = 0;
QByteArray exportPdf(const QString &project, const QByteArray &epoch,
const QString &folder = QString())
{
const int run = m_run++;
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(run));
const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(run));
const QString out = folder.isEmpty()
? m_dir.filePath(QStringLiteral("out%1.pdf").arg(run))
: m_dir.filePath(folder + QStringLiteral("/same.pdf"));
if (!folder.isEmpty())
QDir().mkpath(m_dir.filePath(folder));
QDir().mkpath(home);
QDir().mkpath(tmp);
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("/.local/share"));
env.insert(QStringLiteral("TMPDIR"), tmp);
env.remove(QStringLiteral("QT_HASH_SEED"));
env.remove(QStringLiteral("SOURCE_DATE_EPOCH"));
if (!epoch.isEmpty())
env.insert(QStringLiteral("SOURCE_DATE_EPOCH"), QString::fromLatin1(epoch));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--export-pdf"), project, out});
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
return {};
QFile file(out);
if (!file.open(QIODevice::ReadOnly))
return {};
return file.readAll();
}
/// Every object the xref table lists starts where the table says: the
/// dates are rewritten after Qt writes the file, which moves them.
static bool xrefMatches(const QByteArray &pdf)
{
const int sx = pdf.lastIndexOf("startxref");
if (sx == -1)
return false;
const int xref = pdf.mid(sx + 9).trimmed().split('\n').value(0).toInt();
if (!pdf.mid(xref).startsWith("xref"))
return false;
const QList<QByteArray> lines = pdf.mid(xref).split('\n');
const QList<QByteArray> header = lines.value(1).split(' ');
const int first = header.value(0).toInt();
const int count = header.value(1).toInt();
for (int i = 0; i < count; ++i) {
const QList<QByteArray> entry = lines.value(2 + i).split(' ');
if (entry.value(2) != "n")
continue;
const QByteArray obj = QByteArray::number(first + i) + " 0 obj";
if (pdf.mid(entry.value(0).toInt(), obj.size()) != obj)
return false;
}
return count > 0;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void sameBytesEveryRun()
{
// A project with no cross-reference links: their order is fixed
// separately.
const QString project =
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/741.qet");
QVERIFY2(QFile::exists(project), "examples/741.qet not found");
const QByteArray first = exportPdf(project, "1700000000");
QVERIFY(!first.isEmpty());
QVERIFY(first.contains("/CreationDate (D:20231114221320Z)"));
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
QVERIFY(first.contains("xmp:CreateDate=\"2023-11-14T22:13:20Z\""));
#endif
QVERIFY2(xrefMatches(first), "the xref table does not match the file");
for (int i = 0; i < 3; ++i)
QVERIFY2(exportPdf(project, "1700000000") == first,
"two exports of the same project differ");
}
void sameBytesWithNewUuids()
{
// #1178: a project generated again from the same data has the same
// drawing but new uuids, and its PDF used to differ in the
// document id, which came from the project file.
const QString project =
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/741.qet");
QFile in(project);
QVERIFY(in.open(QIODevice::ReadOnly));
QString xml = QString::fromUtf8(in.readAll());
static const QRegularExpression uuid(QStringLiteral(
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"));
QHash<QString, QString> renamed;
QString regenerated;
qsizetype last = 0;
for (auto it = uuid.globalMatch(xml); it.hasNext(); ) {
const auto match = it.next();
const QString key = match.captured().toLower();
if (!renamed.contains(key))
renamed.insert(key, QUuid::createUuid().toString(QUuid::WithoutBraces));
regenerated += xml.mid(last, match.capturedStart() - last) + renamed.value(key);
last = match.capturedEnd();
}
regenerated += xml.mid(last);
QVERIFY(renamed.size() > 10);
const QString copy = m_dir.filePath(QStringLiteral("regenerated.qet"));
QFile out(copy);
QVERIFY(out.open(QIODevice::WriteOnly));
out.write(regenerated.toUtf8());
out.close();
const QByteArray first = exportPdf(project, "1700000000");
QVERIFY(!first.isEmpty());
QVERIFY2(!first.contains("6f1c2d4e-9a3b-4c5d-8e7f-0a1b2c3d4e5f"),
"the placeholder document id was left in the file");
QVERIFY2(xrefMatches(first), "the xref table does not match the file");
QVERIFY2(exportPdf(copy, "1700000000") == first,
"the same drawing with new uuids gives a different PDF");
}
void sameBytesInAnyFolder()
{
// #1178: the links carry the output path, and rewriting them left
// the old startxref in the file, with the size the file had before.
const QString project =
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/iso_sfc_example.qet");
QVERIFY2(QFile::exists(project), "examples/iso_sfc_example.qet not found");
const QByteArray first = exportPdf(project, "1700000000", QStringLiteral("p"));
QVERIFY(!first.isEmpty());
QVERIFY(first.contains("/GoTo"));
QCOMPARE(first.count("startxref"), 1);
QVERIFY2(xrefMatches(first), "the xref table does not match the file");
QVERIFY2(exportPdf(project, "1700000000", QStringLiteral("longer-folder-name")) == first,
"the same project gives a different PDF in another folder");
}
void nowWithoutTheVariable()
{
const QString project =
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/741.qet");
const QByteArray pdf = exportPdf(project, QByteArray());
QVERIFY(!pdf.isEmpty());
const QByteArray year =
"/CreationDate (D:" + QByteArray::number(QDate::currentDate().year());
QVERIFY(pdf.contains(year));
}
};
QTEST_MAIN(tst_pdfreproducible)
#include "tst_pdfreproducible.moc"
+155
View File
@@ -0,0 +1,155 @@
/*
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 "positionorder.h"
#include <QtTest>
#include <algorithm>
#include <functional>
/**
PositionOrder -- the position comparisons behind renumbering elements
(comparPos) and numbering terminals. std::sort needs a strict weak
ordering; the old comparisons used "<=" and a 1 px tolerance, which
are not one, so two elements at the same position (or three terminals
a fraction apart) were undefined behaviour.
*/
class tst_positionorder : public QObject
{
Q_OBJECT
using Less = std::function<bool(const QPointF &, const QPointF &)>;
static QList<QPair<QString, Less>> orders()
{
return {{QStringLiteral("xThenY"), PositionOrder::xThenY},
{QStringLiteral("yThenX"), PositionOrder::yThenX},
{QStringLiteral("roundedXThenY"), PositionOrder::roundedXThenY},
{QStringLiteral("roundedYThenX"), PositionOrder::roundedYThenX}};
}
private slots:
// Nothing is before itself, and equal points are before each other
// in neither direction: what "<=" broke.
void equalPointsAreNotOrdered()
{
const QPointF p(100, 200), q(100, 200);
for (const auto &order : orders()) {
QVERIFY2(!order.second(p, p), qPrintable(order.first));
QVERIFY2(!order.second(p, q), qPrintable(order.first));
QVERIFY2(!order.second(q, p), qPrintable(order.first));
}
}
void leftToRightThenTopToBottom()
{
QVERIFY(PositionOrder::xThenY(QPointF(10, 500), QPointF(20, 0)));
QVERIFY(!PositionOrder::xThenY(QPointF(20, 0), QPointF(10, 500)));
QVERIFY(PositionOrder::xThenY(QPointF(10, 0), QPointF(10, 5)));
QVERIFY(!PositionOrder::xThenY(QPointF(10, 5), QPointF(10, 0)));
}
void topToBottomThenLeftToRight()
{
QVERIFY(PositionOrder::yThenX(QPointF(500, 10), QPointF(0, 20)));
QVERIFY(!PositionOrder::yThenX(QPointF(0, 20), QPointF(500, 10)));
QVERIFY(PositionOrder::yThenX(QPointF(0, 10), QPointF(5, 10)));
QVERIFY(!PositionOrder::yThenX(QPointF(5, 10), QPointF(0, 10)));
}
// Items a fraction of a pixel apart (placed with Ctrl, or after a
// rotation) count as aligned, and are then ordered on the other axis.
void roundedTreatsFractionsAsAligned()
{
QVERIFY(PositionOrder::roundedXThenY(QPointF(10.2, 0), QPointF(9.8, 5)));
QVERIFY(!PositionOrder::roundedXThenY(QPointF(9.8, 5), QPointF(10.2, 0)));
QVERIFY(PositionOrder::roundedYThenX(QPointF(0, 10.2), QPointF(5, 9.8)));
QVERIFY(!PositionOrder::roundedYThenX(QPointF(5, 9.8), QPointF(0, 10.2)));
}
// The three terminals that broke the tolerance comparison: with
// "within 1 px counts as aligned", a < b (by y), b < c (by y) and
// c < a (by x), a cycle. Rounded, they are simply ordered by x.
void roundedIsTransitiveWhereToleranceWasNot()
{
const QPointF a(2, 0), b(1.1, 1), c(0.2, 2);
QVERIFY(PositionOrder::roundedXThenY(c, b));
QVERIFY(PositionOrder::roundedXThenY(b, a));
QVERIFY(PositionOrder::roundedXThenY(c, a));
QVERIFY(!PositionOrder::roundedXThenY(a, c));
}
// Every order is a strict weak ordering on a grid of awkward points:
// irreflexive, asymmetric, transitive, and with transitive
// equivalence. Checked by brute force on the whole set.
void strictWeakOrdering_data()
{
QTest::addColumn<QString>("name");
for (const auto &order : orders())
QTest::newRow(qPrintable(order.first)) << order.first;
}
void strictWeakOrdering()
{
QFETCH(QString, name);
Less less;
for (const auto &order : orders())
if (order.first == name) less = order.second;
QVERIFY(less);
QList<QPointF> points;
const QList<qreal> values{-1, -0.6, -0.4, 0, 0.2, 0.5, 0.9, 1, 1.1, 2, 10.5, 61.3, 61.7};
for (qreal x : values)
for (qreal y : values)
points << QPointF(x, y);
points << points.first() << QPointF(2, 0) << QPointF(1.1, 1) << QPointF(0.2, 2);
auto equiv = [&](const QPointF &p, const QPointF &q) { return !less(p, q) && !less(q, p); };
for (const QPointF &p : points) {
QVERIFY(!less(p, p));
for (const QPointF &q : points) {
if (less(p, q)) QVERIFY(!less(q, p));
for (const QPointF &r : points) {
if (less(p, q) && less(q, r)) QVERIFY2(less(p, r), "transitive");
if (equiv(p, q) && equiv(q, r)) QVERIFY2(equiv(p, r), "equivalence transitive");
}
}
}
// And std::sort on it ends sorted, with the list intact.
QList<QPointF> sorted = points;
std::sort(sorted.begin(), sorted.end(), less);
QCOMPARE(sorted.size(), points.size());
for (int i = 1; i < sorted.size(); ++i)
QVERIFY(!less(sorted.at(i), sorted.at(i - 1)));
}
// Twenty items at one position, as pasting at the origin leaves
// them: a list std::sort could scan past the end of with "<=".
void manyEqualPositionsSort()
{
QList<QPointF> points(20, QPointF(0, 0));
points << QPointF(-10, 0) << QPointF(10, 0);
std::sort(points.begin(), points.end(), PositionOrder::xThenY);
QCOMPARE(points.size(), 22);
QCOMPARE(points.first(), QPointF(-10, 0));
QCOMPARE(points.last(), QPointF(10, 0));
}
};
QTEST_GUILESS_MAIN(tst_positionorder)
#include "tst_positionorder.moc"
+81
View File
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTemporaryDir>
// With "one text per potential" on, the potential's number is drawn on its
// longest conductor. fixtures/one_text_per_potential_tie.qet has two
// conductors of the same, greatest length in one potential: which of them
// carried the number used to follow pointer order, so the same file
// exported twice could put it on either. Runs the real binary, in separate
// processes, since that order only changes from one run to the next.
class tst_potentialtextcarrier : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
// Export the fixture to SVG and return where the number is drawn: the
// transform of the group holding it. Empty if it is not drawn exactly
// once.
QString carrier(int run)
{
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(run));
const QString out = m_dir.filePath(QStringLiteral("svg%1").arg(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"));
env.insert(QStringLiteral("TMPDIR"), home);
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--export-svg"),
QFINDTESTDATA("fixtures/one_text_per_potential_tie.qet"), out});
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
return {};
const QStringList svgs = QDir(out).entryList({QStringLiteral("*.svg")});
if (svgs.size() != 1)
return {};
QFile f(QDir(out).filePath(svgs.first()));
if (!f.open(QIODevice::ReadOnly))
return {};
const QString svg = QString::fromUtf8(f.readAll());
static const QRegularExpression re(
QStringLiteral("transform=\"([^\"]*)\"[^<]*>\\s*<text[^>]*>42</text>"));
QStringList found;
auto it = re.globalMatch(svg);
while (it.hasNext())
found << it.next().captured(1);
return found.size() == 1 ? found.first() : QString();
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
// Before the fix about half the runs differed from the first, so
// eight agreeing runs leave a 1 in 128 chance of a false pass.
void sameCarrierEveryRun()
{
const QString first = carrier(0);
QVERIFY2(!first.isEmpty(), "the number is not drawn exactly once");
for (int run = 1; run < 8; ++run)
QCOMPARE(carrier(run), first);
}
};
QTEST_GUILESS_MAIN(tst_potentialtextcarrier)
#include "tst_potentialtextcarrier.moc"
+2 -2
View File
@@ -271,7 +271,7 @@ void tst_qeticons::lightIconsStayLightInDarkTheme()
}
/**
QET's own vector icons live in ico/scalable/, the Breeze ones in
QET's own vector icons live in ico/qet/scalable/, the Breeze ones in
ico/breeze/: one file for every size from the toolbar up. Line art
has a recolored copy in the dark theme; colored art has none and is
inherited. Each must resolve in both themes at 22, 24, 32 and 64 px,
@@ -415,7 +415,7 @@ void tst_qeticons::breezeIconsAreSharpAtEverySize()
}
/**
A traced icon (ico/traced/, misc/make_icon_themes.py) is the PNG copied
A traced icon (ico/qet/traced/, misc/make_icon_themes.py) is the PNG copied
pixel by pixel at 16 and 22 pixels, so those files and the 24 pixel
canvas must be drawn unscaled. At every size, at 1x and at 2x, Qt must
pick the folder's own file.
+40 -2
View File
@@ -17,8 +17,9 @@
// - information values were trimmed on save but not on load, so a label
// with stray spaces kept them in its displayed copy until the project
// was opened again (m_000.qet).
// Runs the real binary's --resave twice on every example, and on a
// project whose title block holds a value that is a single space (#973).
// Runs the real binary's --resave twice on every example, on a project
// whose title block holds a value that is a single space (#973), and on
// one that used to crash on opening (bugtracker #345).
class tst_resaveunchanged : public QObject
{
Q_OBJECT
@@ -83,10 +84,47 @@ private slots:
QVERIFY2(a == b, "the second save changed the file");
}
// A contact not linked to a coil, carrying a text built from %{label}:
// opening it dereferenced the missing coil and crashed (bugtracker #345).
// The fixture is a blank project with one such contact.
void unlinkedContactLabelTextOpens()
{
const QString fixture = QFINDTESTDATA("fixtures/unlinked_contact_label.qet");
QVERIFY(!fixture.isEmpty());
const QString first = resave(fixture);
QVERIFY2(!first.isEmpty(), "--resave failed: QElectroTech crashed opening the project");
const QString second = resave(first);
QVERIFY2(!second.isEmpty(), "second --resave failed");
QVERIFY2(read(first) == read(second), "the second save changed the file");
}
// %{machine_manufacturer_reference_auxiliary1..4} resolve like their
// neighbours: they were missing from AssignVariables::replaceVariable()
// and printed as literal text. The fixture has one terminal whose
// four texts are "[%{machine_..._auxiliaryN}|%{manufacturer_reference_auxiliaryN}]",
// saved by a build without the fix, so the literal text is in the file.
void auxiliaryMachineReferenceResolves()
{
const QString fixture = QFINDTESTDATA("fixtures/aux_machine_reference.qet");
QVERIFY(!fixture.isEmpty());
const QString saved = resave(fixture);
QVERIFY2(!saved.isEmpty(), "--resave failed");
const QString xml = QString::fromUtf8(read(saved));
for (int n = 1; n <= 4; ++n) {
const QString shown = QStringLiteral("<text>[MMR-AUX%1|MR-AUX%1]</text>").arg(n);
QVERIFY2(xml.contains(shown), qPrintable(shown + QStringLiteral(" not in the saved file")));
}
QVERIFY2(!xml.contains(QStringLiteral("<text>[%{machine")),
"a machine manufacturer reference variable was left unresolved");
}
// A title-block value that is a single space is kept through two saves
// (#973), and a value with accents comes back as it went in.
void singleSpaceValueKept()
{
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
QSKIP("QDomDocument::PreserveSpacingOnlyNodes needs Qt 6.5 (see QETProject::openFile)");
#endif
QByteArray xml = read(QStringLiteral(QET_EXAMPLES_DIR "/Projet_vierge.qet"));
QVERIFY(xml.contains("<properties>"));
xml.replace("<properties>",
+22
View File
@@ -97,6 +97,28 @@ private slots:
QVERIFY(r.value(QStringLiteral("junk")).toArray().isEmpty());
QVERIFY(r.value(QStringLiteral("badFolio")).toArray().isEmpty());
}
// qet.deleteElement() takes the wires on the element's terminals with
// it, as the Delete key does; they used to stay on the folio, attached
// to an element that was gone.
void deleteElementTakesItsWires()
{
const QJsonObject r = run(QStringLiteral(
"var p = 'embed://import/probe/v2_fuse.elmt';\n"
"var a = qet.addElement(0, p, 400, 400);\n"
"var b = qet.addElement(0, p, 470, 490);\n"
"qet.addConductor(0, a, 0, b, 0);\n"
"var before = qet.conductorUuids(0).length;\n"
"var deleted = qet.deleteElement(0, a);\n"
"var dangling = qet.conductorUuids(0).filter(function (u) {\n"
" return qet.conductorEnds(0, u).some(function (e) { return e.indexOf(a) === 0; }); });\n"
"qet.log('PROBE ' + JSON.stringify({deleted: deleted, before: before,\n"
" after: qet.conductorUuids(0).length, dangling: dangling.length}));\n"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QVERIFY(r.value(QStringLiteral("deleted")).toBool());
QCOMPARE(r.value(QStringLiteral("dangling")).toInt(), 0);
QCOMPARE(r.value(QStringLiteral("after")).toInt(), r.value(QStringLiteral("before")).toInt() - 1);
}
};
QTEST_APPLESS_MAIN(tst_scriptconductoruuid)
+131
View File
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTemporaryDir>
// qet.currentFolio() is the folio on screen in the editor. Through --run
// there is none, so it stands in with the first folio, or -1 when the
// project has no folio. --run also keeps one undo step per call: only a run
// started from the editor is grouped into one, and a headless script that
// calls qet.undo() itself must go on working.
class tst_scriptcurrentfolio : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
// Run @p script on @p project in a sandbox of its own and return the
// JSON object it logged.
QJsonObject run(const QString &script, const QString &project)
{
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write(script.toUtf8());
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--run"), path, project});
if (!proc.waitForFinished(120000)) return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 0)
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
}
return {};
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void firstFolioWithoutAView()
{
const QJsonObject r = run(QStringLiteral(
"qet.log('PROBE ' + JSON.stringify({current: qet.currentFolio(),\n"
" folios: qet.folioCount()}));\n"),
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QVERIFY(r.value(QStringLiteral("folios")).toInt() > 1);
QCOMPARE(r.value(QStringLiteral("current")).toInt(), 0);
}
void noFolioIsMinusOne()
{
const QString project = m_dir.filePath(QStringLiteral("empty.qet"));
QFile out(project);
QVERIFY(out.open(QIODevice::WriteOnly));
out.write("<project version=\"0.100.0\" title=\"empty\">\n</project>\n");
out.close();
const QJsonObject r = run(QStringLiteral(
"qet.log('PROBE ' + JSON.stringify({current: qet.currentFolio(),\n"
" folios: qet.folioCount()}));\n"), project);
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QCOMPARE(r.value(QStringLiteral("folios")).toInt(), 0);
QCOMPARE(r.value(QStringLiteral("current")).toInt(), -1);
}
// The list is read from the build itself: the calls a script uses
// are in it with their parameter names, each once.
void apiSignaturesListed()
{
const QJsonObject r = run(QStringLiteral(
"qet.log('PROBE ' + JSON.stringify({sigs: qet.apiSignatures()}));\n"),
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QStringList sigs;
for (const QJsonValue &v : r.value(QStringLiteral("sigs")).toArray())
sigs << v.toString();
QVERIFY(sigs.size() > 100);
QVERIFY(sigs.contains(QStringLiteral("int currentFolio()")));
QVERIFY(sigs.contains(QStringLiteral(
"int addText(int folioIndex, QString text, double x, double y)")));
QVERIFY(sigs.contains(QStringLiteral(
"bool exportPdf(QString output, bool showTerminals)")));
QCOMPARE(sigs.filter(QStringLiteral(" exportPdf(")).size(), 1);
QVERIFY(sigs.filter(QStringLiteral("setUndoGrouped")).isEmpty());
}
void headlessUndoStaysPerCall()
{
const QJsonObject r = run(QStringLiteral(
"var f = qet.currentFolio();\n"
"var before = qet.texts(f).length;\n"
"qet.addText(f, 'one', 40, 40);\n"
"qet.addText(f, 'two', 40, 80);\n"
"var undone = qet.undo();\n"
"qet.log('PROBE ' + JSON.stringify({undone: undone,\n"
" added: qet.texts(f).length - before}));\n"),
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QCOMPARE(r.value(QStringLiteral("undone")).toBool(), true);
QCOMPARE(r.value(QStringLiteral("added")).toInt(), 1);
}
};
QTEST_MAIN(tst_scriptcurrentfolio)
#include "tst_scriptcurrentfolio.moc"
+100
View File
@@ -0,0 +1,100 @@
/*
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 <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QTemporaryDir>
/**
The project database keeps the width of a free text (text_width in the
independent_text table, NULL for the automatic width), read here with
qet.query() from a script run on fixtures/free_text_width.qet.
*/
class tst_scriptfreetextwidth : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
/// The rows of the independent_text table, by the first word of the text.
QHash<QString, QJsonObject> rows()
{
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write("qet.log('PROBE ' + JSON.stringify(qet.query("
"'SELECT text, text_width, width, height FROM independent_text')));\n");
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
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),
{QStringLiteral("--run"), path, QFINDTESTDATA("fixtures/free_text_width.qet")});
if (!proc.waitForFinished(60000)) return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
QHash<QString, QJsonObject> result;
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i < 0) continue;
const QJsonArray array = QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).array();
for (const QJsonValue &row : array)
result.insert(row.toObject().value(QStringLiteral("text")).toString().section(QLatin1Char(' '), 0, 0),
row.toObject());
}
return result;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
QVERIFY(!QFINDTESTDATA("fixtures/free_text_width.qet").isEmpty());
}
void widthIsInTheDatabase()
{
const QHash<QString, QJsonObject> r = rows();
QCOMPARE(r.size(), 3);
QCOMPARE(r.value(QStringLiteral("FreeAlpha")).value(QStringLiteral("text_width")).toDouble(), 70.0);
QCOMPARE(r.value(QStringLiteral("CentAlpha")).value(QStringLiteral("text_width")).toDouble(), 120.0);
//The automatic width is NULL
const QJsonValue open = r.value(QStringLiteral("OpenAlpha")).value(QStringLiteral("text_width"));
QVERIFY2(open.isNull() || open.toString().isEmpty(), qPrintable(open.toVariant().toString()));
//The box of the row is the wrapped one: taller than one line
QVERIFY(r.value(QStringLiteral("FreeAlpha")).value(QStringLiteral("height")).toDouble()
> 2 * r.value(QStringLiteral("OpenAlpha")).value(QStringLiteral("height")).toDouble());
}
};
QTEST_APPLESS_MAIN(tst_scriptfreetextwidth)
#include "tst_scriptfreetextwidth.moc"
+164
View File
@@ -0,0 +1,164 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QAction>
#include "scripting/scriptheader.h"
#include "shortcutmanager.h"
// The header that turns a stored script into a button, and taking a
// command out of ShortcutManager again when its script is deleted.
class tst_scriptheader : public QObject
{
Q_OBJECT
static QString script(const QString &lines)
{
return QStringLiteral("// ==QETScript==\n") + lines
+ QStringLiteral("// ==/QETScript==\nqet.log('x');\n");
}
private slots:
void fullHeader()
{
const ScriptHeader h = ScriptHeader::parse(script(QStringLiteral(
"// @name Add revision note\n"
"// @icon note.svg\n"
"// @tooltip Puts a note on the folio\n"
"// @shortcut Ctrl+Alt+R\n"
"// @context selection\n"
"// @api 1\n")), QStringLiteral("add-note"));
QVERIFY2(h.isValid(), qPrintable(h.error));
QCOMPARE(h.id, QStringLiteral("add-note"));
QCOMPARE(h.name, QStringLiteral("Add revision note"));
QCOMPARE(h.icon, QStringLiteral("note.svg"));
QCOMPARE(h.tooltip, QStringLiteral("Puts a note on the folio"));
QCOMPARE(h.shortcut, QStringLiteral("Ctrl+Alt+R"));
QCOMPARE(h.context, QStringLiteral("selection"));
QCOMPARE(h.api, 1);
}
void nameAloneIsEnough()
{
const ScriptHeader h = ScriptHeader::parse(
script(QStringLiteral("// @name Just a name\n")), QStringLiteral("a"));
QVERIFY2(h.isValid(), qPrintable(h.error));
QCOMPARE(h.context, QStringLiteral("canvas"));
QVERIFY(h.icon.isEmpty());
}
void refused_data()
{
QTest::addColumn<QString>("text");
QTest::addColumn<QString>("error");
QTest::newRow("no header") << QStringLiteral("qet.log('x');\n")
<< QStringLiteral("no // ==QETScript== header");
QTest::newRow("no name") << script(QStringLiteral("// @icon x.svg\n"))
<< QStringLiteral("@name is required");
QTest::newRow("typo") << script(QStringLiteral("// @name A\n// @shortcutt Ctrl+K\n"))
<< QStringLiteral("unknown header key @shortcutt");
QTest::newRow("context") << script(QStringLiteral("// @name A\n// @context wires\n"))
<< QStringLiteral("@context must be one of: canvas, selection, conductor");
QTest::newRow("api") << script(QStringLiteral("// @name A\n// @api 2\n"))
<< QStringLiteral("@api 2 is not supported by this version (1 is)");
}
void refused()
{
QFETCH(QString, text);
QFETCH(QString, error);
const ScriptHeader h = ScriptHeader::parse(text, QStringLiteral("a"));
QVERIFY(!h.isValid());
QCOMPARE(h.error, error);
}
// Only the header block counts: a "// @name" further down is code.
void onlyTheBlock()
{
const ScriptHeader h = ScriptHeader::parse(
script(QStringLiteral("// @name Real\n"))
+ QStringLiteral("// @name Not this one\n// @bogus key\n"),
QStringLiteral("a"));
QVERIFY2(h.isValid(), qPrintable(h.error));
QCOMPARE(h.name, QStringLiteral("Real"));
}
// What the script manager writes reads back as what was typed, and
// leaves the script's code as it was.
void composeReadsBack()
{
const QString body = QStringLiteral("var f = qet.currentFolio();\nqet.addText(f, 'x', 0, 0);\n");
const QString text = ScriptHeader::compose(
QStringLiteral(" Add note "), QStringLiteral("note.svg"),
QStringLiteral("A tip"), QStringLiteral("Ctrl+Alt+N"),
QStringLiteral("conductor"), body);
const ScriptHeader h = ScriptHeader::parse(text, QStringLiteral("add-note"));
QVERIFY2(h.isValid(), qPrintable(h.error));
QCOMPARE(h.name, QStringLiteral("Add note"));
QCOMPARE(h.icon, QStringLiteral("note.svg"));
QCOMPARE(h.tooltip, QStringLiteral("A tip"));
QCOMPARE(h.shortcut, QStringLiteral("Ctrl+Alt+N"));
QCOMPARE(h.context, QStringLiteral("conductor"));
QCOMPARE(ScriptHeader::bodyOf(text), body);
// empty fields and the default context are left out
const QString bare = ScriptHeader::compose(QStringLiteral("A"), QString(), QString(),
QString(), QStringLiteral("canvas"),
QStringLiteral("qet.log(1);"));
QCOMPARE(bare, QStringLiteral("// ==QETScript==\n// @name A\n// @api 1\n"
"// ==/QETScript==\nqet.log(1);\n"));
}
void bodyWithoutHeader()
{
QCOMPARE(ScriptHeader::bodyOf(QStringLiteral("qet.log(1);\n")),
QStringLiteral("qet.log(1);\n"));
}
void idFor()
{
QCOMPARE(ScriptHeader::idFor(QStringLiteral("Numéroter les fils !"), {}),
QStringLiteral("numeroter-les-fils"));
QCOMPARE(ScriptHeader::idFor(QStringLiteral("Add note"),
{QStringLiteral("add-note"), QStringLiteral("add-note-2")}),
QStringLiteral("add-note-3"));
QCOMPARE(ScriptHeader::idFor(QStringLiteral("!!!"), {}), QStringLiteral("script"));
}
// Two windows register one id; the id stays until both are gone, and a
// script that comes back under a new name is listed by that name.
void unregisterAction()
{
ShortcutManager &m = ShortcutManager::instance();
const QString id = QStringLiteral("diagrameditor.script.tst");
auto listed = [&m, &id]() -> QString {
for (const ShortcutManager::ShortcutInfo &info : m.allShortcuts())
if (info.id == id) return info.description;
return QString();
};
QAction a(QStringLiteral("Old name")), b(QStringLiteral("Old name"));
m.registerAction(&a, id, QStringLiteral("Scripts"), QKeySequence());
m.registerAction(&b, id, QStringLiteral("Scripts"), QKeySequence());
QCOMPARE(listed(), QStringLiteral("Old name"));
m.unregisterAction(&a, id);
QCOMPARE(listed(), QStringLiteral("Old name"));
QCOMPARE(m.action(id, nullptr), &b);
m.unregisterAction(&b, id);
QVERIFY(listed().isEmpty());
QCOMPARE(m.action(id, nullptr), nullptr);
QAction c(QStringLiteral("New name"));
m.registerAction(&c, id, QStringLiteral("Scripts"), QKeySequence());
QCOMPARE(listed(), QStringLiteral("New name"));
m.unregisterAction(&c, id);
// unknown id: nothing to do, nothing breaks
m.unregisterAction(&c, QStringLiteral("diagrameditor.script.none"));
}
};
QTEST_MAIN(tst_scriptheader)
#include "tst_scriptheader.moc"
+170
View File
@@ -0,0 +1,170 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTemporaryDir>
#include <QUuid>
// qet.terminalPosition(folio, element, terminal) and qet.conductorPath(folio,
// uuid): where a wire docks on a terminal and which way it leaves, and a
// wire's drawn path by its uuid. Checked against each other on every
// conductor of the fixture: a path starts and ends exactly where its two
// terminals say a wire docks, and leaves each the way it faces. Runs a
// script through the real binary's --run.
class tst_scriptlayoutreads : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
// Run @p script on the fixture in a sandbox of its own and return the
// JSON object it logged.
QJsonObject run(const QString &script)
{
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write(script.toUtf8());
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
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),
{QStringLiteral("--run"), path,
QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet")});
if (!proc.waitForFinished(60000)) return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 0)
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
}
return {};
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void pathsStartAndEndAtTheirTerminals()
{
const QJsonObject r = run(QStringLiteral(
"var out = [];\n"
"var uuids = qet.conductorUuids(0);\n"
"for (var i = 0; i < uuids.length; i++) {\n"
" var ends = qet.conductorEnds(0, uuids[i]);\n"
" var t = ends.map(function (e) { var m = e.split(' terminal ');\n"
" return qet.terminalPosition(0, m[0], parseInt(m[1], 10)); });\n"
" out.push({path: qet.conductorPath(0, uuids[i]), t: t});\n"
"}\n"
"qet.log('PROBE ' + JSON.stringify({wires: out,\n"
" unknown: qet.conductorPath(0, '{00000000-0000-0000-0000-000000000001}'),\n"
" junk: qet.conductorPath(0, 'not a uuid'),\n"
" badFolio: qet.conductorPath(99, uuids[0]),\n"
" badTerminal: qet.terminalPosition(0, qet.elementUuids(0)[0], 999)}));\n"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
const QJsonArray wires = r.value(QStringLiteral("wires")).toArray();
QCOMPARE(wires.size(), 7); // the fixture's conductors
const QHash<QString, QPointF> step{{QStringLiteral("n"), {0, -1}},
{QStringLiteral("e"), {1, 0}},
{QStringLiteral("s"), {0, 1}},
{QStringLiteral("w"), {-1, 0}}};
for (const QJsonValue &v : wires) {
const QJsonArray path = v.toObject().value(QStringLiteral("path")).toArray();
const QJsonArray t = v.toObject().value(QStringLiteral("t")).toArray();
QVERIFY(path.size() >= 2);
QCOMPARE(t.size(), 2);
const QJsonObject ends[2] = {path.first().toObject(), path.last().toObject()};
const QJsonObject next[2] = {path.at(1).toObject(), path.at(path.size() - 2).toObject()};
for (int k = 0; k < 2; ++k) {
const QJsonObject term = t.at(k).toObject();
QCOMPARE(ends[k].value(QStringLiteral("x")).toDouble(),
term.value(QStringLiteral("x")).toDouble());
QCOMPARE(ends[k].value(QStringLiteral("y")).toDouble(),
term.value(QStringLiteral("y")).toDouble());
// the first step out of a terminal goes the way it faces
const QString facing = term.value(QStringLiteral("facing")).toString();
QVERIFY2(step.contains(facing), qPrintable(facing));
const QPointF d(next[k].value(QStringLiteral("x")).toDouble()
- ends[k].value(QStringLiteral("x")).toDouble(),
next[k].value(QStringLiteral("y")).toDouble()
- ends[k].value(QStringLiteral("y")).toDouble());
if (d.isNull()) continue;
QVERIFY2(d.x() * step[facing].x() + d.y() * step[facing].y() > 0,
qPrintable(facing));
}
}
QVERIFY(r.value(QStringLiteral("unknown")).toArray().isEmpty());
QVERIFY(r.value(QStringLiteral("junk")).toArray().isEmpty());
QVERIFY(r.value(QStringLiteral("badFolio")).toArray().isEmpty());
QVERIFY(r.value(QStringLiteral("badTerminal")).toObject().isEmpty());
}
void pathMatchesConductorSegments()
{
// Where conductorSegments() can name the wire, both give the
// same points.
const QJsonObject r = run(QStringLiteral(
"var same = 0, compared = 0, lines = qet.conductors(0);\n"
"var uuids = qet.conductorUuids(0);\n"
"for (var i = 0; i < uuids.length; i++) {\n"
" var e = qet.conductorEnds(0, uuids[i])[0], n = 0;\n"
" for (var l = 0; l < lines.length; l++) {\n"
" var p = lines[l].split(' : ')[0].split(' -- ');\n"
" if (p[0] === e || p[1] === e) n++;\n"
" }\n"
" if (n !== 1) continue;\n"
" var m = e.split(' terminal ');\n"
" var segs = qet.conductorSegments(0, m[0], parseInt(m[1], 10));\n"
" var pts = [];\n"
" segs.forEach(function (s, k) {\n"
" var c = s.match(/\\(([^,]+),([^)]+)\\)-\\(([^,]+),([^)]+)\\)/);\n"
" if (k === 0) pts.push([+c[1], +c[2]]);\n"
" pts.push([+c[3], +c[4]]); });\n"
" var path = qet.conductorPath(0, uuids[i]).map(function (q) { return [q.x, q.y]; });\n"
" compared++;\n"
" if (JSON.stringify(pts) === JSON.stringify(path)) same++;\n"
"}\n"
"qet.log('PROBE ' + JSON.stringify({same: same, compared: compared}));\n"));
QVERIFY(r.value(QStringLiteral("compared")).toInt() > 0);
QCOMPARE(r.value(QStringLiteral("same")).toInt(), r.value(QStringLiteral("compared")).toInt());
}
void facingTurnsWithTheElement()
{
const QJsonObject r = run(QStringLiteral(
"var el = qet.elementUuids(0)[0];\n"
"var before = qet.terminalPosition(0, el, 0).facing;\n"
"qet.rotateElement(0, el, 90);\n"
"var after = qet.terminalPosition(0, el, 0).facing;\n"
"qet.log('PROBE ' + JSON.stringify({before: before, after: after}));\n"));
const QString order = QStringLiteral("nesw");
const int b = order.indexOf(r.value(QStringLiteral("before")).toString());
const int a = order.indexOf(r.value(QStringLiteral("after")).toString());
QVERIFY(b >= 0 && a >= 0);
QCOMPARE(a, (b + 1) % 4); // a quarter turn clockwise
}
};
QTEST_APPLESS_MAIN(tst_scriptlayoutreads)
#include "tst_scriptlayoutreads.moc"
+239
View File
@@ -0,0 +1,239 @@
// 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>
// Mirroring a symbol on its folio (#1335), through the real binary's --run
// on perceuse.qet: qet.mirrorElement() pushes the same undo command as
// Edit > Miroir horizontal / vertical. A horizontal mirror swaps the
// terminals left and right and turns the ones facing east to face west; a
// vertical one does the same top and bottom; mirroring twice the same way
// puts the symbol back exactly; and a save keeps the mirror.
class tst_scriptmirror : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
// Run @p script on @p project in a sandbox of its own and return the
// JSON object it logged.
QJsonObject run(const QString &script, const QString &project)
{
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write(script.toUtf8());
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--run"), path, project});
if (!proc.waitForFinished(120000)) return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 0)
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
}
return {};
}
static QString facing(const QJsonArray &terminals, int i)
{
return terminals.at(i).toObject().value(QStringLiteral("facing")).toString();
}
static double coord(const QJsonArray &terminals, int i, const char *axis)
{
return terminals.at(i).toObject().value(QLatin1String(axis)).toDouble();
}
// The facing of a terminal once mirrored: across a vertical line
// (horizontal mirror) east and west swap, across a horizontal one north
// and south do.
static QString mirrored(const QString &facing, bool vertical)
{
const QString swap = vertical ? QStringLiteral("ns") : QStringLiteral("ew");
const int i = swap.indexOf(facing);
return i < 0 ? facing : QString(swap.at(1 - i));
}
// @p after is @p before mirrored horizontally (or vertically): the
// terminals face the mirrored way, keep the other coordinate, and are
// mirrored across one same line, so x + x' (or y + y') is the same for
// all of them.
static void checkMirrored(const QJsonArray &before, const QJsonArray &after, bool vertical)
{
QCOMPARE(after.size(), before.size());
const char *across = vertical ? "y" : "x";
const char *along = vertical ? "x" : "y";
const double axis = coord(before, 0, across) + coord(after, 0, across);
const double shift = coord(after, 0, along) - coord(before, 0, along);
for (int i = 0; i < before.size(); ++i) {
QCOMPARE(facing(after, i), mirrored(facing(before, i), vertical));
QCOMPARE(coord(before, i, across) + coord(after, i, across), axis);
QCOMPARE(coord(after, i, along) - coord(before, i, along), shift);
}
}
static QString script(const QString &body)
{
return QStringLiteral(
"function terms(f, el) {\n"
" var r = [], n = qet.elementTerminals(f, el).length;\n"
" for (var i = 0; i < n; i++) r.push(qet.terminalPosition(f, el, i));\n"
" return r;\n"
"}\n") + body;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void mirrorBackAndSave()
{
const QString saved = m_dir.filePath(QStringLiteral("mirrored.qet"));
const QJsonObject r = run(script(QStringLiteral(
// a symbol with terminals at different x and different y, one
// of them facing east or west
"var pick = null;\n"
"for (var f = 0; f < qet.folioCount() && !pick; f++) {\n"
" var els = qet.elementUuids(f);\n"
" for (var e = 0; e < els.length && !pick; e++) {\n"
" var t = terms(f, els[e]);\n"
" if (t.length < 2) continue;\n"
" var side = t.some(function (p) { return p.facing === 'e' || p.facing === 'w'; });\n"
" var xs = t.some(function (p) { return p.x !== t[0].x; });\n"
" var ys = t.some(function (p) { return p.y !== t[0].y; });\n"
" if (side && xs && ys) pick = {f: f, e: els[e]};\n"
" }\n"
"}\n"
"var f = pick.f, el = pick.e, out = {element: el};\n"
"out.before = terms(f, el);\n"
"qet.mirrorElement(f, el, false);\n"
"out.h = terms(f, el); out.hMirror = qet.elementMirror(f, el);\n"
"qet.mirrorElement(f, el, false);\n"
"out.back = terms(f, el); out.backMirror = qet.elementMirror(f, el);\n"
"qet.mirrorElement(f, el, true);\n"
"out.v = terms(f, el); out.vMirror = qet.elementMirror(f, el);\n"
"qet.undo();\n"
"out.undone = terms(f, el);\n"
"qet.mirrorElement(f, el, true);\n"
"out.folio = f;\n"
"out.saved = qet.save(%1);\n"
"qet.log('PROBE ' + JSON.stringify(out));\n")
.arg(QString::fromUtf8(QJsonDocument(QJsonArray{saved}).toJson(QJsonDocument::Compact)).mid(1).chopped(1))),
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
const QJsonArray before = r.value(QStringLiteral("before")).toArray();
QVERIFY(before.size() >= 2);
QVERIFY(!r.value(QStringLiteral("hMirror")).toString().isEmpty());
checkMirrored(before, r.value(QStringLiteral("h")).toArray(), false);
QCOMPARE(r.value(QStringLiteral("backMirror")).toString(), QString());
QCOMPARE(r.value(QStringLiteral("back")).toArray(), before);
checkMirrored(before, r.value(QStringLiteral("v")).toArray(), true);
QVERIFY(r.value(QStringLiteral("vMirror")).toString()
!= r.value(QStringLiteral("hMirror")).toString());
QCOMPARE(r.value(QStringLiteral("undone")).toArray(), before);
//The save keeps the mirror, and only on that element
QVERIFY(r.value(QStringLiteral("saved")).toBool());
QFile file(saved);
QVERIFY(file.open(QIODevice::ReadOnly));
QDomDocument doc;
QVERIFY(bool(doc.setContent(&file)));
const QString element = r.value(QStringLiteral("element")).toString();
const QDomNodeList elements = doc.elementsByTagName(QStringLiteral("element"));
int mirrored_count = 0;
for (int i = 0; i < elements.size(); ++i) {
const QDomElement e = elements.at(i).toElement();
if (e.hasAttribute(QStringLiteral("mirror"))) {
++mirrored_count;
QCOMPARE(e.attribute(QStringLiteral("uuid")), element);
QCOMPARE(e.attribute(QStringLiteral("mirror")),
r.value(QStringLiteral("vMirror")).toString());
}
}
QCOMPARE(mirrored_count, 1);
const QJsonObject reloaded = run(script(QStringLiteral(
"var f = %1, el = '%2';\n"
"qet.log('PROBE ' + JSON.stringify({t: terms(f, el), m: qet.elementMirror(f, el)}));\n")
.arg(r.value(QStringLiteral("folio")).toInt()).arg(element)), saved);
QCOMPARE(reloaded.value(QStringLiteral("m")).toString(),
r.value(QStringLiteral("vMirror")).toString());
QCOMPARE(reloaded.value(QStringLiteral("t")).toArray(),
r.value(QStringLiteral("v")).toArray());
}
/**
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
upright does not swing round.
*/
void turnedElement_data()
{
QTest::addColumn<bool>("vertical");
QTest::newRow("horizontal") << false;
QTest::newRow("vertical") << true;
}
void turnedElement()
{
QFETCH(bool, vertical);
const QJsonObject r = run(script(QStringLiteral(
"var pick = null;\n"
"for (var f = 0; f < qet.folioCount() && !pick; f++) {\n"
" var els = qet.elementUuids(f);\n"
" for (var e = 0; e < els.length && !pick; e++) {\n"
" var t = terms(f, els[e]);\n"
" if (t.length >= 2 && t.some(function (p) { return p.x !== t[0].x; })\n"
" && t.some(function (p) { return p.y !== t[0].y; })) pick = {f: f, e: els[e]};\n"
" }\n"
"}\n"
"var f = pick.f, el = pick.e, out = {};\n"
"qet.rotateElement(f, el, 90);\n"
"out.before = terms(f, el);\n"
"qet.mirrorElement(f, el, %1);\n"
"out.after = terms(f, el); out.mirror = qet.elementMirror(f, el);\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");
checkMirrored(r.value(QStringLiteral("before")).toArray(),
r.value(QStringLiteral("after")).toArray(), vertical);
//turned by 90 degrees, the symbol's own axes are the other way
QCOMPARE(r.value(QStringLiteral("mirror")).toString(),
vertical ? QStringLiteral("horizontal") : QStringLiteral("vertical"));
}
};
QTEST_APPLESS_MAIN(tst_scriptmirror)
#include "tst_scriptmirror.moc"
+145
View File
@@ -0,0 +1,145 @@
/*
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 "shortcutmanager.h"
#include "ui/configpage/shortcutsconfigpage.h"
#include <QAction>
#include <QComboBox>
#include <QKeySequenceEdit>
#include <QMainWindow>
#include <QMenuBar>
#include <QPixmap>
#include <QSettings>
#include <QStandardPaths>
#include <QTest>
#include <QTreeWidget>
/**
The Shortcuts configuration page: the icon and menu columns, the
category filter, finding a command by pressing its key, and the
copied list.
*/
class tst_shortcutspage : public QObject
{
Q_OBJECT
QMainWindow m_window;
QAction *m_save = nullptr;
QAction *m_run = nullptr;
QAction *m_tool = nullptr;
QTreeWidgetItem *row(QTreeWidget *tree, const QString &text)
{
const auto found = tree->findItems(text, Qt::MatchExactly | Qt::MatchRecursive, 0);
return found.isEmpty() ? nullptr : found.first();
}
QStringList visible(QTreeWidget *tree)
{
QStringList texts;
for (const QString &text : {QStringLiteral("Enregistrer"), QStringLiteral("Exécuter"), QStringLiteral("Outil")}) {
if (QTreeWidgetItem *item = row(tree, text); item && !item->isHidden()) {
texts << text;
}
}
return texts;
}
private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
QSettings().remove(QStringLiteral("shortcuts"));
QPixmap pixmap(16, 16);
pixmap.fill(Qt::red);
QMenu *file = m_window.menuBar()->addMenu(QStringLiteral("&Fichier"));
QMenu *scripts = file->addMenu(QStringLiteral("&Scripts"));
m_save = file->addAction(QIcon(pixmap), QStringLiteral("&Enregistrer"));
m_run = scripts->addAction(QStringLiteral("Exécuter"));
//On a toolbar only, in no menu
m_tool = new QAction(QStringLiteral("Outil"), &m_window);
ShortcutManager &manager = ShortcutManager::instance();
manager.registerAction(m_save, QStringLiteral("t.save"), QStringLiteral("Catégorie A"), QKeySequence(Qt::CTRL | Qt::Key_S));
manager.registerAction(m_run, QStringLiteral("t.run"), QStringLiteral("Catégorie A"), QKeySequence(Qt::CTRL | Qt::Key_R));
manager.registerAction(m_tool, QStringLiteral("t.tool"), QStringLiteral("Catégorie B"), QKeySequence());
}
void menuPath()
{
QCOMPARE(ShortcutsConfigPage::menuPath(m_save), QStringLiteral("Fichier"));
QCOMPARE(ShortcutsConfigPage::menuPath(m_run), QStringLiteral("Fichier › Scripts"));
QCOMPARE(ShortcutsConfigPage::menuPath(m_tool), QString());
QCOMPARE(ShortcutsConfigPage::menuPath(nullptr), QString());
}
void iconAndMenuColumns()
{
ShortcutsConfigPage page(nullptr);
auto *tree = page.findChild<QTreeWidget *>();
QVERIFY(row(tree, QStringLiteral("Enregistrer")));
QVERIFY(!row(tree, QStringLiteral("Enregistrer"))->icon(0).isNull());
QVERIFY(row(tree, QStringLiteral("Outil"))->icon(0).isNull());
QCOMPARE(row(tree, QStringLiteral("Exécuter"))->text(1), QStringLiteral("Fichier › Scripts"));
QCOMPARE(row(tree, QStringLiteral("Outil"))->text(1), QString());
}
void categoryFilter()
{
ShortcutsConfigPage page(nullptr);
auto *tree = page.findChild<QTreeWidget *>();
auto *combo = page.findChild<QComboBox *>(QStringLiteral("categoryFilterCombo"));
QCOMPARE(visible(tree).size(), 3);
combo->setCurrentIndex(combo->findText(QStringLiteral("Catégorie B")));
QCOMPARE(visible(tree), QStringList{QStringLiteral("Outil")});
combo->setCurrentIndex(0);
QCOMPARE(visible(tree).size(), 3);
}
void findByKey()
{
ShortcutsConfigPage page(nullptr);
auto *tree = page.findChild<QTreeWidget *>();
auto *key = page.findChild<QKeySequenceEdit *>(QStringLiteral("keySearchEdit"));
key->setKeySequence(QKeySequence(Qt::CTRL | Qt::Key_S));
QCOMPARE(visible(tree), QStringList{QStringLiteral("Enregistrer")});
key->setKeySequence(QKeySequence(Qt::CTRL | Qt::Key_J));
QVERIFY(visible(tree).isEmpty());
key->clear();
QCOMPARE(visible(tree).size(), 3);
}
void copiedList()
{
ShortcutsConfigPage page(nullptr);
auto *combo = page.findChild<QComboBox *>(QStringLiteral("categoryFilterCombo"));
combo->setCurrentIndex(combo->findText(QStringLiteral("Catégorie A")));
const QStringList lines = page.listAsText().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
QCOMPARE(lines.size(), 3);
QCOMPARE(lines.at(0).count(QLatin1Char('\t')), 3);
QVERIFY(lines.contains(QStringLiteral("Catégorie A\tFichier\tEnregistrer\t")
+ QKeySequence(Qt::CTRL | Qt::Key_S).toString(QKeySequence::NativeText)));
QVERIFY(lines.contains(QStringLiteral("Catégorie A\tFichier › Scripts\tExécuter\t")
+ QKeySequence(Qt::CTRL | Qt::Key_R).toString(QKeySequence::NativeText)));
}
};
QTEST_MAIN(tst_shortcutspage)
#include "tst_shortcutspage.moc"
+208
View File
@@ -0,0 +1,208 @@
#include <QtTest>
#include <QGraphicsRectItem>
#include <QGraphicsScene>
#include "shownkinds.h"
class tst_shownkinds : public QObject
{
Q_OBJECT
QGraphicsScene *scene = nullptr;
QGraphicsRectItem *add(QGraphicsItem *parent = nullptr)
{
auto item = new QGraphicsRectItem(0, 0, 10, 10, parent);
item->setFlag(QGraphicsItem::ItemIsSelectable);
if (!parent) {
scene->addItem(item);
}
return item;
}
private slots:
void init()
{
scene = new QGraphicsScene;
for (int k = 0; k < ShownKinds::KindCount; ++k) {
ShownKinds::setShown(ShownKinds::Kind(k), true);
}
}
void cleanup()
{
delete scene;
scene = nullptr;
}
void hideAndShowAgain()
{
auto shape = add();
auto text = add();
ShownKinds::tag(shape, ShownKinds::Shapes);
ShownKinds::tag(text, ShownKinds::FreeTexts);
ShownKinds::setShown(ShownKinds::Shapes, false);
ShownKinds::apply(scene, ShownKinds::Shapes);
QVERIFY(!shape->isVisible());
QVERIFY(text->isVisible());
QCOMPARE(ShownKinds::hiddenCount(), 1);
ShownKinds::setShown(ShownKinds::Shapes, true);
ShownKinds::apply(scene, ShownKinds::Shapes);
QVERIFY(shape->isVisible());
QCOMPARE(ShownKinds::hiddenCount(), 0);
}
// Created while its kind is hidden: starts hidden, whatever created it.
void createdWhileHidden()
{
ShownKinds::setShown(ShownKinds::Pictures, false);
auto picture = add();
ShownKinds::tag(picture, ShownKinds::Pictures);
QVERIFY(!picture->isVisible());
}
// Untagged items (symbols, wires) are never touched.
void untaggedUntouched()
{
auto symbol = add();
auto hidden_symbol = add();
hidden_symbol->setVisible(false);
for (int k = 0; k < ShownKinds::KindCount; ++k) {
ShownKinds::setShown(ShownKinds::Kind(k), false);
ShownKinds::apply(scene, ShownKinds::Kind(k));
}
QVERIFY(symbol->isVisible());
for (int k = 0; k < ShownKinds::KindCount; ++k) {
ShownKinds::setShown(ShownKinds::Kind(k), true);
ShownKinds::apply(scene, ShownKinds::Kind(k));
}
QVERIFY(!hidden_symbol->isVisible());
}
// apply() touches only the kind it is given.
void applyOnlyThatKind()
{
auto shape = add();
ShownKinds::tag(shape, ShownKinds::Shapes);
ShownKinds::setShown(ShownKinds::Shapes, false);
ShownKinds::apply(scene, ShownKinds::Tables);
QVERIFY(shape->isVisible());
}
// An item that hid itself (a wire number switched off) stays hidden
// when its kind is hidden and shown again.
void ownHiddenStaysHidden()
{
auto number = add();
ShownKinds::tag(number, ShownKinds::WireNumbers);
ShownKinds::setVisible(number, false);
ShownKinds::setShown(ShownKinds::WireNumbers, false);
ShownKinds::apply(scene, ShownKinds::WireNumbers);
ShownKinds::setShown(ShownKinds::WireNumbers, true);
ShownKinds::apply(scene, ShownKinds::WireNumbers);
QVERIFY(!number->isVisible());
}
// An item asking to be shown while its kind is hidden stays hidden,
// and appears when the kind is shown (one text per potential moving
// to another wire, a cross-reference changing its snap).
void showVetoedThenRestored()
{
auto number = add();
ShownKinds::tag(number, ShownKinds::WireNumbers);
ShownKinds::setShown(ShownKinds::WireNumbers, false);
ShownKinds::apply(scene, ShownKinds::WireNumbers);
ShownKinds::setVisible(number, true);
QVERIFY(!number->isVisible());
ShownKinds::setShown(ShownKinds::WireNumbers, true);
ShownKinds::apply(scene, ShownKinds::WireNumbers);
QVERIFY(number->isVisible());
}
// Hidden while its kind is hidden: not shown again with the kind.
void hiddenWhileKindHidden()
{
auto number = add();
ShownKinds::tag(number, ShownKinds::WireNumbers);
ShownKinds::setShown(ShownKinds::WireNumbers, false);
ShownKinds::apply(scene, ShownKinds::WireNumbers);
ShownKinds::setVisible(number, false);
ShownKinds::setShown(ShownKinds::WireNumbers, true);
ShownKinds::apply(scene, ShownKinds::WireNumbers);
QVERIFY(!number->isVisible());
}
// wantsVisible() tells the wire carrying a potential's text from the
// others while wire texts are hidden, so deleting it moves the text.
void wantsVisibleWhileKindHidden()
{
auto carrier = add();
auto other = add();
ShownKinds::tag(carrier, ShownKinds::WireNumbers);
ShownKinds::tag(other, ShownKinds::WireNumbers);
ShownKinds::setVisible(other, false);
ShownKinds::setShown(ShownKinds::WireNumbers, false);
ShownKinds::apply(scene, ShownKinds::WireNumbers);
QVERIFY(!carrier->isVisible());
QVERIFY(ShownKinds::wantsVisible(carrier));
QVERIFY(!ShownKinds::wantsVisible(other));
ShownKinds::setShown(ShownKinds::WireNumbers, true);
ShownKinds::apply(scene, ShownKinds::WireNumbers);
QVERIFY(ShownKinds::wantsVisible(carrier));
QVERIFY(!ShownKinds::wantsVisible(other));
}
// A text under a hidden parent still counts as wanting to be visible.
void childOfHiddenParent()
{
auto group = add();
auto text = add(group);
ShownKinds::tag(group, ShownKinds::SymbolTexts);
ShownKinds::tag(text, ShownKinds::CrossReferences);
ShownKinds::setShown(ShownKinds::SymbolTexts, false);
ShownKinds::apply(scene, ShownKinds::SymbolTexts);
ShownKinds::setShown(ShownKinds::CrossReferences, false);
ShownKinds::apply(scene, ShownKinds::CrossReferences);
ShownKinds::setShown(ShownKinds::SymbolTexts, true);
ShownKinds::apply(scene, ShownKinds::SymbolTexts);
QVERIFY(!text->isVisible());
ShownKinds::setShown(ShownKinds::CrossReferences, true);
ShownKinds::apply(scene, ShownKinds::CrossReferences);
QVERIFY(text->isVisible());
}
// isHidden() follows the parents: a cross-reference under a symbol text.
void hiddenThroughParent()
{
auto symbol = add();
auto label = add(symbol);
auto xref = add(label);
ShownKinds::tag(label, ShownKinds::SymbolTexts);
ShownKinds::tag(xref, ShownKinds::CrossReferences);
QVERIFY(!ShownKinds::isHidden(xref));
ShownKinds::setShown(ShownKinds::SymbolTexts, false);
QVERIFY(ShownKinds::isHidden(xref));
QVERIFY(!ShownKinds::isHidden(symbol));
}
// Hidden means safe from Select All, copy and delete: Qt will not
// select it, and drops a selection it had.
void hiddenIsNotSelectable()
{
auto text = add();
ShownKinds::tag(text, ShownKinds::FreeTexts);
text->setSelected(true);
ShownKinds::setShown(ShownKinds::FreeTexts, false);
ShownKinds::apply(scene, ShownKinds::FreeTexts);
QVERIFY(!text->isSelected());
text->setSelected(true);
QVERIFY(scene->selectedItems().isEmpty());
QVERIFY(scene->items(QRectF(-5, -5, 20, 20)).isEmpty());
}
};
QTEST_MAIN(tst_shownkinds)
#include "tst_shownkinds.moc"
+28
View File
@@ -120,6 +120,34 @@ void SmartDeviceTest::defaultQueryAndCsv()
QVERIFY(csv.contains("Terminal block"));
QVERIFY(!csv.contains("Must not be exported"));
QVERIFY(csv.indexOf("-K1") < csv.indexOf("-K2"));
//#1178: a junction -- a terminal-type symbol drawn where wires
//branch -- has no label and no part data. It can be left out, and
//so can the contact blocks; a terminal block with only a part
//number stays.
QSqlQuery raw(db);
QVERIFY(raw.exec(QStringLiteral(
"INSERT INTO element_nomenclature_view "
"(label, element_type, diagram_position, position) "
"VALUES (NULL, 'terminal', 1, 6)")));
QVERIFY(raw.exec(QStringLiteral(
"INSERT INTO element_nomenclature_view "
"(label, designation, manufacturer, manufacturer_reference, "
"element_type, diagram_position, position) "
"VALUES ('', '', '', 'PT 2,5', 'terminal', 1, 7)")));
auto count = [&db](const QString &sql) {
QSqlQuery q(db);
if (!q.exec(sql))
return -1;
int n = 0;
while (q.next())
++n;
return n;
};
QCOMPARE(count(BomExport::defaultQuery()), 6);
QCOMPARE(count(BomExport::defaultQuery(true, false)), 5);
QCOMPARE(count(BomExport::defaultQuery(false, true)), 5);
QCOMPARE(count(BomExport::defaultQuery(false, false)), 4);
}
QSqlDatabase::removeDatabase(connection);
}
+80
View File
@@ -0,0 +1,80 @@
/*
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 "editor/symbolscale.h"
class tst_symbolscale : public QObject
{
Q_OBJECT
private slots:
// Terminals 10 px apart: whole factors are safe, halving is not.
void tenPitchTakesWholeFactors()
{
const QList<QPointF> terminals{{0, -20}, {0, 20}, {10, 0}};
QCOMPARE(SymbolScale::safeFactors(terminals),
(QList<qreal>{2.0, 3.0, 4.0}));
}
// Terminals 20 px apart: every candidate is safe.
void twentyPitchTakesEverything()
{
const QList<QPointF> terminals{{-20, 0}, {20, 40}};
QCOMPARE(SymbolScale::safeFactors(terminals), SymbolScale::candidates());
}
// Off the grid by 5 px today: only even factors bring it on.
void fivePxOffIsRescuedByEvenFactors()
{
const QList<QPointF> terminals{{-5, 0}, {5, 30}};
QVERIFY(!SymbolScale::allOnGrid(terminals));
QCOMPARE(SymbolScale::offGridCount(terminals), 2);
QCOMPARE(SymbolScale::safeFactors(terminals),
(QList<qreal>{2.0, 4.0}));
}
// Off by an odd amount: nothing helps.
void oddOffsetHasNoSafeFactor()
{
QVERIFY(SymbolScale::safeFactors({{-7, 0}, {0, 20}}).isEmpty());
}
void noTerminalTakesEverything()
{
QCOMPARE(SymbolScale::safeFactors({}), SymbolScale::candidates());
}
// Saved with two decimals: 19.999 still counts as 20.
void savedRoundingIsTolerated()
{
QVERIFY(SymbolScale::onGrid(19.999));
QVERIFY(SymbolScale::onGrid(-30.001));
QVERIFY(!SymbolScale::onGrid(19.9));
}
void fontSizeIsWholeAndReadable()
{
QCOMPARE(SymbolScale::scaledFontSize(9, 2.0), 18);
QCOMPARE(SymbolScale::scaledFontSize(9, 1.5), 14);
QCOMPARE(SymbolScale::scaledFontSize(6, 0.5), 4);
}
};
QTEST_APPLESS_MAIN(tst_symbolscale)
#include "tst_symbolscale.moc"
+77
View File
@@ -0,0 +1,77 @@
/*
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 label formula with ten or more sequential numbers: %sequ_10 must get
the tenth value, not the first value followed by a "0". The values are
put in by AssignVariables::assignSequence(), which the bill of
materials uses for every label, so the real binary's --export-bom on
fixtures/ten_sequential_numbers.qet shows the result: its element has
the formula %sequ_10-%sequ_1 and the unit values A to J.
*/
class tst_tensequentialnumbers : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
static bool runQet(const QStringList &arguments)
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
return proc.waitForFinished(60000)
&& proc.exitStatus() == QProcess::NormalExit
&& proc.exitCode() == 0;
}
private slots:
void initTestCase()
{
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
QVERIFY(m_dir.isValid());
}
void tenthValueIsUsed()
{
const QString fixture = QFINDTESTDATA("fixtures/ten_sequential_numbers.qet");
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
const QString out = m_dir.filePath(QStringLiteral("bom.csv"));
QVERIFY2(runQet({QStringLiteral("--export-bom"), fixture, out}), "--export-bom failed");
QFile file(out);
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
const QStringList lines = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
QStringList labels;
for (const QString &line : lines) {
if (line.startsWith(QLatin1Char('"')))
labels << line.section(QLatin1Char(';'), 0, 0);
}
QVERIFY2(labels.contains(QStringLiteral("\"J-A\"")),
qPrintable(QStringLiteral("labels: %1").arg(labels.join(QLatin1Char(' ')))));
QVERIFY2(!labels.contains(QStringLiteral("\"A0-A\"")), "the first value with a 0 appended");
}
};
QTEST_MAIN(tst_tensequentialnumbers)
#include "tst_tensequentialnumbers.moc"
+115
View File
@@ -0,0 +1,115 @@
/*
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 <QTextDocument>
#include <QTextOption>
#include "textlines.h"
class tst_textlines : public QObject
{
Q_OBJECT
private slots:
// A text without a width gives its own lines.
void unwrappedTextGivesItsLines()
{
QTextDocument document(QStringLiteral("Motor protection\nswitch Q12"));
QCOMPARE(TextLines::layoutLines(&document),
QStringList({QStringLiteral("Motor protection"), QStringLiteral("switch Q12")}));
}
// A text without a width gives exactly the lines the DXF export wrote
// before, toPlainText().split('\n'): trailing spaces and tabs kept,
// non-breaking spaces as plain spaces, <br> as a line break.
void unwrappedTextIsUnchanged_data()
{
QTest::addColumn<QString>("html");
QTest::newRow("trailing spaces") << QStringLiteral("K1 \nswitch\t");
QTest::newRow("non-breaking spaces") << QStringLiteral("<p>K1&nbsp;&nbsp;24V&nbsp;DC</p>");
QTest::newRow("line break") << QStringLiteral("<p>Wiper to 6<br>contacts</p>");
QTest::newRow("paragraphs") << QStringLiteral("<p>relais voyant </p><p> preventa</p>");
QTest::newRow("empty") << QString();
}
void unwrappedTextIsUnchanged()
{
QFETCH(QString, html);
QTextDocument document;
if (Qt::mightBeRichText(html))
document.setHtml(html);
else
document.setPlainText(html);
QCOMPARE(TextLines::layoutLines(&document),
document.toPlainText().split(QLatin1Char('\n')));
}
// A text wrapped to its width gives one line per drawn line, without
// the space the line was broken at.
void wrappedTextGivesTheDrawnLines()
{
QTextDocument document(QStringLiteral("Motor protection switch Q12"));
QTextOption option = document.defaultTextOption();
option.setWrapMode(QTextOption::WordWrap);
document.setDefaultTextOption(option);
document.setTextWidth(1);
QCOMPARE(TextLines::layoutLines(&document),
QStringList({QStringLiteral("Motor"), QStringLiteral("protection"),
QStringLiteral("switch"), QStringLiteral("Q12")}));
}
// A wrapped paragraph has no trailing spaces left, even on its last
// line: tabs and spaces that wrap to a line of their own (as in the
// affuteuse_250h example) give an empty line, which is not written but
// still keeps the line spacing.
void wrappedTextHasNoTrailingSpaces()
{
QTextDocument document(QStringLiteral("\tMotor\t\t "));
QTextOption option = document.defaultTextOption();
option.setWrapMode(QTextOption::WordWrap);
document.setDefaultTextOption(option);
document.setTextWidth(80);
const QStringList lines = TextLines::layoutLines(&document);
QVERIFY(lines.size() > 1);
QVERIFY(lines.contains(QStringLiteral("Motor")));
for (const QString &line : lines)
QVERIFY2(line.isEmpty() || !line.back().isSpace(), qPrintable(line));
}
// Non-breaking spaces also become plain spaces in a wrapped text.
void wrappedTextHasPlainSpaces()
{
QTextDocument document;
document.setHtml(QStringLiteral("<p>K1&nbsp;24V and more words</p>"));
QTextOption option = document.defaultTextOption();
option.setWrapMode(QTextOption::WordWrap);
document.setDefaultTextOption(option);
document.setTextWidth(1);
const QStringList lines = TextLines::layoutLines(&document);
QCOMPARE(lines.first(), QStringLiteral("K1 24V"));
for (const QString &line : lines)
QVERIFY(!line.contains(QChar::Nbsp));
}
};
QTEST_MAIN(tst_textlines)
#include "tst_textlines.moc"
+329
View File
@@ -0,0 +1,329 @@
/*
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 <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsTextItem>
#include <QSignalSpy>
#include <QTextBlock>
#include <QTextLayout>
#include <QTextOption>
#include <QUndoStack>
#include <cmath>
#include "QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "QetGraphicsItemModeler/textresizehandles.h"
using TextResize::Corner;
/**
A text with the "textWidth" property the texts of QElectroTech have.
*/
class WidthText : public QGraphicsTextItem
{
Q_OBJECT
Q_PROPERTY(qreal textWidth READ textWidth WRITE setWidth)
public:
WidthText(const QString &text) : QGraphicsTextItem(text)
{
QTextOption option = document()->defaultTextOption();
option.setWrapMode(QTextOption::WordWrap);
document()->setDefaultTextOption(option);
}
void setWidth(qreal width) {setTextWidth(width);}
};
/// The number of lines the text is laid out on.
static int lineCount(const QGraphicsTextItem &text)
{
text.document()->size();
int lines = 0;
for (QTextBlock block = text.document()->begin() ; block.isValid() ; block = block.next())
lines += block.layout()->lineCount();
return lines;
}
static bool samePoint(const QPointF &a, const QPointF &b)
{
return qAbs(a.x() - b.x()) < 1e-6 && qAbs(a.y() - b.y()) < 1e-6;
}
static QPointF sceneCorner(const QGraphicsTextItem &text, Corner corner)
{
return text.mapToScene(TextResize::cornerOf(text.boundingRect(), corner));
}
/// Send a mouse event of type to the handle at corner, as the scene would.
static void sendMouse(QGraphicsScene &scene, TextResizeHandles &handles,
Corner corner, QEvent::Type type, const QPointF &scene_pos)
{
QGraphicsSceneMouseEvent event(type);
event.setButton(Qt::LeftButton);
event.setButtons(type == QEvent::GraphicsSceneMouseRelease ? Qt::NoButton : Qt::LeftButton);
event.setScenePos(scene_pos);
scene.sendEvent(handles.handle(corner), &event);
}
/// Drag the handle at corner from where it is by offset, in scene coordinates.
static void drag(QGraphicsScene &scene, TextResizeHandles &handles,
WidthText &text, Corner corner, const QPointF &offset)
{
const QPointF start = sceneCorner(text, corner);
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMousePress, start);
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseMove, start + offset / 2);
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseMove, start + offset);
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseRelease, start + offset);
}
class tst_textresizehandles : public QObject
{
Q_OBJECT
private slots:
// Pure geometry: the width given by a drag does not change while the
// text moves to keep its opposite corner in place.
void widthForDragUsesThePressTransform()
{
WidthText text(QStringLiteral("Motor protection switch"));
text.setRotation(37);
text.setPos(10, 10);
const QTransform press_inverse = text.sceneTransform().inverted();
const QPointF fixed = TextResize::cornerOf(text.boundingRect(), TextResize::TopLeft);
const QPointF mouse = text.mapToScene(QPointF(80, 3));
const qreal width = TextResize::widthForDrag(press_inverse, fixed, mouse, TextResize::BottomRight, 0);
QCOMPARE(qRound(width * 1000), 80000);
text.setPos(500, -300);
QCOMPARE(TextResize::widthForDrag(press_inverse, fixed, mouse, TextResize::BottomRight, 0), width);
}
// The minimum width is the longest word: a text that narrow does not
// break any word, one narrower would.
void minimumWidthIsTheLongestWord()
{
WidthText text(QStringLiteral("a Motorschutzschalter b"));
const qreal minimum = TextResize::minimumWidth(text.document());
WidthText word(QStringLiteral("Motorschutzschalter"));
QVERIFY(qAbs(minimum - word.boundingRect().width()) < 0.5);
text.setWidth(minimum);
QVERIFY(qAbs(text.boundingRect().width() - minimum) < 0.5);
QCOMPARE(lineCount(text), 3);
}
// The minimum width never cuts a word, even for a document that would
// otherwise break a word anywhere.
void minimumWidthKeepsWords()
{
QGraphicsTextItem text(QStringLiteral("a Motorschutzschalter"));
QTextOption option = text.document()->defaultTextOption();
option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
text.document()->setDefaultTextOption(option);
QGraphicsTextItem word(QStringLiteral("Motorschutzschalter"));
QVERIFY(qAbs(TextResize::minimumWidth(text.document()) - word.boundingRect().width()) < 0.5);
}
// pinCorner() puts a corner where it was, for each corner, rotation,
// rotation point and inside a rotated parent.
void pinCornerKeepsTheCorner()
{
QGraphicsRectItem parent(0, 0, 10, 10);
parent.setRotation(30);
for (bool with_parent : {false, true})
for (int c = TextResize::TopLeft ; c <= TextResize::BottomLeft ; ++c)
for (qreal angle : {0.0, 90.0, 37.0})
for (bool centre : {false, true}) {
WidthText text(QStringLiteral("Motor protection switch"));
if (with_parent)
text.setParentItem(&parent);
text.setRotation(angle);
text.setTransformOriginPoint(centre ? text.boundingRect().center() : QPointF());
const Corner corner = Corner(c);
const QPointF before = sceneCorner(text, corner);
text.setWidth(70);
if (centre)
text.setTransformOriginPoint(text.boundingRect().center());
TextResize::pinCorner(&text, corner, before);
QVERIFY2(samePoint(sceneCorner(text, corner), before),
qPrintable(QStringLiteral("corner %1 angle %2 centre %3 parent %4")
.arg(c).arg(angle).arg(centre).arg(with_parent)));
text.setParentItem(nullptr);
}
}
// Dragging a handle keeps the opposite corner in place, wraps the text
// and reports the change once, on release.
void dragKeepsTheOppositeCorner()
{
for (int c = TextResize::TopLeft ; c <= TextResize::BottomLeft ; ++c)
for (qreal angle : {0.0, 90.0, 37.0}) {
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
scene.addItem(text);
text->setPos(100, 100);
text->setRotation(angle);
auto *handles = new TextResizeHandles(text);
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
const Corner corner = Corner(c);
const QPointF fixed = sceneCorner(*text, TextResize::opposite(corner));
const qreal natural = text->boundingRect().width();
const QPointF start_pos = text->pos();
//Narrower by 40 along the text, whatever its rotation;
//a vertical move of the mouse is ignored.
QTransform turn;
turn.rotate(angle);
const qreal sign = TextResize::isLeft(corner) ? 1 : -1;
drag(scene, *handles, *text, corner, turn.map(QPointF(sign * 40, 25)));
const QString what = QStringLiteral("corner %1 angle %2").arg(c).arg(angle);
QCOMPARE(text->textWidth(), qreal(qRound(natural - 40)));
QVERIFY2(lineCount(*text) > 1, qPrintable(what));
QVERIFY2(samePoint(sceneCorner(*text, TextResize::opposite(corner)), fixed), qPrintable(what));
QCOMPARE(finished.count(), 1);
const QList<QVariant> args = finished.first();
QCOMPARE(args.at(0).toReal(), qreal(-1));
QCOMPARE(args.at(1).toReal(), text->textWidth());
QCOMPARE(args.at(2).toPointF(), start_pos);
QCOMPARE(args.at(3).toPointF(), text->pos());
//The handles follow the new box
QVERIFY(samePoint(handles->handle(corner)->scenePos(), sceneCorner(*text, corner)));
}
}
// The handles still work after the text left its scene and came back,
// as undoing a delete does: Qt drops scene event filters on removal.
void dragAfterLeavingTheScene()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
scene.addItem(text);
auto *handles = new TextResizeHandles(text);
scene.removeItem(text);
scene.addItem(text);
const QPointF start_pos = text->pos();
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-40, 0));
QVERIFY(text->textWidth() > 0);
QCOMPARE(text->pos(), start_pos);
delete text;
}
// The handles also work when they are created before the text is in a
// scene.
void dragWhenCreatedOutsideAScene()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
auto *handles = new TextResizeHandles(text);
scene.addItem(text);
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-40, 0));
QVERIFY(text->textWidth() > 0);
}
// A text is never made narrower than its longest word.
void dragStopsAtTheLongestWord()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("a Motorschutzschalter b"));
scene.addItem(text);
auto *handles = new TextResizeHandles(text);
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-1000, 0));
QCOMPARE(text->textWidth(), std::ceil(TextResize::minimumWidth(text->document())));
}
// A press and release without moving changes nothing.
void clickChangesNothing()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch"));
scene.addItem(text);
auto *handles = new TextResizeHandles(text);
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
drag(scene, *handles, *text, TextResize::TopRight, QPointF());
QCOMPARE(finished.count(), 0);
QCOMPARE(text->textWidth(), qreal(-1));
}
// Double-clicking a handle goes back to the automatic width.
void doubleClickResetsToAutomaticWidth()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch"));
scene.addItem(text);
text->setWidth(60);
auto *handles = new TextResizeHandles(text);
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
sendMouse(scene, *handles, TextResize::BottomLeft, QEvent::GraphicsSceneMouseDoubleClick,
sceneCorner(*text, TextResize::BottomLeft));
QCOMPARE(text->textWidth(), qreal(-1));
QCOMPARE(finished.count(), 1);
QCOMPARE(finished.first().at(0).toReal(), qreal(60));
QCOMPARE(finished.first().at(1).toReal(), qreal(-1));
//Already automatic: nothing to undo
sendMouse(scene, *handles, TextResize::BottomLeft, QEvent::GraphicsSceneMouseDoubleClick,
sceneCorner(*text, TextResize::BottomLeft));
QCOMPARE(finished.count(), 1);
}
// One undo step gives back the width and the position, one redo
// applies them again.
void undoRestoresWidthAndPosition()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
scene.addItem(text);
text->setPos(100, 100);
text->setRotation(90);
auto *handles = new TextResizeHandles(text);
QUndoStack stack;
connect(handles, &TextResizeHandles::resizeFinished, &stack,
[&stack, text](qreal ow, qreal nw, QPointF op, QPointF np) {
stack.push(new TextResizeCommand(text, ow, nw, op, np));
});
drag(scene, *handles, *text, TextResize::TopLeft, QPointF(0, 40));
QCOMPARE(stack.count(), 1);
const qreal new_width = text->textWidth();
const QPointF new_pos = text->pos();
QVERIFY(new_pos != QPointF(100, 100));
stack.undo();
QCOMPARE(text->textWidth(), qreal(-1));
QCOMPARE(text->pos(), QPointF(100, 100));
stack.redo();
QCOMPARE(text->textWidth(), new_width);
QCOMPARE(text->pos(), new_pos);
}
};
QTEST_MAIN(tst_textresizehandles)
#include "tst_textresizehandles.moc"
+146
View File
@@ -0,0 +1,146 @@
/*
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 "toolbarsettings.h"
#include "ui/configpage/toolbarsconfigpage.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDockWidget>
#include <QMainWindow>
#include <QSettings>
#include <QStandardPaths>
#include <QTest>
#include <QToolBar>
/**
ToolbarSettings and its configuration page: defaults change nothing,
saved values reach every open window that took them, and a toolbar
inside a panel or a window that never took them is left alone.
*/
class tst_toolbarsettings : public QObject
{
Q_OBJECT
QMainWindow *m_window = nullptr;
QToolBar *m_top = nullptr;
QToolBar *m_left = nullptr;
QToolBar *m_in_dock = nullptr;
QMainWindow *m_other = nullptr;
QToolBar *m_other_bar = nullptr;
QSize m_default_size;
private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
QSettings().remove(QStringLiteral("toolbars"));
m_window = new QMainWindow();
m_default_size = m_window->iconSize();
m_top = new QToolBar(QStringLiteral("top"), m_window);
m_window->addToolBar(Qt::TopToolBarArea, m_top);
//Created without a parent, as some QET toolbars are
m_left = new QToolBar(QStringLiteral("left"));
m_window->addToolBar(Qt::LeftToolBarArea, m_left);
auto *dock = new QDockWidget(m_window);
m_in_dock = new QToolBar(dock);
dock->setWidget(m_in_dock);
m_window->addDockWidget(Qt::LeftDockWidgetArea, dock);
m_window->show();
//A window that never takes the settings, like the print window
m_other = new QMainWindow();
m_other_bar = new QToolBar(QStringLiteral("other"), m_other);
m_other->addToolBar(Qt::TopToolBarArea, m_other_bar);
m_other->show();
}
void cleanupTestCase()
{
delete m_window;
delete m_other;
}
void defaultsChangeNothing()
{
QCOMPARE(ToolbarSettings::iconSize(), 0);
QCOMPARE(ToolbarSettings::buttonStyle(), Qt::ToolButtonIconOnly);
QCOMPARE(ToolbarSettings::locked(), false);
ToolbarSettings::applyTo(m_window);
QCOMPARE(m_window->iconSize(), m_default_size);
QCOMPARE(m_top->iconSize(), m_default_size);
QCOMPARE(m_top->toolButtonStyle(), Qt::ToolButtonIconOnly);
QVERIFY(m_top->isMovable());
QVERIFY(m_left->isMovable());
}
void pageSavesAndAppliesToOpenWindows()
{
{
ToolbarsConfigPage page;
auto *size = page.findChild<QComboBox *>(QStringLiteral("iconSizeCombo"));
auto *style = page.findChild<QComboBox *>(QStringLiteral("buttonStyleCombo"));
size->setCurrentIndex(size->findData(32));
style->setCurrentIndex(style->findData(int(Qt::ToolButtonTextUnderIcon)));
page.findChild<QCheckBox *>(QStringLiteral("lockedCheck"))->setChecked(true);
page.applyConf();
}
QCOMPARE(ToolbarSettings::iconSize(), 32);
QCOMPARE(ToolbarSettings::buttonStyle(), Qt::ToolButtonTextUnderIcon);
QVERIFY(ToolbarSettings::locked());
QCOMPARE(m_top->iconSize(), QSize(32, 32));
QCOMPARE(m_left->iconSize(), QSize(32, 32));
QCOMPARE(m_top->toolButtonStyle(), Qt::ToolButtonTextUnderIcon);
QVERIFY(!m_top->isMovable());
QVERIFY(!m_left->isMovable());
//A toolbar inside a panel is not one of the window's toolbars
QVERIFY(m_in_dock->isMovable());
//Nor is a window that did not take the settings when it opened
QCOMPARE(m_other_bar->iconSize(), m_default_size);
QCOMPARE(m_other_bar->toolButtonStyle(), Qt::ToolButtonIconOnly);
QVERIFY(m_other_bar->isMovable());
//A page opened now shows what was saved
ToolbarsConfigPage page;
QCOMPARE(page.findChild<QComboBox *>(QStringLiteral("iconSizeCombo"))->currentData().toInt(), 32);
QVERIFY(page.findChild<QCheckBox *>(QStringLiteral("lockedCheck"))->isChecked());
}
void backToDefaultsRemovesTheKeys()
{
ToolbarSettings::save(0, Qt::ToolButtonIconOnly, false);
QVERIFY(!QSettings().contains(QStringLiteral("toolbars/icon_size")));
QVERIFY(!QSettings().contains(QStringLiteral("toolbars/button_style")));
QVERIFY(!QSettings().contains(QStringLiteral("toolbars/locked")));
ToolbarSettings::applyToAll();
QCOMPARE(m_top->iconSize(), m_default_size);
QCOMPARE(m_top->toolButtonStyle(), Qt::ToolButtonIconOnly);
QVERIFY(m_top->isMovable());
}
void unknownStyleFallsBack()
{
QSettings().setValue(QStringLiteral("toolbars/button_style"), 99);
QCOMPARE(ToolbarSettings::buttonStyle(), Qt::ToolButtonIconOnly);
QSettings().remove(QStringLiteral("toolbars/button_style"));
}
};
QTEST_MAIN(tst_toolbarsettings)
#include "tst_toolbarsettings.moc"
+121
View File
@@ -0,0 +1,121 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTemporaryDir>
// qet.addElement() compares the uuid of the symbol it is given with the copy
// already embedded under the same name. A symbol file that exists but cannot
// be read has no uuid, and was reported as "would collide with a different
// element" (#1178 follow-up: on Windows, any symbol whose full path reaches
// 260 characters). Made unreadable here with the file's permissions, which
// gives the same null uuid on Linux.
class tst_unreadableelement : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
QString m_collection;
QString m_symbol;
// Runs a script placing common://custom/my_siren.elmt into project and
// saving it to output; returns everything the run printed.
QString placeTheSymbol(const QString &project, const QString &output)
{
const QString root = m_dir.path();
const QString home = root + QStringLiteral("/home");
const QString settings = root + QStringLiteral("/settings");
QDir().mkpath(settings + QStringLiteral("/QElectroTech"));
QDir().mkpath(root + QStringLiteral("/tmp"));
QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini"));
if (!ini.open(QIODevice::WriteOnly | QIODevice::Text))
return QString();
ini.write("[elements-collections]\ncommon-collection-path=");
ini.write(m_collection.toUtf8());
ini.write("\n");
ini.close();
const QString script = root + QStringLiteral("/add.js");
QFile js(script);
if (!js.open(QIODevice::WriteOnly))
return QString();
js.write("var u = qet.addElement(0, 'common://custom/my_siren.elmt', 100, 100);\n"
"qet.log('RESULT ' + (u ? 'placed' : 'refused'));\n"
"if (u) qet.save('");
js.write(output.toUtf8());
js.write("');\n");
js.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share"));
env.insert(QStringLiteral("TMPDIR"), root + QStringLiteral("/tmp"));
env.insert(QStringLiteral("QET_SETTINGS_DIR"), settings);
QProcess proc;
proc.setProcessEnvironment(env);
proc.setProcessChannelMode(QProcess::MergedChannels);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script, project});
if (!proc.waitForFinished(60000))
return QString();
return QString::fromUtf8(proc.readAll());
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
m_collection = m_dir.filePath(QStringLiteral("collection"));
m_symbol = m_collection + QStringLiteral("/custom/my_siren.elmt");
QVERIFY(QDir().mkpath(m_collection + QStringLiteral("/custom")));
QVERIFY(QFile::copy(QStringLiteral(QET_ELEMENTS_DIR "/10_electric/10_allpole/380_signaling_operating/12_acoustic_signaling/sirene.elmt"),
m_symbol));
QVERIFY(QFile::copy(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"),
m_dir.filePath(QStringLiteral("p.qet"))));
}
void anUnreadableSymbolIsNotACollision()
{
// Placed once while readable: the project now embeds it.
const QString embedded = m_dir.filePath(QStringLiteral("embedded.qet"));
QVERIFY(placeTheSymbol(m_dir.filePath(QStringLiteral("p.qet")), embedded)
.contains(QStringLiteral("RESULT placed")));
QVERIFY(QFile::exists(embedded));
#ifdef Q_OS_WIN
// Qt cannot take read permission away on Windows: it only knows the
// read-only flag there, so this returns false. The check needs a
// file the user cannot read, which Windows cannot give it.
QSKIP("Windows cannot make the file unreadable through QFile");
#endif
QVERIFY(QFile::setPermissions(m_symbol, QFileDevice::Permissions()));
QFile probe(m_symbol);
if (probe.open(QIODevice::ReadOnly))
QSKIP("permissions do not stop this user reading the file (root?)");
const QString out = placeTheSymbol(embedded, m_dir.filePath(QStringLiteral("again.qet")));
QFile::setPermissions(m_symbol, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
QVERIFY2(out.contains(QStringLiteral("RESULT refused")), qPrintable(out));
QVERIFY2(out.contains(QStringLiteral("could not read element")), qPrintable(out));
QVERIFY2(!out.contains(QStringLiteral("would collide")), qPrintable(out));
}
// Readable again, the same symbol is placed a second time as before.
void theSameSymbolIsPlacedAgain()
{
const QString out = placeTheSymbol(m_dir.filePath(QStringLiteral("embedded.qet")),
m_dir.filePath(QStringLiteral("twice.qet")));
QVERIFY2(out.contains(QStringLiteral("RESULT placed")), qPrintable(out));
}
};
QTEST_APPLESS_MAIN(tst_unreadableelement)
#include "tst_unreadableelement.moc"
+138
View File
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTemporaryDir>
// A project can keep the texts drawn in its turned symbols horizontal
// (<symbol_texts upright="true"/>, Project properties). Through the real
// binary on tremie_vibrante.qet: the symbols of the first folio are turned
// by 90 degrees, then the folio is exported to DXF with the setting off and
// on. The "M" of each motor turns with its symbol when it is off, and stays
// horizontal when it is on.
class tst_uprightsymboltexts : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
QProcessEnvironment environment()
{
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
return env;
}
bool runQet(const QStringList &arguments)
{
QProcess proc;
proc.setProcessEnvironment(environment());
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
return proc.waitForFinished(120000) && proc.exitCode() == 0;
}
// The rotation (DXF group code 50) of every TEXT entity of @p dxf whose
// text (group code 1) is @p text
static QList<double> textRotations(const QString &dxf, const QString &text)
{
QFile file(dxf);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
const QStringList lines = QString::fromUtf8(file.readAll())
.split(QLatin1Char('\n'));
QList<double> rotations;
bool in_text = false, match = false;
double rotation = 0;
for (int i = 0; i + 1 < lines.size(); i += 2) {
const QString code = lines.at(i).trimmed();
const QString value = lines.at(i + 1).trimmed();
if (code == QLatin1String("0")) {
if (in_text && match) rotations << rotation;
in_text = value == QLatin1String("TEXT");
match = false;
rotation = 0;
} else if (in_text && code == QLatin1String("1")) {
match = value == text;
} else if (in_text && code == QLatin1String("50")) {
rotation = value.toDouble();
}
}
return rotations;
}
// The DXF of the first folio of @p project, exported into @p name
QString exportDxf(const QString &project, const QString &name)
{
const QString out = m_dir.filePath(name);
QDir().mkpath(out);
if (!runQet({QStringLiteral("--export-dxf"), project, out})) return {};
const QStringList files = QDir(out).entryList({QStringLiteral("01*.dxf")});
return files.isEmpty() ? QString() : QDir(out).filePath(files.first());
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void motorTextStaysHorizontal()
{
//The symbols of the first folio, turned by 90 degrees
const QString off = m_dir.filePath(QStringLiteral("off.qet"));
const QString script = m_dir.filePath(QStringLiteral("turn.js"));
QFile js(script);
QVERIFY(js.open(QIODevice::WriteOnly));
js.write(QStringLiteral(
"var els = qet.elementUuids(0);\n"
"for (var e = 0; e < els.length; e++) qet.rotateElement(0, els[e], 90);\n"
"qet.save(%1);\n").arg(QLatin1Char('"') + off + QLatin1Char('"')).toUtf8());
js.close();
QVERIFY(runQet({QStringLiteral("--run"), script,
QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet")}));
//An existing project is read with the setting off, and saved so
QFile off_file(off);
QVERIFY(off_file.open(QIODevice::ReadOnly));
QString xml = QString::fromUtf8(off_file.readAll());
off_file.close();
QVERIFY(!xml.contains(QStringLiteral("symbol_texts")));
//The same project with the setting on
const QString on = m_dir.filePath(QStringLiteral("on.qet"));
const int root_end = xml.indexOf(QLatin1Char('>'), xml.indexOf(QStringLiteral("<project"))) + 1;
QVERIFY(root_end > 0);
xml.insert(root_end, QStringLiteral("\n<symbol_texts upright=\"true\"/>"));
QFile on_file(on);
QVERIFY(on_file.open(QIODevice::WriteOnly));
on_file.write(xml.toUtf8());
on_file.close();
const QList<double> turned = textRotations(exportDxf(off, QStringLiteral("dxf-off")),
QStringLiteral("M"));
const QList<double> upright = textRotations(exportDxf(on, QStringLiteral("dxf-on")),
QStringLiteral("M"));
//The two motors of the folio
QCOMPARE(turned.size(), 2);
QCOMPARE(upright.size(), 2);
for (double rotation : turned)
QCOMPARE(std::fmod(rotation, 360.0), 270.0);
for (double rotation : upright)
QCOMPARE(std::fmod(rotation, 360.0), 0.0);
}
};
QTEST_APPLESS_MAIN(tst_uprightsymboltexts)
#include "tst_uprightsymboltexts.moc"
+388
View File
@@ -0,0 +1,388 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDir>
#include <QDomDocument>
#include <QFile>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTemporaryDir>
#include "wiringrules.h"
// The checks that drive QElectroTech through a script (--run) need a
// build with scripting; without it --run is not an option and the
// process never exits (QET_HAS_SCRIPTING, top-level CMakeLists.txt).
#ifdef QET_HAS_SCRIPTING
# define SKIP_WITHOUT_SCRIPTING
#else
# define SKIP_WITHOUT_SCRIPTING QSKIP("built without scripting: --run is not available")
#endif
// How many wires a terminal may take (discussion #1158). The rules are
// tested on their own; the project setting and the refusal are tested
// through the real binary: --resave keeps a <wiring_rules> element and
// adds none to a project without it, and qet.addConductor() is refused
// past the limit unless the master switch is off.
class tst_wiringrules : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
int m_run = 0;
// Environment of one sandboxed run; @p master_off writes the master
// switch off into that run's own settings, @p application_max_wires
// the limit every project follows unless it sets its own.
QProcessEnvironment sandbox(bool master_off = false, int application_max_wires = 0)
{
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run));
const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(m_run));
const QString settings = m_dir.filePath(QStringLiteral("settings%1").arg(m_run));
++m_run;
QDir().mkpath(home);
QDir().mkpath(tmp);
QDir().mkpath(settings + QStringLiteral("/QElectroTech"));
if (master_off || application_max_wires) {
QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini"));
if (ini.open(QIODevice::WriteOnly | QIODevice::Text))
ini.write(QStringLiteral("[diagrameditor]\nwiring_rules_enabled=%1\nwiring_rules_max_wires=%2\n")
.arg(master_off ? QStringLiteral("false") : QStringLiteral("true"))
.arg(application_max_wires).toUtf8());
}
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share"));
env.insert(QStringLiteral("QET_SETTINGS_DIR"), settings);
env.insert(QStringLiteral("TMPDIR"), tmp);
return env;
}
// The fixture with @p rules inserted before <newdiagrams>, as a new file
QString fixtureWith(const QString &rules)
{
QFile source(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"));
if (!source.open(QIODevice::ReadOnly | QIODevice::Text))
return {};
QString text = QString::fromUtf8(source.readAll());
const int newdiagrams = text.indexOf(QLatin1String("<newdiagrams"));
if (newdiagrams < 0)
return {};
text.insert(newdiagrams, rules + QStringLiteral("\n "));
const QString path = m_dir.filePath(QStringLiteral("fixture%1.qet").arg(m_run));
QFile out(path);
if (!out.open(QIODevice::WriteOnly | QIODevice::Text))
return {};
out.write(text.toUtf8());
return path;
}
// Runs --resave on @p in, returns the saved file's text (empty on failure)
QString resave(const QString &in)
{
const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
QProcess proc;
proc.setProcessEnvironment(sandbox());
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--resave"), in, out});
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
return {};
QFile file(out);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return {};
return QString::fromUtf8(file.readAll());
}
// Wires a free terminal to a terminal that already has a wire, through
// qet.addConductor(); returns what the script printed after "PROBE ".
QString addWireToWiredTerminal(const QString &project, bool master_off,
int application_max_wires = 0)
{
const QString script_path = m_dir.filePath(QStringLiteral("probe%1.js").arg(m_run));
QFile script(script_path);
if (!script.open(QIODevice::WriteOnly))
return {};
// The first end of the first conductor is a terminal with a wire.
// Any terminal of another element with no wire is the other end.
script.write(
"var end = qet.conductorEnds(0, qet.conductorUuids(0)[0])[0].split(' terminal ');\n"
"var wired = {};\n"
"qet.conductorUuids(0).forEach(function (u) {\n"
" qet.conductorEnds(0, u).forEach(function (e) { wired[e] = true; }); });\n"
"var free = null;\n"
"qet.elementUuids(0).forEach(function (el) {\n"
" if (free || el == end[0]) return;\n"
" var n = qet.elementTerminals(0, el).length;\n"
" for (var i = 0; i < n && !free; ++i)\n"
" if (!wired[el + ' terminal ' + i]) free = [el, i]; });\n"
"qet.log('PROBE ' + (free ? qet.addConductor(0, end[0], parseInt(end[1]), free[0], free[1])\n"
" : 'nofree'));\n");
script.close();
QProcess proc;
proc.setProcessEnvironment(sandbox(master_off, application_max_wires));
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script_path, project});
if (!proc.waitForFinished(60000))
return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 0)
return line.mid(i + mark.size()).trimmed();
}
return {};
}
// On a fixture with @p rules: a symbol wired to four others is
// deleted, so QElectroTech rewires the four to keep the potential.
// Returns the most wires any one of them ends with.
QString mostWiresAfterDeletingTheHub(const QString &rules)
{
const QString project = fixtureWith(rules);
const QString script_path = m_dir.filePath(QStringLiteral("hub%1.js").arg(m_run));
QFile script(script_path);
if (project.isEmpty() || !script.open(QIODevice::WriteOnly))
return {};
// Placed on a diagonal, so no two terminals line up and nothing
// is auto-connected.
script.write(
"var p = 'embed://import/probe/v2_fuse.elmt';\n"
"var hub = qet.addElement(0, p, 400, 400);\n"
"var others = [];\n"
"for (var i = 1; i <= 4; ++i) others.push(qet.addElement(0, p, 400 + 70 * i, 400 + 90 * i));\n"
"others.forEach(function (o) { qet.addConductor(0, hub, 0, o, 0); });\n"
"qet.deleteElement(0, hub);\n"
"var count = {};\n"
"qet.conductorUuids(0).forEach(function (u) {\n"
" qet.conductorEnds(0, u).forEach(function (e) { count[e] = (count[e] || 0) + 1; }); });\n"
"var most = 0;\n"
"others.forEach(function (o) { most = Math.max(most, count[o + ' terminal 0'] || 0); });\n"
"qet.log('PROBE ' + most);\n");
script.close();
QProcess proc;
proc.setProcessEnvironment(sandbox());
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script_path, project});
if (!proc.waitForFinished(60000))
return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 0)
return line.mid(i + mark.size()).trimmed();
}
return {};
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void limit()
{
WiringRules::Settings rules;
QCOMPARE(WiringRules::limit(rules, true, false), 0);
QCOMPARE(WiringRules::limit(rules, true, true), 0);
rules.max_wires = 4;
QCOMPARE(WiringRules::limit(rules, true, false), 4);
QCOMPARE(WiringRules::limit(rules, true, true), 4);
rules.one_wire_per_report = true;
QCOMPARE(WiringRules::limit(rules, true, true), 1);
QCOMPARE(WiringRules::limit(rules, true, false), 4);
// The master switch turns every rule off
QCOMPARE(WiringRules::limit(rules, false, false), 0);
QCOMPARE(WiringRules::limit(rules, false, true), 0);
}
void hasRoom()
{
QVERIFY(WiringRules::hasRoom(0, 0));
QVERIFY(WiringRules::hasRoom(0, 19));
QVERIFY(WiringRules::hasRoom(2, 1));
QVERIFY(!WiringRules::hasRoom(2, 2));
QVERIFY(!WiringRules::hasRoom(2, 3));
QVERIFY(!WiringRules::hasRoom(1, 1));
}
void chainsOnlyUnderALimit()
{
WiringRules::Settings rules;
QVERIFY(!WiringRules::chainsWires(rules, true));
rules.one_wire_per_report = true;
QVERIFY(!WiringRules::chainsWires(rules, true));
rules.max_wires = 2;
QVERIFY(WiringRules::chainsWires(rules, true));
QVERIFY(!WiringRules::chainsWires(rules, false));
}
void chainOrder()
{
QCOMPARE(WiringRules::chainOrder({}), QList<int>());
QCOMPARE(WiringRules::chainOrder({QPointF(5, 5)}), QList<int>({0}));
// Starts top left, then always the nearest along the grid
const QList<QPointF> row {QPointF(300, 0), QPointF(0, 0), QPointF(200, 0), QPointF(100, 0)};
QCOMPARE(WiringRules::chainOrder(row), QList<int>({1, 3, 2, 0}));
// Same x: the higher one starts
const QList<QPointF> column {QPointF(0, 100), QPointF(0, 0), QPointF(0, 50)};
QCOMPARE(WiringRules::chainOrder(column), QList<int>({1, 2, 0}));
// Each index exactly once
const QList<QPointF> scattered {QPointF(40, 90), QPointF(10, 300), QPointF(220, 10),
QPointF(10, 10), QPointF(220, 300)};
QList<int> order = WiringRules::chainOrder(scattered);
QCOMPARE(order.first(), 3);
std::sort(order.begin(), order.end());
QCOMPARE(order, QList<int>({0, 1, 2, 3, 4}));
}
void projectOverridesApplication()
{
WiringRules::Settings application;
application.max_wires = 2;
application.one_wire_per_report = true;
// A project that follows the application gets its rules
WiringRules::Settings follows;
QCOMPARE(WiringRules::effective(follows, application).max_wires, 2);
QVERIFY(WiringRules::effective(follows, application).one_wire_per_report);
QVERIFY(!WiringRules::effective(follows, application).own);
// Its own rules win, "no limit" included
WiringRules::Settings own;
own.own = true;
QCOMPARE(WiringRules::effective(own, application).max_wires, 0);
QVERIFY(!WiringRules::effective(own, application).one_wire_per_report);
QVERIFY(WiringRules::effective(own, application).own);
own.max_wires = 6;
QCOMPARE(WiringRules::effective(own, application).max_wires, 6);
}
void warnsWhenARuleIsTurnedOn()
{
WiringRules::Settings off;
WiringRules::Settings limit;
limit.max_wires = 4;
WiringRules::Settings report;
report.one_wire_per_report = true;
QVERIFY(WiringRules::turnsRuleOn(off, limit));
QVERIFY(WiringRules::turnsRuleOn(off, report));
QVERIFY(WiringRules::turnsRuleOn(limit, [&]{ auto s = limit; s.one_wire_per_report = true; return s; }()));
// Changing a limit that was already on, or turning rules off: no warning
QVERIFY(!WiringRules::turnsRuleOn(limit, [&]{ auto s = limit; s.max_wires = 2; return s; }()));
QVERIFY(!WiringRules::turnsRuleOn(limit, off));
QVERIFY(!WiringRules::turnsRuleOn(off, off));
}
void xmlRoundTrip()
{
QDomDocument doc;
QDomElement root = doc.createElement(QStringLiteral("project"));
doc.appendChild(root);
// Following the application: nothing written, and reading
// nothing follows the application
WiringRules::Settings follows;
follows.max_wires = 3; // not the project's: not written
WiringRules::toXml(follows, root);
QVERIFY(root.firstChildElement().isNull());
QVERIFY(WiringRules::fromXml(root).isDefault());
// Its own rules, even "none", are written and read back as its own
WiringRules::Settings none;
none.own = true;
WiringRules::toXml(none, root);
QCOMPARE(root.firstChildElement().tagName(), QStringLiteral("wiring_rules"));
QVERIFY(WiringRules::fromXml(root) == none);
root.removeChild(root.firstChildElement());
WiringRules::Settings rules;
rules.own = true;
rules.max_wires = 2;
rules.one_wire_per_report = true;
WiringRules::toXml(rules, root);
QCOMPARE(root.firstChildElement().tagName(), QStringLiteral("wiring_rules"));
QVERIFY(WiringRules::fromXml(root) == rules);
}
void savedSettingSurvivesResave()
{
const QString plain = resave(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"));
QVERIFY2(!plain.isEmpty(), "--resave failed");
QVERIFY(!plain.contains(QLatin1String("wiring_rules")));
const QString with_rules = fixtureWith(
QStringLiteral("<wiring_rules max_wires_per_terminal=\"2\" one_wire_per_report=\"true\"/>"));
QVERIFY(!with_rules.isEmpty());
const QString saved = resave(with_rules);
QVERIFY2(!saved.isEmpty(), "--resave failed");
QVERIFY2(saved.contains(QLatin1String("max_wires_per_terminal=\"2\""))
&& saved.contains(QLatin1String("one_wire_per_report=\"true\"")),
"the setting was lost on save");
// A project's own "no rule" is kept too: it overrides the application's
const QString own_none = fixtureWith(QStringLiteral("<wiring_rules/>"));
QVERIFY(!own_none.isEmpty());
const QString saved_none = resave(own_none);
QVERIFY2(!saved_none.isEmpty(), "--resave failed");
QVERIFY2(saved_none.contains(QLatin1String("<wiring_rules/>")),
"a project's own rules were lost on save");
}
void wirePastTheLimitIsRefused()
{
SKIP_WITHOUT_SCRIPTING;
// Without a rule the wire is drawn, as on master
QCOMPARE(addWireToWiredTerminal(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"), false),
QStringLiteral("true"));
// One wire per terminal: the terminal is full
const QString limited = fixtureWith(
QStringLiteral("<wiring_rules max_wires_per_terminal=\"1\"/>"));
QVERIFY(!limited.isEmpty());
QCOMPARE(addWireToWiredTerminal(limited, false), QStringLiteral("false"));
// The master switch off: the project's rule does nothing
QCOMPARE(addWireToWiredTerminal(limited, true), QStringLiteral("true"));
// The application's limit applies to a project that sets none...
const QString plain = QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet");
QCOMPARE(addWireToWiredTerminal(plain, false, 1), QStringLiteral("false"));
// ...not to one that sets its own, here "no limit"
const QString own_none = fixtureWith(QStringLiteral("<wiring_rules/>"));
QVERIFY(!own_none.isEmpty());
QCOMPARE(addWireToWiredTerminal(own_none, false, 1), QStringLiteral("true"));
// ...and the master switch still turns it off
QCOMPARE(addWireToWiredTerminal(plain, true, 1), QStringLiteral("true"));
}
void deletingASymbolChainsTheWires()
{
SKIP_WITHOUT_SCRIPTING;
// No rule: the four are wired to one of them, as on master
QCOMPARE(mostWiresAfterDeletingTheHub(QString()), QStringLiteral("3"));
// A limit: one after another, none gets more than two
QCOMPARE(mostWiresAfterDeletingTheHub(
QStringLiteral("<wiring_rules max_wires_per_terminal=\"4\"/>")),
QStringLiteral("2"));
}
};
QTEST_GUILESS_MAIN(tst_wiringrules)
#include "tst_wiringrules.moc"
+94
View File
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include "properties/xrefproperties.h"
/**
XRefProperties reads the cross-reference position from a project's
<xref xrefpos="..."> and from the settings file. Older versions saved an
empty value; read as-is it drew the cross-reference over the element's
label (issue #1238). An empty or unknown value must fall back to the
default, AlignBottom, and a valid one must be kept.
*/
class tst_xrefpos : public QObject
{
Q_OBJECT
static Qt::AlignmentFlag fromXml(const QString &attribute, bool present = true)
{
QDomDocument doc;
QDomElement e = doc.createElement("xref");
e.setAttribute("type", "protection");
if (present)
e.setAttribute("xrefpos", attribute);
XRefProperties xrp;
xrp.fromXml(e);
return xrp.getXrefPos();
}
private slots:
void xml_data()
{
QTest::addColumn<QString>("stored");
QTest::addColumn<int>("expected");
QTest::newRow("empty") << "" << int(Qt::AlignBottom);
QTest::newRow("garbage") << "Sideways" << int(Qt::AlignBottom);
QTest::newRow("not offered") << "AlignJustify" << int(Qt::AlignBottom);
QTest::newRow("bottom") << "AlignBottom" << int(Qt::AlignBottom);
QTest::newRow("top") << "AlignTop" << int(Qt::AlignTop);
QTest::newRow("left") << "AlignLeft" << int(Qt::AlignLeft);
QTest::newRow("right") << "AlignRight" << int(Qt::AlignRight);
QTest::newRow("baseline") << "AlignBaseline" << int(Qt::AlignBaseline);
QTest::newRow("hcenter") << "AlignHCenter" << int(Qt::AlignHCenter);
}
void xml()
{
QFETCH(QString, stored);
QFETCH(int, expected);
QCOMPARE(int(fromXml(stored)), expected);
}
void xmlMissing()
{
QCOMPARE(fromXml(QString(), false), Qt::AlignBottom);
}
/**
An empty value is written back as "AlignBottom" on the next save,
so the file heals instead of carrying the empty value forward.
*/
void emptyIsRewritten()
{
QDomDocument doc;
QDomElement e = doc.createElement("xref");
e.setAttribute("xrefpos", "");
XRefProperties xrp;
xrp.fromXml(e);
QCOMPARE(xrp.toXml(doc).attribute("xrefpos"), QStringLiteral("AlignBottom"));
}
void settings()
{
QTemporaryDir dir;
QVERIFY(dir.isValid());
QSettings s(dir.filePath("qet.ini"), QSettings::IniFormat);
XRefProperties xrp;
s.setValue("xrefprotectionxrefpos", "");
xrp.fromSettings(s, "xrefprotection");
QCOMPARE(xrp.getXrefPos(), Qt::AlignBottom);
s.remove("xrefprotectionxrefpos");
xrp.setXrefPos(Qt::AlignTop);
xrp.fromSettings(s, "xrefprotection");
QCOMPARE(xrp.getXrefPos(), Qt::AlignBottom);
s.setValue("xrefprotectionxrefpos", "AlignRight");
xrp.fromSettings(s, "xrefprotection");
QCOMPARE(xrp.getXrefPos(), Qt::AlignRight);
}
};
QTEST_GUILESS_MAIN(tst_xrefpos)
#include "tst_xrefpos.moc"