Merge remote-tracking branch 'origin/master' into c++20-server

Dieser Commit ist enthalten in:
Reinder Feenstra 2025-01-21 23:48:18 +01:00
Commit 989311e069
292 geänderte Dateien mit 14521 neuen und 20024 gelöschten Zeilen

Datei anzeigen

@ -1,11 +1,22 @@
---
Checks: >
-*,
misc-*,
-misc-no-recursion,
modernize-*,
-modernize-avoid-bind,
-modernize-avoid-c-arrays,
-modernize-use-trailing-return-type,
readability-*,
-readability-braces-around-statements,
-readability-implicit-bool-conversion,
-readability-identifier-length,
-readability-magic-numbers
-readability-magic-numbers,
-readability-suspicious-call-argument
WarningsAsErrors: >
readability-*,
-readability-function-cognitive-complexity
CheckOptions:
- {key: readability-identifier-naming.NamespaceCase, value: CamelCase}

Datei anzeigen

@ -20,16 +20,6 @@ jobs:
build_deb: false
defines: ""
- name: "ubuntu_22.04"
os: ubuntu-22.04
generator: "Unix Makefiles"
arch: ""
target: traintastic-client
jobs: 4
build_type: Release
build_deb: true
defines: ""
- name: "ubuntu_24.04"
os: ubuntu-24.04
generator: "Unix Makefiles"
@ -60,16 +50,6 @@ jobs:
build_deb: true
defines: "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache"
- name: "macos-12"
os: "macos-12"
generator: "Unix Makefiles"
arch: ""
target: traintastic-client
jobs: 3
build_type: Release
build_deb: false
defines: ""
- name: "macos-13"
os: "macos-13"
generator: "Unix Makefiles"
@ -80,16 +60,6 @@ jobs:
build_deb: false
defines: ""
- name: "macos-14"
os: "macos-14"
generator: "Unix Makefiles"
arch: ""
target: traintastic-client
jobs: 3
build_type: Release
build_deb: false
defines: ""
steps:
- uses: FranzDiebold/github-env-vars-action@v2
@ -112,8 +82,16 @@ jobs:
uses: jurplel/install-qt-action@v4
with:
cache: true
modules: qtwebsockets
version: 6.5.*
- name: Set CMAKE_OSX_ARCHITECTURES
if: startswith(matrix.config.os, 'macos')
uses: actions/github-script@v7
with:
script: |
core.exportVariable('CMAKE_OSX_ARCHITECTURES', 'x86_64');
# Ubuntu only:
- name: apt update
if: startswith(matrix.config.os, 'ubuntu')
@ -122,7 +100,7 @@ jobs:
# Ubuntu only:
- name: Install packages
if: startswith(matrix.config.os, 'ubuntu')
run: sudo apt install qtbase5-dev qtbase5-dev-tools libqt5svg5-dev
run: sudo apt install qtbase5-dev qtbase5-dev-tools libqt5svg5-dev libqt5websockets5-dev
# All:
- name: Create Build Environment
@ -157,22 +135,24 @@ jobs:
# Windows only:
- name: Upload artifact
if: startswith(matrix.config.os, 'windows')
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: traintastic-client
name: traintastic-client-windows
path: ${{github.workspace}}/client/build/${{matrix.config.build_type}}
# Linux only:
- name: Upload debian package artifact
if: matrix.config.build_deb
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: traintastic-client-deb
name: traintastic-client-deb-${{matrix.config.name}}
path: ${{github.workspace}}/client/build/*.deb
build-server:
name: server ${{matrix.config.name}}
runs-on: ${{matrix.config.os}}
env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
strategy:
fail-fast: false
matrix:
@ -186,33 +166,9 @@ jobs:
jobs: 4
build_type: Release
build_deb: false
defines: "-DENABLE_CLANG_TIDY=ON"
ccov: false
- name: "windows_x64_mingw"
os: windows-2022
generator: "MinGW Makefiles"
arch: ""
toolset: ""
target: all
jobs: 4
build_type: Release
build_deb: false
defines: ""
ccov: false
- name: "ubuntu_22.04"
os: ubuntu-22.04
generator: "Unix Makefiles"
arch: ""
toolset: ""
target: all
jobs: 4
build_type: Release
build_deb: true
defines: "-DENABLE_CLANG_TIDY=ON -DINSTALL_SYSTEMD_SERVICE=ON"
ccov: false
- name: "ubuntu_24.04"
os: ubuntu-24.04
generator: "Unix Makefiles"
@ -222,11 +178,11 @@ jobs:
jobs: 4
build_type: Release
build_deb: true
defines: "-DENABLE_CLANG_TIDY=ON -DINSTALL_SYSTEMD_SERVICE=ON"
defines: "-DINSTALL_SYSTEMD_SERVICE=ON"
ccov: false
- name: "ubuntu_latest (debug+ccov)"
os: ubuntu-latest
- name: "ubuntu_24.04 (debug+ccov)"
os: ubuntu-24.04
generator: "Unix Makefiles"
arch: ""
toolset: ""
@ -234,7 +190,7 @@ jobs:
jobs: 4
build_type: Debug
build_deb: false
defines: "-DENABLE_CLANG_TIDY=ON -DCODE_COVERAGE=ON"
defines: "-DCODE_COVERAGE=ON"
ccov: true
- name: "raspberrypios_arm7"
@ -261,18 +217,6 @@ jobs:
defines: "-DINSTALL_SYSTEMD_SERVICE=ON -DCMAKE_CXX_COMPILER_LAUNCHER=ccache"
ccov: false
- name: "macos-12"
os: "macos-12"
generator: "Unix Makefiles"
arch: ""
toolset: ""
target: all
jobs: 3
build_type: Release
build_deb: false
defines: ""
ccov: false
- name: "macos-13"
os: "macos-13"
generator: "Unix Makefiles"
@ -285,18 +229,6 @@ jobs:
defines: ""
ccov: false
- name: "macos-14"
os: "macos-14"
generator: "Unix Makefiles"
arch: ""
toolset: ""
target: all
jobs: 3
build_type: Release
build_deb: false
defines: ""
ccov: false
steps:
- uses: FranzDiebold/github-env-vars-action@v2
@ -306,6 +238,27 @@ jobs:
with:
submodules: recursive
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Set VCPKG_ROOT
if: startswith(matrix.config.os, 'windows') || startswith(matrix.config.os, 'macos')
uses: actions/github-script@v7
with:
script: |
core.exportVariable('VCPKG_ROOT', process.env.VCPKG_INSTALLATION_ROOT || '');
- name: Set CMAKE_OSX_ARCHITECTURES
if: startswith(matrix.config.os, 'macos')
uses: actions/github-script@v7
with:
script: |
core.exportVariable('CMAKE_OSX_ARCHITECTURES', 'x86_64');
# Ubuntu only:
- name: apt update
if: startswith(matrix.config.os, 'ubuntu')
@ -314,12 +267,12 @@ jobs:
# Ubuntu only:
- name: Install packages
if: startswith(matrix.config.os, 'ubuntu')
run: sudo apt install libboost-program-options-dev liblua5.3-dev lcov libarchive-dev clang-tidy libsystemd-dev
run: sudo apt install libboost-program-options-dev liblua5.4-dev lcov libarchive-dev clang-tidy libsystemd-dev
# MacOS only:
- name: Install brew packages
if: startswith(matrix.config.os, 'macos')
run: brew install libarchive lua@5.3
run: brew install libarchive lua@5.4
# All:
- name: Create Build Environment
@ -366,17 +319,17 @@ jobs:
# Windows only:
- name: Upload artifact
if: matrix.config.name == 'windows_x64_clang'
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: traintastic-server
name: traintastic-server-windows
path: ${{github.workspace}}/server/build/Release/traintastic-server.exe
# Linux only:
- name: Upload debian package artifact
if: matrix.config.build_deb
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: traintastic-server-deb
name: traintastic-server-deb-${{matrix.config.name}}
path: ${{github.workspace}}/server/build/*.deb
# Code coverage:
@ -395,7 +348,7 @@ jobs:
- name: "Code coverage: upload artifact"
if: matrix.config.ccov
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: traintastic-server-test-code-coverage
path: ${{github.workspace}}/server/build/ccov/*
@ -408,10 +361,6 @@ jobs:
fail-fast: false
matrix:
config:
- name: "ubuntu_22.04"
os: ubuntu-22.04
defines: ""
- name: "ubuntu_24.04"
os: ubuntu-24.04
defines: ""
@ -429,7 +378,7 @@ jobs:
submodules: recursive
- name: "Download artifact: lang"
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-lang
path: ${{github.workspace}}/shared/translations
@ -447,9 +396,9 @@ jobs:
run: cpack
- name: Upload debian package artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: traintastic-data-deb
name: traintastic-data-deb-${{matrix.config.name}}
path: ${{github.workspace}}/shared/build/*.deb
build-lang:
@ -469,7 +418,7 @@ jobs:
run: python3 json2lang.py
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: traintastic-lang
path: ${{github.workspace}}/shared/translations/*.lang
@ -494,7 +443,7 @@ jobs:
run: ./builddoc.py html-single-page --output-dir build
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: traintastic-manual
path: ${{github.workspace}}/manual/build/*
@ -516,7 +465,7 @@ jobs:
run: ./buildluadoc.py
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: traintastic-manual-lua
path: ${{github.workspace}}/manual/build.luadoc/*
@ -535,31 +484,31 @@ jobs:
submodules: recursive
- name: Download artifacts 1/2
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-client
name: traintastic-client-windows
path: ${{github.workspace}}/client/build/Release
- name: Download artifacts 2/2
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-server
name: traintastic-server-windows
path: ${{github.workspace}}/server/build
- name: "Download artifact: lang"
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-lang
path: ${{github.workspace}}/shared/translations
- name: "Download artifact: manual"
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-manual
path: ${{github.workspace}}/manual/build
- name: "Download artifact: manual-lua"
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-manual-lua
path: ${{github.workspace}}/manual/build.luadoc
@ -570,7 +519,7 @@ jobs:
"C:/Program Files (x86)/Inno Setup 6/ISCC.exe" %GITHUB_WORKSPACE%/package/innosetup/traintastic.iss
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: package-innosetup
path: ${{github.workspace}}/package/innosetup/output
@ -585,43 +534,46 @@ jobs:
- uses: FranzDiebold/github-env-vars-action@v2
- name: Download artifacts 1/6
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: package-innosetup
path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}}
- name: Download artifacts 2/6
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-client-deb
pattern: traintastic-client-deb-*
merge-multiple: true
path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}}
- name: Download artifacts 3/6
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-server-deb
pattern: traintastic-server-deb-*
merge-multiple: true
path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}}
- name: Download artifacts 4/6
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-server-test-code-coverage
path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}}/ccov
- name: Download artifacts 5/6
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-manual
path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}}/manual
- name: Download artifacts 6/6
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-data-deb
pattern: traintastic-data-deb-*
merge-multiple: true
path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}}
- name: "Download artifact: manual-lua"
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: traintastic-manual-lua
path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}}/manual-lua

3
.gitmodules vendored
Datei anzeigen

@ -1,3 +1,6 @@
[submodule "shared/data/lncv"]
path = shared/data/lncv
url = ../lncv.git
[submodule "server/thirdparty/catch2"]
path = server/thirdparty/catch2
url = https://github.com/catchorg/Catch2.git

Datei anzeigen

@ -90,17 +90,17 @@ file(GLOB SOURCES
"thirdparty/QtWaitingSpinner/*.hpp"
"thirdparty/QtWaitingSpinner/*.cpp")
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Widgets Network Svg Xml)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Widgets Network Svg Xml WebSockets)
if(QT_VERSION_MAJOR EQUAL 5)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets Network Svg Xml)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets Network Svg Xml WebSockets)
else()
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets Network Svg Xml SvgWidgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets Network Svg Xml SvgWidgets WebSockets)
endif()
message(STATUS "Found Qt ${QT_VERSION}")
target_sources(traintastic-client PRIVATE ${SOURCES})
target_link_libraries(traintastic-client PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::Xml)
target_link_libraries(traintastic-client PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::Xml Qt${QT_VERSION_MAJOR}::WebSockets)
if(QT_VERSION_MAJOR GREATER 5)
target_link_libraries(traintastic-client PRIVATE Qt${QT_VERSION_MAJOR}::SvgWidgets)
endif()
@ -130,6 +130,11 @@ install(TARGETS traintastic-client
RUNTIME DESTINATION "/opt/traintastic/bin/"
)
if(LINUX)
install(FILES ${CMAKE_SOURCE_DIR}/debian/traintastic-client.desktop DESTINATION share/applications)
install(FILES ${CMAKE_SOURCE_DIR}/debian/traintastic_256.png DESTINATION "/opt/traintastic/icons/")
endif()
### BUILD TYPE ###
if (CMAKE_BUILD_TYPE EQUAL "Release")

Datei anzeigen

@ -333,11 +333,13 @@
"protocol_dr5000_usb": {
"title": "$wizard.add_interface.protocol:title$",
"text": "$wizard.add_interface.protocol:text$",
"bottom_text": "$wizard.add_interface.protocol_dr5000_usb:bottom_text$",
"type": "radio",
"options": [
{
"name": "LocoNet",
"name": "LocoNet ($wizard.add_interface:recommended$)",
"next": "serial_port",
"checked": true,
"actions": {
"create_interface": {
"class_id": "interface.loconet",

Datei anzeigen

@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=Traintastic client
Comment=Model railroad control and automation software
Exec=/opt/traintastic/bin/traintastic-client
Icon=/opt/traintastic/icons/traintastic_256.png
Terminal=false
Categories=Science;Engineering;

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 7.6 KiB

Datei anzeigen

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="96"
height="96"
viewBox="0 0 25.399999 25.400001"
version="1.1"
id="svg8"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="clear_persistent_variables.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="58.303571"
inkscape:cy="31.785714"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1015"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="0">
<inkscape:grid
type="xygrid"
id="grid3713"
spacingx="0.26458333"
empspacing="4" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-nc/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-nc/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:prohibits
rdf:resource="http://creativecommons.org/ns#CommercialUse" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-271.59998)">
<path
style="fill:none;stroke:#ffffff;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 3.175,292.76665 -10e-8,-16.93334 c 2.0852342,1e-5 3.0686586,0 4.2333333,0 4.2333328,2.11667 4.2333328,6.35 0,9.525 H 3.1749999"
id="path3223"
sodipodi:nodetypes="ccccc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 13.758333,275.83331 17.991666,292.76665 22.225,275.83331"
id="path4097"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;stroke:#cd5c5c;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 2.1166666,294.88331 23.283333,273.71665"
id="path1540"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#cd5c5c;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 2.1166666,273.71665 23.283333,294.88331"
id="path1542"
sodipodi:nodetypes="cc" />
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 3.7 KiB

Datei anzeigen

@ -92,5 +92,7 @@
<file>board_tile.rail.nx_button.svg</file>
<file>board_tile.misc.switch.svg</file>
<file>board_tile.misc.label.svg</file>
<file>clear_persistent_variables.svg</file>
<file>swap.svg</file>
</qresource>
</RCC>

116
client/gfx/dark/swap.svg Normale Datei
Datei anzeigen

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="96"
height="96"
viewBox="0 0 25.399999 25.400001"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="swap.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="14.017857"
inkscape:cy="40.089286"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid3713"
spacingx="0.26458333"
empspacing="4" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-nc/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-nc/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:prohibits
rdf:resource="http://creativecommons.org/ns#CommercialUse" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-271.59998)">
<circle
style="fill:#ea80fc;fill-opacity:1;stroke:none;stroke-width:2.51975;stroke-linecap:round;stroke-linejoin:round"
id="path252"
cx="12.7"
cy="284.29996"
r="12.7" />
<g
id="g1229">
<path
style="fill:none;stroke:#ffffff;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 20.108333,280.06665 H 5.2916667"
id="path819"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 8.4666667,276.89165 -3.175,3.175 3.175,3.175"
id="path821"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
</g>
<path
style="fill:none;stroke:#ffffff;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 5.2916667,288.53331 14.8166663,0"
id="path819-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 16.933333,285.35831 3.175,3.175 -3.175,3.175"
id="path821-7"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 4.1 KiB

Datei anzeigen

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="96"
height="96"
viewBox="0 0 25.399999 25.400001"
version="1.1"
id="svg8"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="clear_persistent_variables.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="58.303571"
inkscape:cy="31.785714"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1015"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="0">
<inkscape:grid
type="xygrid"
id="grid3713"
spacingx="0.26458333"
empspacing="4" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-nc/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-nc/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:prohibits
rdf:resource="http://creativecommons.org/ns#CommercialUse" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-271.59998)">
<path
style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 3.175,292.76665 -10e-8,-16.93334 c 2.0852342,1e-5 3.0686586,0 4.2333333,0 4.2333328,2.11667 4.2333328,6.35 0,9.525 H 3.1749999"
id="path3223"
sodipodi:nodetypes="ccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 13.758333,275.83331 17.991666,292.76665 22.225,275.83331"
id="path4097"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;stroke:#b00020;stroke-width:2.11666667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 2.1166666,294.88331 23.283333,273.71665"
id="path1540"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#b00020;stroke-width:2.11666667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 2.1166666,273.71665 23.283333,294.88331"
id="path1542"
sodipodi:nodetypes="cc" />
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 3.7 KiB

Datei anzeigen

@ -66,5 +66,6 @@
<file>board_tile.rail.nx_button.svg</file>
<file>board_tile.misc.switch.svg</file>
<file>board_tile.misc.label.svg</file>
<file>clear_persistent_variables.svg</file>
</qresource>
</RCC>

Datei anzeigen

@ -32,6 +32,7 @@
#include "getboardcolorscheme.hpp"
#include "tilepainter.hpp"
#include "../network/board.hpp"
#include "../network/callmethod.hpp"
#include "../network/object.tpp"
#include "../network/object/blockrailtile.hpp"
#include "../network/object/nxbuttonrailtile.hpp"
@ -39,6 +40,7 @@
#include "../network/abstractvectorproperty.hpp"
#include "../utils/enum.hpp"
#include "../utils/rectf.hpp"
#include "../misc/mimedata.hpp"
#include "../settings/boardsettings.hpp"
QRect rectToViewport(const QRect& r, const int gridSize)
@ -64,14 +66,14 @@ constexpr QRect updateTileRect(const int x, const int y, const int w, const int
}
BoardAreaWidget::BoardAreaWidget(BoardWidget& board, QWidget* parent) :
BoardAreaWidget::BoardAreaWidget(std::shared_ptr<Board> board, QWidget* parent) :
QWidget(parent),
m_colorScheme{&BoardColorScheme::dark},
m_board{board},
m_boardLeft{board.board().getProperty("left")},
m_boardTop{board.board().getProperty("top")},
m_boardRight{board.board().getProperty("right")},
m_boardBottom{board.board().getProperty("bottom")},
m_board{std::move(board)},
m_boardLeft{m_board->getProperty("left")},
m_boardTop{m_board->getProperty("top")},
m_boardRight{m_board->getProperty("right")},
m_boardBottom{m_board->getProperty("bottom")},
m_grid{Grid::Dot},
m_zoomLevel{0},
m_mouseLeftButtonPressed{false},
@ -84,6 +86,7 @@ BoardAreaWidget::BoardAreaWidget(BoardWidget& board, QWidget* parent) :
{
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
setFocusPolicy(Qt::StrongFocus);
setAcceptDrops(true);
if(Q_LIKELY(m_boardLeft))
connect(m_boardLeft, &AbstractProperty::valueChanged, this, &BoardAreaWidget::updateMinimumSize);
@ -96,7 +99,7 @@ BoardAreaWidget::BoardAreaWidget(BoardWidget& board, QWidget* parent) :
connect(&BoardSettings::instance(), &SettingsBase::changed, this, &BoardAreaWidget::settingsChanged);
for(const auto& [l, object] : m_board.board().tileObjects())
for(const auto& [l, object] : m_board->tileObjects())
tileObjectAdded(l.x, l.y, object);
settingsChanged();
@ -112,7 +115,7 @@ void BoardAreaWidget::tileObjectAdded(int16_t x, int16_t y, const ObjectPtr& obj
{
try
{
const TileData& tileData = m_board.board().tileData().at(l);
const TileData& tileData = m_board->tileData().at(l);
update(updateTileRect(l.x - boardLeft(), l.y - boardTop(), tileData.width(), tileData.height(), getTileSize()));
}
catch(...)
@ -127,7 +130,7 @@ void BoardAreaWidget::tileObjectAdded(int16_t x, int16_t y, const ObjectPtr& obj
connect(property, &BaseProperty::valueChanged, this, handler);
};
switch(m_board.board().getTileId(l))
switch(m_board->getTileId(l))
{
case TileId::RailTurnoutLeft45:
case TileId::RailTurnoutLeft90:
@ -192,6 +195,7 @@ void BoardAreaWidget::tileObjectAdded(int16_t x, int16_t y, const ObjectPtr& obj
case TileId::RailTunnel:
case TileId::RailOneWay:
case TileId::RailLink:
case TileId::HiddenRailCrossOver:
case TileId::ReservedForFutureExpension:
break;
@ -307,7 +311,7 @@ void BoardAreaWidget::setMouseMoveTileSizeMax(uint8_t width, uint8_t height)
TurnoutPosition BoardAreaWidget::getTurnoutPosition(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(ObjectPtr object = m_board->getTileObject(l))
if(const auto* p = object->getProperty("position"))
return p->toEnum<TurnoutPosition>();
return TurnoutPosition::Unknown;
@ -315,7 +319,7 @@ TurnoutPosition BoardAreaWidget::getTurnoutPosition(const TileLocation& l) const
SensorState BoardAreaWidget::getSensorState(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(ObjectPtr object = m_board->getTileObject(l))
if(const auto* p = object->getProperty("state"))
return p->toEnum<SensorState>();
return SensorState::Unknown;
@ -323,7 +327,7 @@ SensorState BoardAreaWidget::getSensorState(const TileLocation& l) const
DirectionControlState BoardAreaWidget::getDirectionControlState(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(ObjectPtr object = m_board->getTileObject(l))
if(const auto* p = object->getProperty("state"))
return p->toEnum<DirectionControlState>();
return DirectionControlState::Both;
@ -331,7 +335,7 @@ DirectionControlState BoardAreaWidget::getDirectionControlState(const TileLocati
SignalAspect BoardAreaWidget::getSignalAspect(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(ObjectPtr object = m_board->getTileObject(l))
if(const auto* p = object->getProperty("aspect"))
return p->toEnum<SignalAspect>();
return SignalAspect::Unknown;
@ -339,7 +343,7 @@ SignalAspect BoardAreaWidget::getSignalAspect(const TileLocation& l) const
Color BoardAreaWidget::getColor(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(ObjectPtr object = m_board->getTileObject(l))
if(const auto* p = object->getProperty("color"))
return p->toEnum<Color>();
return Color::None;
@ -347,7 +351,7 @@ Color BoardAreaWidget::getColor(const TileLocation& l) const
DecouplerState BoardAreaWidget::getDecouplerState(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(ObjectPtr object = m_board->getTileObject(l))
if(const auto* p = object->getProperty("state"))
return p->toEnum<DecouplerState>();
return DecouplerState::Deactivated;
@ -355,7 +359,7 @@ DecouplerState BoardAreaWidget::getDecouplerState(const TileLocation& l) const
bool BoardAreaWidget::getNXButtonEnabled(const TileLocation& l) const
{
if(auto object = std::dynamic_pointer_cast<NXButtonRailTile>(m_board.board().getTileObject(l)))
if(auto object = std::dynamic_pointer_cast<NXButtonRailTile>(m_board->getTileObject(l)))
{
return object->getPropertyValueBool("enabled", false);
}
@ -364,7 +368,7 @@ bool BoardAreaWidget::getNXButtonEnabled(const TileLocation& l) const
bool BoardAreaWidget::getNXButtonPressed(const TileLocation& l) const
{
if(auto object = std::dynamic_pointer_cast<NXButtonRailTile>(m_board.board().getTileObject(l)))
if(auto object = std::dynamic_pointer_cast<NXButtonRailTile>(m_board->getTileObject(l)))
{
return object->isPressed();
}
@ -379,11 +383,11 @@ TileLocation BoardAreaWidget::pointToTileLocation(const QPoint& p)
QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const
{
const auto tileId = m_board.board().getTileId(l);
const auto tileId = m_board->getTileId(l);
if(isRailTurnout(tileId))
{
if(auto turnout = m_board.board().getTileObject(l))
if(auto turnout = m_board->getTileObject(l))
{
QString text = "<b>" + turnout->getPropertyValueString("name") + "</b>";
if(auto* position = turnout->getProperty("position")) /*[[likely]]*/
@ -395,7 +399,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const
}
else if(isRailSignal(tileId))
{
if(auto signal = m_board.board().getTileObject(l))
if(auto signal = m_board->getTileObject(l))
{
QString text = "<b>" + signal->getPropertyValueString("name") + "</b>";
if(auto* aspect = signal->getProperty("aspect")) /*[[likely]]*/
@ -407,7 +411,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const
}
else if(tileId == TileId::RailSensor)
{
if(auto sensor = m_board.board().getTileObject(l))
if(auto sensor = m_board->getTileObject(l))
{
QString text = "<b>" + sensor->getPropertyValueString("name") + "</b>";
if(auto* state = sensor->getProperty("state")) /*[[likely]]*/
@ -419,7 +423,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const
}
else if(tileId == TileId::RailBlock)
{
if(auto block = m_board.board().getTileObject(l))
if(auto block = m_board->getTileObject(l))
{
QString text = "<b>" + block->getPropertyValueString("name") + "</b>";
if(auto* state = block->getProperty("state")) /*[[likely]]*/
@ -431,7 +435,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const
}
else if(tileId == TileId::RailDirectionControl)
{
if(auto directionControl = m_board.board().getTileObject(l))
if(auto directionControl = m_board->getTileObject(l))
{
QString text = "<b>" + directionControl->getPropertyValueString("name") + "</b>";
if(auto* state = directionControl->getProperty("state")) /*[[likely]]*/
@ -443,7 +447,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const
}
else if(tileId == TileId::RailSensor)
{
if(auto sensor = m_board.board().getTileObject(l))
if(auto sensor = m_board->getTileObject(l))
{
QString text = "<b>" + sensor->getPropertyValueString("name") + "</b>";
if(auto* state = sensor->getProperty("state")) /*[[likely]]*/
@ -455,7 +459,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const
}
else if(tileId == TileId::Switch)
{
if(auto switch_ = m_board.board().getTileObject(l))
if(auto switch_ = m_board->getTileObject(l))
{
QString text = "<b>" + switch_->getPropertyValueString("name") + "</b>";
if(auto* value = switch_->getProperty("value")) /*[[likely]]*/
@ -480,7 +484,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const
else if(tileId == TileId::PushButton ||
tileId == TileId::RailNXButton)
{
if(auto tile = m_board.board().getTileObject(l))
if(auto tile = m_board->getTileObject(l))
{
return "<b>" + tile->getPropertyValueString("name") + "</b>";
}
@ -667,7 +671,7 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event)
painter.save();
for(auto it : m_board.board().tileData())
for(auto it : m_board->tileData())
if(it.first.x + it.second.width() - 1 >= tiles.left() && it.first.x <= tiles.right() &&
it.first.y + it.second.height() - 1 >= tiles.top() && it.first.y <= tiles.bottom())
{
@ -727,7 +731,7 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event)
break;
case TileId::RailBlock:
tilePainter.drawBlock(id, r, a, state & 0x01, state & 0x02, m_board.board().getTileObject(it.first));
tilePainter.drawBlock(id, r, a, state & 0x01, state & 0x02, m_board->getTileObject(it.first));
break;
case TileId::RailDirectionControl:
@ -748,7 +752,7 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event)
case TileId::Label:
{
if(auto label = m_board.board().getTileObject(it.first)) /*[[likely]]*/
if(auto label = m_board->getTileObject(it.first)) /*[[likely]]*/
{
tilePainter.drawLabel(r, a,
label->getPropertyValueString("text"),
@ -763,7 +767,7 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event)
break;
}
case TileId::Switch:
if(auto sw = m_board.board().getTileObject(it.first)) /*[[likely]]*/
if(auto sw = m_board->getTileObject(it.first)) /*[[likely]]*/
{
tilePainter.drawSwitch(r,
sw->getPropertyValueBool("value", false),
@ -837,6 +841,65 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event)
}
}
void BoardAreaWidget::dragEnterEvent(QDragEnterEvent *event)
{
if(event->mimeData()->hasFormat(AssignTrainMimeData::mimeType))
{
m_dragMoveTileLocation = TileLocation::invalid;
event->acceptProposedAction();
}
}
void BoardAreaWidget::dragMoveEvent(QDragMoveEvent* event)
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
const TileLocation l = pointToTileLocation(event->pos());
#else
const TileLocation l = pointToTileLocation(event->position().toPoint());
#endif
if(m_dragMoveTileLocation != l)
{
m_dragMoveTileLocation = l;
if(event->mimeData()->hasFormat(AssignTrainMimeData::mimeType) &&
m_board->getTileId(l) == TileId::RailBlock)
{
return event->accept();
}
event->ignore();
}
}
void BoardAreaWidget::dropEvent(QDropEvent* event)
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
const TileLocation l = pointToTileLocation(event->pos());
#else
const TileLocation l = pointToTileLocation(event->position().toPoint());
#endif
switch(m_board->getTileId(l))
{
case TileId::RailBlock:
if(auto* assignTrain = dynamic_cast<const AssignTrainMimeData*>(event->mimeData()))
{
if(auto tile = std::dynamic_pointer_cast<BlockRailTile>(m_board->getTileObject(l)))
{
if(auto* method = tile->getMethod("assign_train"))
{
callMethod(*method, nullptr, assignTrain->trainId());
return event->accept();
}
}
}
break;
default:
break;
}
event->ignore();
}
void BoardAreaWidget::settingsChanged()
{
const auto& s = BoardSettings::instance();

Datei anzeigen

@ -38,7 +38,7 @@
#include "../network/abstractproperty.hpp"
#include "../network/objectptr.hpp"
class BoardWidget;
class Board;
class BoardAreaWidget : public QWidget
{
@ -66,7 +66,7 @@ class BoardAreaWidget : public QWidget
protected:
static constexpr int boardMargin = 1; // tile
BoardWidget& m_board;
std::shared_ptr<Board> m_board;
AbstractProperty* m_boardLeft;
AbstractProperty* m_boardTop;
AbstractProperty* m_boardRight;
@ -89,6 +89,8 @@ class BoardAreaWidget : public QWidget
uint8_t m_mouseMoveTileWidthMax;
uint8_t m_mouseMoveTileHeightMax;
TileLocation m_dragMoveTileLocation;
inline int boardLeft() const { return Q_LIKELY(m_boardLeft) ? m_boardLeft->toInt() - boardMargin : 0; }
inline int boardTop() const { return Q_LIKELY(m_boardTop) ? m_boardTop->toInt() - boardMargin: 0; }
inline int boardRight() const { return Q_LIKELY(m_boardRight) ? m_boardRight->toInt() + boardMargin: 0; }
@ -115,6 +117,9 @@ class BoardAreaWidget : public QWidget
void mouseMoveEvent(QMouseEvent* event) final;
void wheelEvent(QWheelEvent* event) final;
void paintEvent(QPaintEvent* event) final;
void dragEnterEvent(QDragEnterEvent* event) final;
void dragMoveEvent(QDragMoveEvent* event) final;
void dropEvent(QDropEvent* event) final;
protected slots:
void settingsChanged();
@ -124,7 +129,7 @@ class BoardAreaWidget : public QWidget
static constexpr int zoomLevelMin = -2;
static constexpr int zoomLevelMax = 15;
BoardAreaWidget(BoardWidget& board, QWidget* parent = nullptr);
BoardAreaWidget(std::shared_ptr<Board> board, QWidget* parent = nullptr);
Grid grid() const { return m_grid; }
void nextGrid();

Datei anzeigen

@ -34,7 +34,6 @@
#include <QApplication>
#include <QKeyEvent>
#include <QPainter>
#include <QTimer>
#include <traintastic/locale/locale.hpp>
#include "getboardcolorscheme.hpp"
#include "tilepainter.hpp"
@ -84,7 +83,7 @@ BoardWidget::BoardWidget(std::shared_ptr<Board> object, QWidget* parent) :
QWidget(parent),
m_object{std::move(object)},
m_nxManagerRequestId{Connection::invalidRequestId},
m_boardArea{new BoardAreaWidget(*this, this)},
m_boardArea{new BoardAreaWidget(m_object, this)},
m_statusBar{new QStatusBar(this)},
m_statusBarMessage{new QLabel(this)},
m_statusBarCoords{new QLabel(this)},
@ -92,6 +91,7 @@ BoardWidget::BoardWidget(std::shared_ptr<Board> object, QWidget* parent) :
m_editActions{new QActionGroup(this)}
, m_tileMoveStarted{false}
, m_tileResizeStarted{false}
, m_nxButtonTimerId(0)
{
setWindowIcon(Theme::getIconForClassId(object->classId));
setFocusPolicy(Qt::StrongFocus);
@ -447,6 +447,8 @@ BoardWidget::BoardWidget(std::shared_ptr<Board> object, QWidget* parent) :
BoardWidget::~BoardWidget()
{
stopTimerAndReleaseButtons();
if(m_nxManagerRequestId != Connection::invalidRequestId)
{
m_object->connection()->cancelRequest(m_nxManagerRequestId);
@ -459,6 +461,10 @@ void BoardWidget::worldEditChanged(bool value)
m_editActionNone->activate(QAction::Trigger);
m_toolbarEdit->setVisible(value);
m_statusBar->setVisible(value);
// Stop timers in edit mode
if(value)
stopTimerAndReleaseButtons();
}
void BoardWidget::gridChanged(BoardAreaWidget::Grid value)
@ -650,7 +656,8 @@ void BoardWidget::tileClicked(int16_t x, int16_t y)
if(nxButton->isPressed())
{
releaseNXButton(nxButton);
stopTimerAndReleaseButtons();
return;
}
else
{
@ -669,31 +676,13 @@ void BoardWidget::tileClicked(int16_t x, int16_t y)
m_nxButtonPressed.reset();
QTimer::singleShot(nxButtonReleaseDelay, this,
[this, weak1=std::weak_ptr<NXButtonRailTile>(firstButton), weak2=std::weak_ptr<NXButtonRailTile>(nxButton)]()
{
if(auto btn = weak1.lock())
{
releaseNXButton(btn);
}
if(auto btn = weak2.lock())
{
releaseNXButton(btn);
}
});
startReleaseTimer(firstButton, nxButton);
}
else
{
m_nxButtonPressed = nxButton;
QTimer::singleShot(nxButtonHoldTime, this,
[this, weak=std::weak_ptr<NXButtonRailTile>(nxButton)]()
{
if(auto btn = weak.lock())
{
releaseNXButton(btn);
}
});
startHoldTimer(nxButton);
}
}
}
@ -778,6 +767,53 @@ void BoardWidget::rightClicked()
rotateTile(true);
}
void BoardWidget::startHoldTimer(const std::shared_ptr<NXButtonRailTile>& nxButton)
{
stopTimerAndReleaseButtons();
m_releaseButton1 = nxButton;
assert(m_nxButtonTimerId == 0);
m_nxButtonTimerId = startTimer(nxButtonHoldTime);
}
void BoardWidget::startReleaseTimer(const std::shared_ptr<NXButtonRailTile> &firstButton,
const std::shared_ptr<NXButtonRailTile> &nxButton)
{
// Do not release first button yet
m_releaseButton1.reset();
stopTimerAndReleaseButtons();
m_releaseButton1 = firstButton;
m_releaseButton2 = nxButton;
assert(m_nxButtonTimerId == 0);
m_nxButtonTimerId = startTimer(nxButtonReleaseDelay);
}
void BoardWidget::stopTimerAndReleaseButtons()
{
if(m_nxButtonTimerId)
{
// Instantly release buttons
if(auto btn = m_releaseButton1.lock())
{
releaseNXButton(btn);
}
m_releaseButton1.reset();
if(auto btn = m_releaseButton2.lock())
{
releaseNXButton(btn);
}
m_releaseButton2.reset();
killTimer(m_nxButtonTimerId);
m_nxButtonTimerId = 0;
}
}
void BoardWidget::actionSelected(const Board::TileInfo* info)
{
m_boardArea->setMouseMoveAction(BoardAreaWidget::MouseMoveAction::None);
@ -825,6 +861,17 @@ void BoardWidget::keyPressEvent(QKeyEvent* event)
}
}
void BoardWidget::timerEvent(QTimerEvent *e)
{
if(e->timerId() == m_nxButtonTimerId)
{
stopTimerAndReleaseButtons();
return;
}
QWidget::timerEvent(e);
}
void BoardWidget::rotateTile(bool ccw)
{
if(m_tileRotates != 0)

Datei anzeigen

@ -78,11 +78,25 @@ class BoardWidget : public QWidget
TileRotate m_tileRotateLast = TileRotate::Deg0; //!< Last used tile rotate for add/move
std::weak_ptr<NXButtonRailTile> m_nxButtonPressed;
int m_nxButtonTimerId;
std::weak_ptr<NXButtonRailTile> m_releaseButton1;
std::weak_ptr<NXButtonRailTile> m_releaseButton2;
void startHoldTimer(const std::shared_ptr<NXButtonRailTile> &nxButton);
void startReleaseTimer(const std::shared_ptr<NXButtonRailTile> &firstButton,
const std::shared_ptr<NXButtonRailTile> &nxButton);
void actionSelected(const Board::TileInfo* tile);
void keyPressEvent(QKeyEvent* event) override;
void timerEvent(QTimerEvent *e) override;
void rotateTile(bool ccw = false);
void releaseNXButton(const std::shared_ptr<NXButtonRailTile>& nxButton);
protected slots:
void stopTimerAndReleaseButtons();
protected slots:
void worldEditChanged(bool value);
void gridChanged(BoardAreaWidget::Grid value);

Datei anzeigen

@ -39,7 +39,7 @@ std::unique_ptr<QMenu> TileMenu::getBlockRailTileMenu(const ObjectPtr& tile, QWi
menu->addAction(new MethodAction(*assignTrain,
[parent, assignTrain]()
{
std::make_unique<ObjectSelectListDialog>(*assignTrain, parent)->exec();
std::make_unique<ObjectSelectListDialog>(*assignTrain, false, parent)->exec();
}));
if(auto* removeTrain = tile->getMethod("remove_train"))
{

Datei anzeigen

@ -177,6 +177,7 @@ void TilePainter::draw(TileId id, const QRectF& r, TileRotate rotate, bool isRes
break;
case TileId::None:
case TileId::HiddenRailCrossOver:
case TileId::ReservedForFutureExpension:
break;
}
@ -774,7 +775,7 @@ void TilePainter::drawTriangle(const QRectF& r)
{r.right(), r.bottom()},
{r.left(), r.bottom()}}};
m_painter.drawConvexPolygon(points.data(), points.size());
m_painter.drawConvexPolygon(points.data(), static_cast<int>(points.size()));
}
void TilePainter::drawLED(const QRectF& r, const QColor& color, const QColor& borderColor)

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2021,2023 Reinder Feenstra
* Copyright (C) 2019-2021,2023-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -150,8 +150,10 @@ void ConnectDialog::socketReadyRead()
QString name = QString::fromUtf8(message.read<QByteArray>());
QUrl url;
url.setScheme("ws");
url.setHost(host.toString());
url.setPort(port);
url.setPath("/client");
auto it = m_servers.find(url);
if(it == m_servers.end())
@ -230,6 +232,12 @@ void ConnectDialog::serverTextChanged(const QString& text)
{
QString url{text};
m_url = QUrl::fromUserInput(url.remove(QRegularExpression("\\s*\\(.*\\)$")));
m_url.setScheme("ws");
m_url.setPath("/client");
if(m_url.port() == -1)
{
m_url.setPort(Connection::defaultPort);
}
m_connect->setEnabled(m_url.isValid());
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2020,2023 Reinder Feenstra
* Copyright (C) 2019-2020,2023-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -34,19 +34,20 @@
#include "../widget/alertwidget.hpp"
#include <traintastic/locale/locale.hpp>
ObjectSelectListDialog::ObjectSelectListDialog(Method& method, QWidget* parent) :
ObjectSelectListDialog(static_cast<InterfaceItem&>(method), parent)
ObjectSelectListDialog::ObjectSelectListDialog(Method& method, bool multiSelect, QWidget* parent) :
ObjectSelectListDialog(static_cast<InterfaceItem&>(method), multiSelect, parent)
{
}
ObjectSelectListDialog::ObjectSelectListDialog(ObjectProperty& property, QWidget* parent) :
ObjectSelectListDialog(static_cast<InterfaceItem&>(property), parent)
ObjectSelectListDialog(static_cast<InterfaceItem&>(property), false, parent)
{
}
ObjectSelectListDialog::ObjectSelectListDialog(InterfaceItem& item, QWidget* parent) :
ObjectSelectListDialog::ObjectSelectListDialog(InterfaceItem& item, bool multiSelect, QWidget* parent) :
QDialog(parent, Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint),
m_item{item},
m_multiSelect{multiSelect},
m_buttons{new QDialogButtonBox(this)},
m_tableWidget{new TableWidget()}
{
@ -58,7 +59,14 @@ ObjectSelectListDialog::ObjectSelectListDialog(InterfaceItem& item, QWidget* par
connect(m_buttons->button(QDialogButtonBox::Ok), &QPushButton::clicked, this,
[this]()
{
acceptRow(m_tableWidget->selectionModel()->selectedIndexes().first().row());
if(m_multiSelect)
{
acceptRows(m_tableWidget->selectionModel()->selectedIndexes());
}
else
{
acceptRow(m_tableWidget->selectionModel()->selectedIndexes().first().row());
}
});
m_buttons->button(QDialogButtonBox::Cancel)->setText(Locale::tr("qtapp.object_select_list_dialog:cancel"));
connect(m_buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &ObjectSelectListDialog::reject);
@ -91,7 +99,8 @@ ObjectSelectListDialog::ObjectSelectListDialog(InterfaceItem& item, QWidget* par
connect(m_tableWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this,
[this](const QItemSelection&, const QItemSelection&)
{
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(m_tableWidget->selectionModel()->selectedRows().count() == 1);
const auto selectionCount = m_tableWidget->selectionModel()->selectedRows().count();
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(m_multiSelect ? selectionCount > 0 : selectionCount == 1);
});
connect(m_tableWidget, &TableWidget::doubleClicked, this,
[this](const QModelIndex& index)
@ -133,3 +142,16 @@ void ObjectSelectListDialog::acceptRow(int row)
accept();
}
void ObjectSelectListDialog::acceptRows(const QModelIndexList& indexes)
{
if(auto* m = dynamic_cast<Method*>(&m_item))
{
for(const auto& index : indexes)
{
callMethod(*m, nullptr, m_tableWidget->getRowObjectId(index.row()));
}
}
accept();
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2020 Reinder Feenstra
* Copyright (C) 2019-2020,2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -24,6 +24,7 @@
#define TRAINTASTIC_CLIENT_DIALOG_OBJECTSELECTLISTDIALOG_HPP
#include <QDialog>
#include <QModelIndexList>
#include "../network/objectptr.hpp"
class QDialogButtonBox;
@ -39,17 +40,19 @@ class ObjectSelectListDialog : public QDialog
protected:
InterfaceItem& m_item;
const bool m_multiSelect;
ObjectPtr m_object;
int m_requestId;
QDialogButtonBox* m_buttons;
TableWidget* m_tableWidget;
ObjectSelectListDialog(InterfaceItem& item, QWidget* parent);
ObjectSelectListDialog(InterfaceItem& item, bool multiSelect, QWidget* parent);
void acceptRow(int row);
void acceptRows(const QModelIndexList& indexes);
public:
ObjectSelectListDialog(Method& method, QWidget* parent);
ObjectSelectListDialog(Method& method, bool multiSelect, QWidget* parent);
ObjectSelectListDialog(ObjectProperty& property, QWidget* parent);
};

Datei anzeigen

@ -26,6 +26,9 @@
#endif
#include <QCommandLineParser>
#include <QMessageBox>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
#include <QStyleHints>
#endif
#include <version.hpp>
#include "mainwindow.hpp"
#include "settings/generalsettings.hpp"
@ -124,9 +127,15 @@ int main(int argc, char* argv[])
if(logMissingStrings)
const_cast<Locale*>(Locale::instance.get())->enableMissingLogging();
// Auto select icon set based on background color lightness:
const qreal backgroundLightness = QApplication::style()->standardPalette().window().color().lightnessF();
Theme::setIconSet(backgroundLightness < 0.5 ? Theme::IconSet::Dark : Theme::IconSet::Light);
// Detect light/dark:
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
const QPalette defaultPalette;
const auto text = defaultPalette.color(QPalette::WindowText);
const auto window = defaultPalette.color(QPalette::Window);
Theme::setDark(text.lightness() > window.lightness());
#else
Theme::setDark(QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark);
#endif
MainWindow mw;
if(options.fullscreen)

Datei anzeigen

@ -122,6 +122,7 @@ MainWindow::MainWindow(QWidget* parent) :
QAction* boardsAction;
QAction* trainsAction;
m_mdiArea->setBackground(palette().window().color().darker());
m_mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
@ -171,7 +172,6 @@ MainWindow::MainWindow(QWidget* parent) :
if(const ObjectPtr& traintastic = m_connection->traintastic())
traintastic->callMethod("close_world");
});
m_actionCloseWorld->setShortcut(QKeySequence::Close);
menu->addSeparator();
m_actionImportWorld = menu->addAction(Theme::getIcon("world_import"), Locale::tr("qtapp.mainmenu:import_world") + "...",
[this]()
@ -510,7 +510,7 @@ MainWindow::MainWindow(QWidget* parent) :
})->setShortcut(QKeySequence::HelpContents);
auto* subMenu = menu->addMenu(Locale::tr("qtapp.mainmenu:wizards"));
subMenu->addAction(Locale::tr("wizard.introduction:title"), this, &MainWindow::showIntroductionWizard);
subMenu->addAction(Locale::tr("wizard.add_interface.welcome:title"), this, &MainWindow::showAddInterfaceWizard);
m_actionAddInterfaceWizard = subMenu->addAction(Locale::tr("wizard.add_interface.welcome:title"), this, &MainWindow::showAddInterfaceWizard);
//menu->addSeparator();
//menu->addAction(Locale::tr("qtapp.mainmenu:about_qt") + "...", qApp, &QApplication::aboutQt);
menu->addAction(Locale::tr("qtapp.mainmenu:about") + "...", this, &MainWindow::showAbout);
@ -667,7 +667,8 @@ void MainWindow::changeEvent(QEvent* event)
void MainWindow::worldChanged()
{
m_newWorldWizard.reset();
m_wizard.newWorld.reset();
m_wizard.addInterface.reset();
if(m_world)
m_mdiArea->closeAllSubWindows();
@ -738,13 +739,13 @@ void MainWindow::worldChanged()
if(m_newWorldRequested && m_world)
{
m_newWorldRequested = false;
m_newWorldWizard = std::make_unique<NewWorldWizard>(m_world, this);
connect(m_newWorldWizard.get(), &NewWorldWizard::finished,
m_wizard.newWorld = std::make_unique<NewWorldWizard>(m_world, this);
connect(m_wizard.newWorld.get(), &NewWorldWizard::finished,
[this]()
{
m_newWorldWizard.release()->deleteLater();
m_wizard.newWorld.release()->deleteLater();
});
m_newWorldWizard->open();
m_wizard.newWorld->open();
}
}
@ -761,13 +762,21 @@ void MainWindow::loadWorld()
if(!m_connection)
return;
std::unique_ptr<WorldListDialog> d = std::make_unique<WorldListDialog>(m_connection, this);
if(d->exec() == QDialog::Accepted)
{
Method* method = m_connection->traintastic()->getMethod("load_world");
if(Q_LIKELY(method))
method->call(d->uuid());
}
m_loadWorldDialog = std::make_unique<WorldListDialog>(m_connection, this);
connect(m_loadWorldDialog.get(), &WorldListDialog::finished,
[this](int result)
{
if(result == QDialog::Accepted)
{
if(Method* method = m_connection->traintastic()->getMethod("load_world")) /*[[likely]]*/
{
method->call(m_loadWorldDialog->uuid());
}
}
m_loadWorldDialog.release()->deleteLater();
});
m_loadWorldDialog->setModal(true);
m_loadWorldDialog->show();
}
void MainWindow::toggleFullScreen()
@ -929,17 +938,18 @@ IntroductionWizard* MainWindow::showIntroductionWizard()
return introductionWizard;
}
AddInterfaceWizard* MainWindow::showAddInterfaceWizard()
void MainWindow::showAddInterfaceWizard()
{
if(!m_world) /*[[unlikely]]*/
if(m_world && !m_wizard.addInterface) /*[[likely]]*/
{
return nullptr;
m_wizard.addInterface = std::make_unique<AddInterfaceWizard>(m_world, this);
connect(m_wizard.addInterface.get(), &AddInterfaceWizard::finished,
[this]()
{
m_wizard.addInterface.release()->deleteLater();
});
m_wizard.addInterface->open();
}
auto* addInterfaceWizard = new AddInterfaceWizard(m_world, this);
addInterfaceWizard->setAttribute(Qt::WA_DeleteOnClose);
addInterfaceWizard->open();
return addInterfaceWizard;
}
NewBoardWizard* MainWindow::showNewBoardWizard(const ObjectPtr& board)
@ -970,6 +980,10 @@ void MainWindow::connectionStateChanged()
if(m_connection && m_connection->state() == Connection::State::Disconnected)
{
m_connection.reset();
if(m_loadWorldDialog)
{
m_loadWorldDialog->reject();
}
if(m_serverLog)
{
delete m_serverLog;
@ -989,7 +1003,6 @@ void MainWindow::updateActions()
const bool connected = m_connection && m_connection->state() == Connection::State::Connected;
const bool haveWorld = connected && m_connection->world();
m_actionConnectToServer->setEnabled(!m_connection);
m_actionConnectToServer->setVisible(!connected);
m_actionDisconnectFromServer->setVisible(connected);
@ -1013,6 +1026,7 @@ void MainWindow::updateActions()
m_actionServerShutdown->setEnabled(m && m->getAttributeBool(AttributeName::Enabled, false));
}
m_menuProgramming->setEnabled(haveWorld);
m_actionAddInterfaceWizard->setEnabled(haveWorld);
setMenuEnabled(m_menuWorld, haveWorld);
m_worldOnlineOfflineToolButton->setEnabled(haveWorld);

Datei anzeigen

@ -46,6 +46,7 @@ class NewWorldWizard;
class IntroductionWizard;
class AddInterfaceWizard;
class NewBoardWizard;
class WorldListDialog;
class MainWindow final : public QMainWindow
{
@ -55,7 +56,12 @@ class MainWindow final : public QMainWindow
std::shared_ptr<Connection> m_connection;
ObjectPtr m_world;
bool m_newWorldRequested = false;
std::unique_ptr<NewWorldWizard> m_newWorldWizard;
std::unique_ptr<WorldListDialog> m_loadWorldDialog;
struct
{
std::unique_ptr<AddInterfaceWizard> addInterface;
std::unique_ptr<NewWorldWizard> newWorld;
} m_wizard;
int m_clockRequest;
ObjectPtr m_clock;
QSplitter* m_splitter;
@ -99,6 +105,7 @@ class MainWindow final : public QMainWindow
QAction* m_actionServerShutdown;
QAction* m_actionServerLog;
QMenu* m_menuProgramming;
QAction* m_actionAddInterfaceWizard;
// Main toolbar:
QToolBar* m_toolbar;
QToolButton* m_worldOnlineOfflineToolButton;
@ -137,7 +144,7 @@ class MainWindow final : public QMainWindow
const ObjectPtr& worldClock() const { return m_clock; }
IntroductionWizard* showIntroductionWizard();
AddInterfaceWizard* showAddInterfaceWizard();
void showAddInterfaceWizard();
NewBoardWizard* showNewBoardWizard(const ObjectPtr& board);
void showLuaScriptsList();

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2022-2023 Reinder Feenstra
* Copyright (C) 2022-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -32,6 +32,7 @@
#include "../settings/statusbarsettings.hpp"
#include "../widget/status/interfacestatuswidget.hpp"
#include "../widget/status/luastatuswidget.hpp"
#include "../widget/status/simulationstatuswidget.hpp"
MainWindowStatusBar::MainWindowStatusBar(MainWindow& mainWindow)
: QStatusBar(&mainWindow)
@ -148,6 +149,8 @@ void MainWindowStatusBar::updateStatuses()
m_statuses->layout()->addWidget(new InterfaceStatusWidget(object, this));
else if(object->classId() == "status.lua")
m_statuses->layout()->addWidget(new LuaStatusWidget(object, this));
else if(object->classId() == "status.simulation")
m_statuses->layout()->addWidget(new SimulationStatusWidget(object, this));
}
});
}

44
client/src/misc/mimedata.hpp Normale Datei
Datei anzeigen

@ -0,0 +1,44 @@
/**
* client/src/misc/mimedata.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_MISC_MIMEDATA_HPP
#define TRAINTASTIC_CLIENT_MISC_MIMEDATA_HPP
#include <QMimeData>
class AssignTrainMimeData : public QMimeData
{
public:
inline static const auto mimeType = QLatin1String("application/vnd.traintastic.assign_train");
explicit AssignTrainMimeData(const QString& trainId)
{
setData(mimeType, trainId.toUtf8());
}
inline QString trainId() const
{
return QString::fromUtf8(data(mimeType));
}
};
#endif

Datei anzeigen

@ -21,7 +21,7 @@
*/
#include "connection.hpp"
#include <QTcpSocket>
#include <QWebSocket>
#include <QUrl>
#include <QCryptographicHash>
#include <traintastic/network/message.hpp>
@ -130,23 +130,31 @@ inline static QStringList readObjectIdArray(const Message& message, const int le
Connection::Connection() :
QObject(),
m_socket{new QTcpSocket(this)},
m_socket{new QWebSocket()},
m_state{State::Disconnected},
m_worldProperty{nullptr},
m_worldRequestId{invalidRequestId}
, m_serverLogTableModel{nullptr}
{
connect(m_socket, &QTcpSocket::connected, this, &Connection::socketConnected);
connect(m_socket, &QTcpSocket::disconnected, this, &Connection::socketDisconnected);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
connect(m_socket, static_cast<void(QTcpSocket::*)(QAbstractSocket::SocketError)>(&QTcpSocket::error), this, &Connection::socketError);
connect(m_socket, &QWebSocket::connected, this, &Connection::socketConnected);
connect(m_socket, &QWebSocket::disconnected, this, &Connection::socketDisconnected);
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
connect(m_socket, &QWebSocket::errorOccurred, this, &Connection::socketError);
#else
connect(m_socket, &QTcpSocket::errorOccurred, this, &Connection::socketError);
connect(m_socket, static_cast<void(QWebSocket::*)(QAbstractSocket::SocketError)>(&QWebSocket::error), this, &Connection::socketError);
#endif
m_socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
connect(m_socket, &QTcpSocket::readyRead, this, &Connection::socketReadyRead);
connect(m_socket, &QWebSocket::binaryMessageReceived,
[this](const QByteArray& data)
{
const Message::Header& header = *reinterpret_cast<const Message::Header*>(data.data());
auto message = std::make_shared<Message>(header);
if(header.dataSize != 0)
{
std::memcpy(message->data(), data.data() + sizeof(header), message->dataSize());
}
processMessage(message);
});
}
bool Connection::isDisconnected() const
@ -172,12 +180,12 @@ void Connection::connectToHost(const QUrl& url, const QString& username, const Q
else
m_password = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256);
setState(State::Connecting);
m_socket->connectToHost(url.host(), static_cast<quint16>(url.port(defaultPort)));
m_socket->open(url);
}
void Connection::disconnectFromHost()
{
m_socket->disconnectFromHost();
m_socket->close();
}
void Connection::cancelRequest(int requestId)
@ -443,7 +451,8 @@ int Connection::getTileData(Board& object)
void Connection::send(std::unique_ptr<Message>& message)
{
Q_ASSERT(!message->isRequest());
m_socket->write(static_cast<const char*>(**message), message->size());
QByteArray bytes(static_cast<const char*>(**message), message->size()); // Deep copy :(
m_socket->sendBinaryMessage(bytes); // sendBinaryMessage only supports QByteArray
}
void Connection::send(std::unique_ptr<Message>& message, std::function<void(const std::shared_ptr<Message>&)> callback)
@ -451,7 +460,8 @@ void Connection::send(std::unique_ptr<Message>& message, std::function<void(cons
Q_ASSERT(message->isRequest());
Q_ASSERT(!m_requestCallback.contains(message->requestId()));
m_requestCallback[message->requestId()] = callback;
m_socket->write(static_cast<const char*>(**message), message->size());
QByteArray bytes(static_cast<const char*>(**message), message->size()); // Deep copy :(
m_socket->sendBinaryMessage(bytes); // sendBinaryMessage only supports QByteArray
}
ObjectPtr Connection::readObject(const Message& message)
@ -1050,14 +1060,14 @@ void Connection::socketConnected()
else
{
setState(State::ErrorNewSessionFailed);
m_socket->disconnectFromHost();
m_socket->close();
}
});
}
else
{
setState(State::ErrorAuthenticationFailed);
m_socket->disconnectFromHost();
m_socket->close();
}
});
}
@ -1071,32 +1081,3 @@ void Connection::socketError(QAbstractSocket::SocketError)
{
setState(State::SocketError);
}
void Connection::socketReadyRead()
{
while(m_socket->bytesAvailable() != 0)
{
if(!m_readBuffer.message) // read header
{
m_readBuffer.offset += m_socket->read(reinterpret_cast<char*>(&m_readBuffer.header) + m_readBuffer.offset, sizeof(m_readBuffer.header) - m_readBuffer.offset);
if(m_readBuffer.offset == sizeof(m_readBuffer.header))
{
if(m_readBuffer.header.dataSize != 0)
m_readBuffer.message = std::make_shared<Message>(m_readBuffer.header);
else
processMessage(std::make_shared<Message>(m_readBuffer.header));
m_readBuffer.offset = 0;
}
}
else // read data
{
m_readBuffer.offset += m_socket->read(reinterpret_cast<char*>(m_readBuffer.message->data()) + m_readBuffer.offset, m_readBuffer.message->dataSize() - m_readBuffer.offset);
if(m_readBuffer.offset == m_readBuffer.message->dataSize())
{
processMessage(m_readBuffer.message);
m_readBuffer.message.reset();
m_readBuffer.offset = 0;
}
}
}
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2021,2023 Reinder Feenstra
* Copyright (C) 2019-2021,2023-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -35,7 +35,7 @@
#include "objectptr.hpp"
#include "tablemodelptr.hpp"
class QTcpSocket;
class QWebSocket;
class ServerLogTableModel;
class Property;
class ObjectProperty;
@ -73,7 +73,7 @@ class Connection : public QObject, public std::enable_shared_from_this<Connectio
using SocketError = QAbstractSocket::SocketError;
protected:
QTcpSocket* m_socket;
QWebSocket* m_socket;
State m_state;
QString m_username;
QByteArray m_password;
@ -108,7 +108,6 @@ class Connection : public QObject, public std::enable_shared_from_this<Connectio
void socketConnected();
void socketDisconnected();
void socketError(QAbstractSocket::SocketError);
void socketReadyRead();
public:
static const quint16 defaultPort = 5740;

Datei anzeigen

@ -70,7 +70,7 @@ class ServerLogTableModel final : public QAbstractTableModel
ServerLogTableModel(std::shared_ptr<Connection> connection);
~ServerLogTableModel();
int columnCount(const QModelIndex& parent = QModelIndex()) const final { Q_UNUSED(parent); return m_columnHeaders.size(); }
int columnCount(const QModelIndex& parent = QModelIndex()) const final { Q_UNUSED(parent); return static_cast<int>(m_columnHeaders.size()); }
int rowCount(const QModelIndex& parent = QModelIndex()) const final;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const final;

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021,2023 Reinder Feenstra
* Copyright (C) 2021,2023-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -23,6 +23,7 @@
#include "theme.hpp"
#include <cassert>
#include <QFile>
#include <QPalette>
const QString iconPathDefault = QStringLiteral(":/");
const QString iconPathDark = QStringLiteral(":/dark/");
@ -31,8 +32,20 @@ const QString iconExtension = QStringLiteral(".svg");
const std::array<const QString*, 3> iconPathsDark = {&iconPathDark, &iconPathDefault, &iconPathLight};
const std::array<const QString*, 3> iconPathsLight = {&iconPathLight, &iconPathDefault, &iconPathDark};
bool Theme::s_isDark = false;
Theme::IconSet Theme::s_iconSet = Theme::IconSet::Light;
bool Theme::isDark()
{
return s_isDark;
}
void Theme::setDark(bool value)
{
s_isDark = value;
setIconSet(value ? Theme::IconSet::Dark : Theme::IconSet::Light);
}
void Theme::setIconSet(IconSet value)
{
s_iconSet = value;

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021 Reinder Feenstra
* Copyright (C) 2021,2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -39,11 +39,15 @@ class Theme
private:
Theme() = delete;
static bool s_isDark;
static IconSet s_iconSet;
static const std::array<const QString*, 3>& getIconPaths();
public:
static bool isDark();
static void setDark(bool value);
static void setIconSet(IconSet value);
static QString getIconFile(const QString& id);

Datei anzeigen

@ -33,6 +33,7 @@
#include <traintastic/enum/decouplerstate.hpp>
#include <traintastic/enum/direction.hpp>
#include <traintastic/enum/directioncontrolstate.hpp>
#include <traintastic/enum/externaloutputchangeaction.hpp>
#include <traintastic/enum/lengthunit.hpp>
#include <traintastic/enum/loconetf9f28.hpp>
#include <traintastic/enum/loconetfastclock.hpp>
@ -105,6 +106,7 @@ QString translateEnum(const QString& enumName, qint64 value)
TRANSLATE_ENUM(DecouplerState)
TRANSLATE_ENUM(Direction)
TRANSLATE_ENUM(DirectionControlState)
TRANSLATE_ENUM(ExternalOutputChangeAction)
TRANSLATE_ENUM(LengthUnit)
TRANSLATE_ENUM(LocoNetF9F28)
TRANSLATE_ENUM(LocoNetFastClock)

Datei anzeigen

@ -24,6 +24,7 @@
#include "list/marklincanlocomotivelistwidget.hpp"
#include "objectlist/boardlistwidget.hpp"
#include "objectlist/throttleobjectlistwidget.hpp"
#include "objectlist/trainlistwidget.hpp"
#include "object/luascripteditwidget.hpp"
#include "object/objecteditwidget.hpp"
#include "object/itemseditwidget.hpp"
@ -34,6 +35,7 @@
#include "propertydoublespinbox.hpp"
#include "propertyspinbox.hpp"
#include "propertylineedit.hpp"
#include "propertypairoutputaction.hpp"
#include "../board/boardwidget.hpp"
#include "../network/object.hpp"
#include "../network/inputmonitor.hpp"
@ -47,7 +49,7 @@ QWidget* createWidgetIfCustom(const ObjectPtr& object, QWidget* parent)
if(classId == "command_station_list")
return new ObjectListWidget(object, parent); // todo remove
else if(classId == "decoder_list" || classId == "list.train")
else if(classId == "decoder_list")
return new ThrottleObjectListWidget(object, parent); // todo remove
else if(classId == "controller_list")
return new ObjectListWidget(object, parent); // todo remove
@ -61,6 +63,10 @@ QWidget* createWidgetIfCustom(const ObjectPtr& object, QWidget* parent)
{
return new BoardListWidget(object, parent);
}
if(classId == "list.train")
{
return new TrainListWidget(object, parent);
}
else if(object->classId().startsWith("list."))
return new ObjectListWidget(object, parent);
else if(classId == "lua.script")
@ -117,6 +123,10 @@ QWidget* createWidget(Property& property, QWidget* parent)
break; // TODO
case ValueType::Enum:
if(property.enumName() == "pair_output_action")
{
return new PropertyPairOutputAction(property, parent);
}
return new PropertyComboBox(property, parent);
case ValueType::Integer:

Datei anzeigen

@ -160,7 +160,7 @@ void InputMonitorWidget::keyReleaseEvent(QKeyEvent* event)
uint32_t InputMonitorWidget::pageCount() const
{
return static_cast<uint32_t>(m_addressMax->toInt64() - m_addressMin->toInt64() + m_leds.size()) / m_leds.size();
return static_cast<uint32_t>(m_addressMax->toInt64() - m_addressMin->toInt64() + m_leds.size()) / static_cast<uint32_t>(m_leds.size());
}
void InputMonitorWidget::setPage(uint32_t value)
@ -184,7 +184,7 @@ void InputMonitorWidget::setGroupBy(uint32_t value)
LEDWidget* InputMonitorWidget::getLED(uint32_t address)
{
const uint32_t first = static_cast<uint32_t>(m_addressMin->toInt64()) + m_page * m_leds.size();
const uint32_t first = static_cast<uint32_t>(m_addressMin->toInt64()) + m_page * static_cast<uint32_t>(m_leds.size());
if(address >= first && (address - first) < m_leds.size())
return m_leds[address - first];
@ -199,7 +199,7 @@ void InputMonitorWidget::updateLEDs()
const uint32_t addressMin = static_cast<uint32_t>(m_addressMin->toInt64());
const uint32_t addressMax = static_cast<uint32_t>(m_addressMax->toInt64());
uint32_t address = addressMin + m_page * m_leds.size();
uint32_t address = addressMin + m_page * static_cast<uint32_t>(m_leds.size());
for(auto* led : m_leds)
{

Datei anzeigen

@ -0,0 +1,82 @@
/**
* client/src/widget/methodicon.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "methodicon.hpp"
#include <QMouseEvent>
#include <QIcon>
#include "../network/method.hpp"
MethodIcon::MethodIcon(Method& method, QIcon icon, QWidget* parent) :
QLabel(parent),
m_method{method}
{
setPixmap(icon.pixmap(32, 32));
setCursor(Qt::PointingHandCursor);
setEnabled(m_method.getAttributeBool(AttributeName::Enabled, true));
setVisible(m_method.getAttributeBool(AttributeName::Visible, true));
setToolTip(m_method.displayName());
connect(&m_method, &Method::attributeChanged, this,
[this](AttributeName name, const QVariant& value)
{
switch(name)
{
case AttributeName::Enabled:
setEnabled(value.toBool());
break;
case AttributeName::Visible:
setVisible(value.toBool());
break;
case AttributeName::DisplayName:
setToolTip(m_method.displayName());
break;
default:
break;
}
});
}
void MethodIcon::mousePressEvent(QMouseEvent* event)
{
if(event->button() == Qt::LeftButton)
{
m_mouseLeftButtonPressed = true;
}
}
void MethodIcon::mouseReleaseEvent(QMouseEvent* event)
{
if(m_mouseLeftButtonPressed && event->button() == Qt::LeftButton)
{
m_mouseLeftButtonPressed = false;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
if(rect().contains(event->localPos().toPoint())) // test if mouse release in widget
#else
if(rect().contains(event->position().toPoint())) // test if mouse release in widget
#endif
{
m_method.call();
}
}
}

Datei anzeigen

@ -0,0 +1,43 @@
/**
* client/src/widget/methodicon.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_METHODICON_HPP
#define TRAINTASTIC_CLIENT_WIDGET_METHODICON_HPP
#include <QLabel>
class Method;
class MethodIcon : public QLabel
{
protected:
Method& m_method;
bool m_mouseLeftButtonPressed;
void mousePressEvent(QMouseEvent* event) final;
void mouseReleaseEvent(QMouseEvent* event) final;
public:
MethodIcon(Method& item, QIcon icon, QWidget* parent = nullptr);
};
#endif

Datei anzeigen

@ -29,6 +29,7 @@
#include <version.hpp>
#include <traintastic/locale/locale.hpp>
#include <traintastic/utils/standardpaths.hpp>
#include "../../misc/methodaction.hpp"
#include "../../network/object.hpp"
#include "../../network/property.hpp"
#include "../../network/method.hpp"
@ -108,6 +109,17 @@ void LuaScriptEditWidget::buildForm()
m_stop->setEnabled(value.toBool());
});
if(auto* method = m_object->getMethod("clear_persistent_variables"))
{
QWidget* spacer = new QWidget(this);
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
spacer->show();
toolbar->addWidget(spacer);
toolbar->addAction(new MethodAction(Theme::getIcon("clear_persistent_variables"), *method, this));
}
QWidget* spacer = new QWidget(this);
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
spacer->show();

Datei anzeigen

@ -40,6 +40,7 @@
#include "../propertydirectioncontrol.hpp"
#include "../propertyvaluelabel.hpp"
#include "../methodpushbutton.hpp"
#include "../unitpropertycombobox.hpp"
#include "../unitpropertyedit.hpp"
#include "../createwidget.hpp"
#include "../../theme/theme.hpp"
@ -107,7 +108,16 @@ void ObjectEditWidget::buildForm()
{
Property* property = static_cast<Property*>(baseProperty);
if(UnitProperty* unitProperty = dynamic_cast<UnitProperty*>(property))
w = new UnitPropertyEdit(*unitProperty, this);
{
if(unitProperty->hasAttribute(AttributeName::Values))
{
w = new UnitPropertyComboBox(*unitProperty, this);
}
else
{
w = new UnitPropertyEdit(*unitProperty, this);
}
}
else if(!property->isWritable())
w = new PropertyValueLabel(*property, this);
else if(property->type() == ValueType::Boolean)

Datei anzeigen

@ -146,10 +146,11 @@ ObjectListWidget::ObjectListWidget(const ObjectPtr& object_, QWidget* parent) :
method->argumentTypes().size() == 1 && method->argumentTypes()[0] == ValueType::Object &&
method->resultType() == ValueType::Invalid)
{
const bool multiSelect = object_->classId() == "list.train_vehicle";
m_actionAdd = m_toolbar->addAction(Theme::getIcon("add"), method->displayName(),
[this, method]()
[this, method, multiSelect]()
{
std::make_unique<ObjectSelectListDialog>(*method, this)->exec();
std::make_unique<ObjectSelectListDialog>(*method, multiSelect, this)->exec();
});
m_actionAdd->setEnabled(method->getAttributeBool(AttributeName::Enabled, true));
connect(method, &Method::attributeChanged, this,
@ -398,6 +399,16 @@ ObjectListWidget::ObjectListWidget(const ObjectPtr& object_, QWidget* parent) :
}
}
if(auto* method = object()->getMethod("clear_persistent_variables"))
{
QWidget* spacer = new QWidget(this);
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
spacer->show();
m_toolbar->addWidget(spacer);
m_toolbar->addAction(new MethodAction(Theme::getIcon("clear_persistent_variables"), *method, this));
}
if(!m_toolbar->actions().empty())
{
static_cast<QVBoxLayout*>(this->layout())->insertWidget(0, m_toolbar);

Datei anzeigen

@ -0,0 +1,41 @@
/**
* client/src/widget/objectlist/trainlistwidget.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "trainlistwidget.hpp"
#include <QDrag>
#include "../tablewidget.hpp"
#include "../../misc/mimedata.hpp"
TrainListWidget::TrainListWidget(const ObjectPtr& object, QWidget* parent)
: ThrottleObjectListWidget(object, parent)
{
connect(m_tableWidget, &TableWidget::rowDragged,
[this](int row)
{
if(auto trainId = m_tableWidget->getRowObjectId(row); !trainId.isEmpty())
{
QDrag* drag = new QDrag(m_tableWidget);
drag->setMimeData(new AssignTrainMimeData(trainId));
drag->exec(Qt::CopyAction);
}
});
}

Datei anzeigen

@ -1,9 +1,9 @@
/**
* server/src/enum/turnoutposition.hpp
* client/src/widget/objectlist/trainlistwidget.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2020-2021 Reinder Feenstra
* Copyright (C) 2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -20,9 +20,15 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_ENUM_TURNOUTPOSITION_HPP
#define TRAINTASTIC_SERVER_ENUM_TURNOUTPOSITION_HPP
#ifndef TRAINTASTIC_CLIENT_WIDGET_OBJECTLIST_TRAINLISTWIDGET_HPP
#define TRAINTASTIC_CLIENT_WIDGET_OBJECTLIST_TRAINLISTWIDGET_HPP
#include <traintastic/enum/turnoutposition.hpp>
#include "throttleobjectlistwidget.hpp"
class TrainListWidget : public ThrottleObjectListWidget
{
public:
explicit TrainListWidget(const ObjectPtr& object, QWidget* parent = nullptr);
};
#endif

Datei anzeigen

@ -161,7 +161,7 @@ OutputKeyboardWidget::OutputKeyboardWidget(std::shared_ptr<OutputKeyboard> objec
connect(led, &LEDWidget::clicked, this,
[this, index=i]()
{
const uint32_t address = static_cast<uint32_t>(m_addressMin->toInt64()) + m_page * m_leds.size() / 2 + index / 2;
const uint32_t address = static_cast<uint32_t>(m_addressMin->toInt64()) + m_page * static_cast<uint32_t>(m_leds.size()) / 2 + index / 2;
const auto value = (index & 0x1) ? OutputPairValue::Second : OutputPairValue::First;
callMethod(*m_setOutputValue, nullptr, address, value);
});
@ -251,7 +251,7 @@ uint32_t OutputKeyboardWidget::pageCount() const
{
leds *= 2;
}
return static_cast<uint32_t>(leds + m_leds.size() - 1) / m_leds.size();
return static_cast<uint32_t>(leds + m_leds.size() - 1) / static_cast<uint32_t>(m_leds.size());
}
void OutputKeyboardWidget::setPage(uint32_t value)
@ -277,7 +277,7 @@ LEDWidget* OutputKeyboardWidget::getLED(uint32_t address)
{
assert(m_object->outputType() == OutputType::Single);
const uint32_t first = static_cast<uint32_t>(m_addressMin->toInt64()) + m_page * m_leds.size();
const uint32_t first = static_cast<uint32_t>(m_addressMin->toInt64()) + m_page * static_cast<uint32_t>(m_leds.size());
if(address >= first && (address - first) < m_leds.size())
return m_leds[address - first];
@ -289,7 +289,7 @@ std::pair<LEDWidget*, LEDWidget*> OutputKeyboardWidget::getLEDs(uint32_t address
{
assert(m_object->outputType() == OutputType::Pair);
const uint32_t first = static_cast<uint32_t>(m_addressMin->toInt64()) + m_page * m_leds.size() / 2;
const uint32_t first = static_cast<uint32_t>(m_addressMin->toInt64()) + m_page * static_cast<uint32_t>(m_leds.size()) / 2;
if(address >= first && (address - first) < m_leds.size())
return {m_leds[(address - first) * 2], m_leds[(address - first) * 2 + 1]};
@ -309,7 +309,7 @@ void OutputKeyboardWidget::updateLEDs()
{
case OutputType::Single:
{
uint32_t address = addressMin + m_page * m_leds.size();
uint32_t address = addressMin + m_page * static_cast<uint32_t>(m_leds.size());
for(auto* led : m_leds)
{
const auto& outputState = m_object->getOutputState(address);
@ -329,7 +329,7 @@ void OutputKeyboardWidget::updateLEDs()
}
case OutputType::Pair:
{
uint32_t address = addressMin + m_page * m_leds.size() / 2;
uint32_t address = addressMin + m_page * static_cast<uint32_t>(m_leds.size()) / 2;
bool second = false;
for(auto* led : m_leds)
{

Datei anzeigen

@ -27,14 +27,20 @@
#include <QTableWidget>
#include <QHeaderView>
#include <QPushButton>
#include <QEvent>
#include <traintastic/locale/locale.hpp>
#include "createwidget.hpp"
#include "interfaceitemnamelabel.hpp"
#include "propertycheckbox.hpp"
#include "propertycombobox.hpp"
#include "propertypairoutputaction.hpp"
#include "propertyspinbox.hpp"
#include "objectpropertycombobox.hpp"
#include "propertyaddresses.hpp"
#include "outputmapoutputactionwidget.hpp"
#include "methodicon.hpp"
#include "../board/tilepainter.hpp"
#include "../board/getboardcolorscheme.hpp"
#include "../dialog/objectselectlistdialog.hpp"
#include "../network/callmethod.hpp"
#include "../network/method.hpp"
@ -46,9 +52,12 @@
#include "../theme/theme.hpp"
#include "../misc/methodaction.hpp"
constexpr int columnCountNonOutput = 2;
constexpr int columnUse = 0;
constexpr int columnKey = 1;
constexpr int columnKey = 0;
static bool hasUseColumn(const QString& classId)
{
return classId == "output_map.signal";
}
static void setComboBoxMinimumWidth(QComboBox* comboBox)
{
@ -58,10 +67,14 @@ static void setComboBoxMinimumWidth(QComboBox* comboBox)
OutputMapWidget::OutputMapWidget(ObjectPtr object, QWidget* parent)
: QWidget(parent)
, m_object{std::move(object)}
, m_hasUseColumn{hasUseColumn(m_object->classId())}
, m_columnCountNonOutput{m_hasUseColumn ? 2 : 1}
, m_addresses{m_object->getVectorProperty("addresses")}
, m_ecosObject{dynamic_cast<Property*>(m_object->getProperty("ecos_object"))}
, m_items{m_object->getObjectVectorProperty("items")}
, m_table{new QTableWidget(this)}
, m_getParentRequestId{Connection::invalidRequestId}
, m_getItemsRequestId{Connection::invalidRequestId}
{
QVBoxLayout* l = new QVBoxLayout();
@ -92,16 +105,49 @@ OutputMapWidget::OutputMapWidget(ObjectPtr object, QWidget* parent)
}
l->addLayout(form);
m_table->setColumnCount(columnCountNonOutput);
const int listViewIconSize = m_table->style()->pixelMetric(QStyle::PM_ListViewIconSize);
m_table->setIconSize({listViewIconSize, listViewIconSize});
m_table->setColumnCount(m_columnCountNonOutput);
m_table->setRowCount(0);
m_table->setHorizontalHeaderLabels({Locale::tr("output_map:use"), Locale::tr(m_object->classId() + ":key")});
QStringList labels;
labels.append(Locale::tr(m_object->classId() + ":key"));
if(m_hasUseColumn)
{
labels.append(Locale::tr("output_map:use"));
}
m_table->setHorizontalHeaderLabels(labels);
m_table->verticalHeader()->setVisible(false);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
l->addWidget(m_table);
if(auto* swapOutputs = m_object->getMethod("swap_outputs"))
{
m_swapOutputs = new MethodIcon(*swapOutputs, Theme::getIcon("swap"), m_table);
if(!swapOutputs->getAttributeBool(AttributeName::Visible, true))
{
m_swapOutputs->hide();
}
m_table->installEventFilter(this);
m_swapOutputs->installEventFilter(this);
}
setLayout(l);
if(auto* parentObject = m_object->getObjectProperty("parent"))
{
m_getParentRequestId = parentObject->getObject(
[this](const ObjectPtr& obj, std::optional<const Error> ec)
{
if(obj && !ec)
{
m_getParentRequestId = Connection::invalidRequestId;
m_parentObject = obj;
updateKeyIcons();
}
});
}
if(m_items) /*[[likely]]*/
{
m_getItemsRequestId = m_items->getObjects(
@ -112,26 +158,30 @@ OutputMapWidget::OutputMapWidget(ObjectPtr object, QWidget* parent)
updateItems(objects);
}
});
connect(&BoardSettings::instance(), &BoardSettings::changed, this, &OutputMapWidget::updateKeyIcons);
}
}
OutputMapWidget::~OutputMapWidget()
{
if(m_getParentRequestId != Connection::invalidRequestId)
{
m_object->connection()->cancelRequest(m_getParentRequestId);
}
if(m_getItemsRequestId != Connection::invalidRequestId)
{
m_object->connection()->cancelRequest(m_getItemsRequestId);
}
}
void OutputMapWidget::updateItems(const std::vector<ObjectPtr>& items)
{
m_table->setRowCount(items.size());
m_table->setRowCount(static_cast<int>(items.size()));
m_itemObjects = items;
m_actions.resize(items.size());
for(size_t i = 0; i < items.size(); i++)
{
if(auto* p = dynamic_cast<Property*>(items[i]->getProperty("use")))
{
QWidget* w = new QWidget(m_table);
QHBoxLayout* l = new QHBoxLayout();
l->setAlignment(Qt::AlignCenter);
l->addWidget(new PropertyCheckBox(*p, w));
w->setLayout(l);
m_table->setCellWidget(i, columnUse, w);
}
if(auto* p = items[i]->getProperty("key"))
{
QString text;
@ -162,34 +212,116 @@ void OutputMapWidget::updateItems(const std::vector<ObjectPtr>& items)
assert(false);
text = "?";
}
m_table->setItem(i, columnKey, new QTableWidgetItem(text));
m_table->setItem(static_cast<int>(i), columnKey, new QTableWidgetItem(text));
}
if(m_hasUseColumn)
{
const int columnUse = columnKey + 1;
if(auto* p = dynamic_cast<Property*>(items[i]->getProperty("use")))
{
QWidget* w = new QWidget(m_table);
QHBoxLayout* l = new QHBoxLayout();
l->setAlignment(Qt::AlignCenter);
l->addWidget(new PropertyCheckBox(*p, w));
w->setLayout(l);
m_table->setCellWidget(static_cast<int>(i), columnUse, w);
}
}
if(auto* p = items[i]->getProperty("visible"))
{
m_table->setRowHidden(static_cast<int>(i), !p->toBool());
connect(p, &Property::valueChangedBool, this,
[this, row=static_cast<int>(i)](bool value)
{
m_table->setRowHidden(row, !value);
});
}
if(auto* outputActions = dynamic_cast<ObjectVectorProperty*>(items[i]->getVectorProperty("output_actions")))
{
updateTableOutputActions(*outputActions, i);
updateTableOutputActions(*outputActions, static_cast<int>(i));
connect(outputActions, &ObjectVectorProperty::valueChanged, this,
[this, row=i]()
[this, row=static_cast<int>(i)]()
{
updateTableOutputActions(*dynamic_cast<ObjectVectorProperty*>(sender()), row);
});
}
}
updateKeyIcons();
updateTableOutputColumns();
}
void OutputMapWidget::updateKeyIcons()
{
if(!m_parentObject)
{
return;
}
if(auto tileIdProperty = m_parentObject->getProperty("tile_id"))
{
const bool darkBackground = m_table->palette().window().color().lightnessF() < 0.5;
const auto tileId = tileIdProperty->toEnum<TileId>();
const int iconSize = m_table->iconSize().height();
QImage image(iconSize, iconSize, QImage::Format_ARGB32);
QPainter painter{&image};
painter.setRenderHint(QPainter::Antialiasing, true);
TilePainter tilePainter{painter, iconSize, *getBoardColorScheme(darkBackground ? BoardSettings::ColorScheme::Dark : BoardSettings::ColorScheme::Light)};
for(size_t i = 0; i < m_itemObjects.size(); i++)
{
if(auto* key = m_itemObjects[i]->getProperty("key"))
{
image.fill(Qt::transparent);
if(isRailTurnout(tileId))
{
tilePainter.drawTurnout(tileId, image.rect(), TileRotate::Deg0, TurnoutPosition::Unknown, static_cast<TurnoutPosition>(key->toInt()));
}
else if(isRailSignal(tileId))
{
tilePainter.drawSignal(tileId, image.rect(), TileRotate::Deg0, false, static_cast<SignalAspect>(key->toInt()));
}
else if(tileId == TileId::RailDirectionControl)
{
tilePainter.drawDirectionControl(tileId, image.rect(), TileRotate::Deg0, false, static_cast<DirectionControlState>(key->toInt()));
}
else if(tileId == TileId::RailDecoupler)
{
tilePainter.drawRailDecoupler(image.rect(), TileRotate::Deg90, false, static_cast<DecouplerState>(key->toInt()));
}
else if(tileId == TileId::Switch)
{
tilePainter.drawSwitch(image.rect(), key->toBool());
}
else
{
break; // tileId not supported (yet)
}
m_table->item(static_cast<int>(i), columnKey)->setIcon(QPixmap::fromImage(image));
}
}
}
}
void OutputMapWidget::updateTableOutputColumns()
{
if(m_addresses && m_addresses->getAttributeBool(AttributeName::Visible, true))
{
const auto size = m_addresses->size();
m_table->setColumnCount(columnCountNonOutput + size);
m_table->setColumnCount(m_columnCountNonOutput + size);
for(int i = 0; i < size; i++)
{
const int column = columnCountNonOutput + i;
const int column = m_columnCountNonOutput + i;
const int address = m_addresses->getInt(i);
auto* item = new QTableWidgetItem(QString("#%1").arg(address));
item->setToolTip(Locale::tr("output_map:address_x").arg(address));
@ -198,15 +330,26 @@ void OutputMapWidget::updateTableOutputColumns()
}
else if(m_ecosObject && m_ecosObject->getAttributeBool(AttributeName::Visible, true))
{
m_table->setColumnCount(columnCountNonOutput + 1);
m_table->setHorizontalHeaderItem(columnCountNonOutput, new QTableWidgetItem(Locale::tr("output.ecos_object:state")));
m_table->setColumnCount(m_columnCountNonOutput + 1);
m_table->setHorizontalHeaderItem(m_columnCountNonOutput, new QTableWidgetItem(Locale::tr("output.ecos_object:state")));
}
else
{
m_table->setColumnCount(columnCountNonOutput);
m_table->setColumnCount(m_columnCountNonOutput);
}
}
bool OutputMapWidget::eventFilter(QObject* object, QEvent* event)
{
if(m_swapOutputs && ((object == m_table && event->type() == QEvent::Resize) || (object == m_swapOutputs && event->type() == QEvent::Show)))
{
auto pnt = m_swapOutputs->rect().bottomRight();
pnt = m_table->rect().bottomRight() - pnt - pnt / 4;
m_swapOutputs->move(pnt.x(), pnt.y());
}
return QWidget::eventFilter(object, event);
}
void OutputMapWidget::updateTableOutputActions(ObjectVectorProperty& property, int row)
{
if(!property.empty())
@ -214,21 +357,21 @@ void OutputMapWidget::updateTableOutputActions(ObjectVectorProperty& property, i
m_dummy = property.getObjects(
[this, row](const std::vector<ObjectPtr>& objects, std::optional<const Error> /*ec*/)
{
const int columnCount = static_cast<int>(columnCountNonOutput + objects.size());
const int columnCount = static_cast<int>(m_columnCountNonOutput + objects.size());
if(columnCount > m_table->columnCount())
{
m_table->setColumnCount(columnCount);
}
auto& rowActions = m_actions[row];
int column = columnCountNonOutput;
int column = m_columnCountNonOutput;
for(auto& object : objects)
{
if(column >= static_cast<int>(rowActions.size()) || object.get() != rowActions[column].get())
{
if(auto* action = dynamic_cast<Property*>(object->getProperty("action")))
{
m_table->setCellWidget(row, column, new PropertyComboBox(*action, this));
m_table->setCellWidget(row, column, createWidget(*action, this));
}
else if(auto* aspect = dynamic_cast<Property*>(object->getProperty("aspect")))
{

Datei anzeigen

@ -29,6 +29,7 @@
class QTableWidget;
class Method;
class MethodAction;
class MethodIcon;
class AbstractProperty;
class AbstractVectorProperty;
class ObjectVectorProperty;
@ -40,21 +41,30 @@ class OutputMapWidget : public QWidget
protected:
ObjectPtr m_object;
ObjectPtr m_parentObject;
const bool m_hasUseColumn;
const int m_columnCountNonOutput;
AbstractVectorProperty* m_addresses;
Property* m_ecosObject;
ObjectVectorProperty* m_items;
QTableWidget* m_table;
MethodIcon* m_swapOutputs = nullptr;
std::vector<ObjectPtr> m_itemObjects;
std::vector<std::vector<ObjectPtr>> m_actions;
int m_getParentRequestId;
int m_getItemsRequestId;
int m_dummy;
void updateTableOutputActions(ObjectVectorProperty& property, int row);
void updateItems(const std::vector<ObjectPtr>& items);
void updateKeyIcons();
void updateTableOutputColumns();
bool eventFilter(QObject* object, QEvent* event) override;
public:
explicit OutputMapWidget(ObjectPtr object, QWidget* parent = nullptr);
~OutputMapWidget() override;
};
#endif

Datei anzeigen

@ -189,6 +189,7 @@ PropertyLuaCodeEdit::Highlighter::Highlighter(QTextDocument* parent) :
QStringLiteral("\\bworld(?=\\.)"),
QStringLiteral("\\bset(?=\\.)"),
QStringLiteral("\\benum(?=\\.)"),
QStringLiteral("\\bpv(?=\\.)"),
};
for(const auto& regex : globals)
m_rules.append(Rule(regex, QColor(0xFF, 0x8C, 0x00)));

Datei anzeigen

@ -0,0 +1,168 @@
/**
* client/src/widget/propertypairoutputaction.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "propertypairoutputaction.hpp"
#include <QKeyEvent>
#include <QPainter>
#include "../network/property.hpp"
PropertyPairOutputAction::PropertyPairOutputAction(Property& property, QWidget* parent)
: QWidget(parent)
, m_property{property}
{
assert(m_property.enumName() == "pair_output_action");
setFocusPolicy(Qt::StrongFocus);
connect(&m_property, &Property::valueChanged, this,
[this]()
{
update(rect());
});
}
PairOutputAction PropertyPairOutputAction::value() const
{
return m_property.toEnum<PairOutputAction>();
}
void PropertyPairOutputAction::setValue(PairOutputAction newValue)
{
m_property.setValueEnum(newValue);
}
void PropertyPairOutputAction::toggleValue()
{
switch(value())
{
case PairOutputAction::None:
setValue(PairOutputAction::First);
break;
case PairOutputAction::First:
setValue(PairOutputAction::Second);
break;
case PairOutputAction::Second:
default:
setValue(PairOutputAction::None);
break;
}
}
void PropertyPairOutputAction::toggleValue(PairOutputAction action)
{
setValue(action == value() ? PairOutputAction::None : action);
}
void PropertyPairOutputAction::keyPressEvent(QKeyEvent* event)
{
switch(event->key())
{
case Qt::Key_Enter:
case Qt::Key_Space:
toggleValue();
return;
case Qt::Key_1:
case Qt::Key_R:
toggleValue(PairOutputAction::First);
return;
case Qt::Key_2:
case Qt::Key_G:
toggleValue(PairOutputAction::Second);
return;
}
QWidget::keyPressEvent(event);
}
void PropertyPairOutputAction::mousePressEvent(QMouseEvent* event)
{
if(event->button() == Qt::LeftButton)
{
m_mouseLeftClickPos = event->pos();
}
}
void PropertyPairOutputAction::mouseReleaseEvent(QMouseEvent* event)
{
if(event->button() == Qt::LeftButton && m_mouseLeftClickPos)
{
const auto [first, second] = outputRects();
if(first.contains(*m_mouseLeftClickPos) && first.contains(event->pos()))
{
toggleValue(PairOutputAction::First);
}
else if(second.contains(*m_mouseLeftClickPos) && second.contains(event->pos()))
{
toggleValue(PairOutputAction::Second);
}
m_mouseLeftClickPos.reset();
}
}
void PropertyPairOutputAction::paintEvent(QPaintEvent* /*event*/)
{
constexpr int thinkness = 2;
const QColor firstOnColor(Qt::red);
const QColor secondOnColor(Qt::green);
const auto textOnColor = palette().color(QPalette::Active, QPalette::WindowText);
const auto offColor = palette().color(QPalette::Disabled, QPalette::WindowText);
const auto [left, right] = outputRects();
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.save();
QPen p(painter.pen());
p.setWidth(thinkness);
p.setColor(value() == PairOutputAction::First ? firstOnColor : offColor);
painter.setPen(p);
painter.drawEllipse(left.adjusted(thinkness, thinkness, -thinkness, -thinkness));
p.setColor(value() == PairOutputAction::Second ? secondOnColor : offColor);
painter.setPen(p);
painter.drawEllipse(right.adjusted(thinkness, thinkness, -thinkness, -thinkness));
painter.restore();
painter.setPen(value() == PairOutputAction::First ? textOnColor : offColor);
painter.drawText(left, Qt::AlignCenter, "R");
painter.setPen(value() == PairOutputAction::Second ? textOnColor : offColor);
painter.drawText(right, Qt::AlignCenter, "G");
}
std::pair<QRect, QRect> PropertyPairOutputAction::outputRects() const
{
constexpr int margin = 1;
const int height = rect().height();
const int hCenter = rect().width() / 2;
return {
{hCenter - margin - height, 0, height, height},
{hCenter + margin, 0, height, height}};
}

Datei anzeigen

@ -0,0 +1,55 @@
/**
* client/src/widget/propertypairoutputaction.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_PROPERTYPAIROUTPUTACTION_HPP
#define TRAINTASTIC_CLIENT_WIDGET_PROPERTYPAIROUTPUTACTION_HPP
#include <optional>
#include <QWidget>
#include <traintastic/enum/pairoutputaction.hpp>
class Property;
class PropertyPairOutputAction : public QWidget
{
private:
Property& m_property;
std::optional<QPoint> m_mouseLeftClickPos = std::nullopt;
std::pair<QRect, QRect> outputRects() const;
protected:
void keyPressEvent(QKeyEvent* event) final;
void mousePressEvent(QMouseEvent* event) final;
void mouseReleaseEvent(QMouseEvent* event) final;
void paintEvent(QPaintEvent* event) final;
public:
PropertyPairOutputAction(Property& property, QWidget* parent = nullptr);
PairOutputAction value() const;
void setValue(PairOutputAction action);
void toggleValue();
void toggleValue(PairOutputAction output);
};
#endif

Datei anzeigen

@ -35,7 +35,11 @@ PropertySpinBox::PropertySpinBox(Property& property, QWidget* parent) :
Q_ASSERT(m_property.type() == ValueType::Integer);
setEnabled(m_property.getAttributeBool(AttributeName::Enabled, true));
setVisible(m_property.getAttributeBool(AttributeName::Visible, true));
setRange(std::numeric_limits<int>::min(), std::numeric_limits<int>::max());
updateRange();
if(auto unit = m_property.getAttributeString(AttributeName::Unit, ""); !unit.isEmpty())
{
setSuffix(unit.prepend(" "));
}
setValue(m_property.toInt());
connect(&m_property, &AbstractProperty::valueChangedInt, this,
[this](int value)
@ -58,6 +62,22 @@ PropertySpinBox::PropertySpinBox(Property& property, QWidget* parent) :
setVisible(value.toBool());
break;
case AttributeName::Min:
case AttributeName::Max:
updateRange();
break;
case AttributeName::Unit:
if(auto unit = value.toString(); !unit.isEmpty())
{
setSuffix(unit.prepend(" "));
}
else
{
setSuffix("");
}
break;
default:
break;
}
@ -105,3 +125,10 @@ void PropertySpinBox::focusOutEvent(QFocusEvent* event)
QSpinBox::focusOutEvent(event);
setValue(m_property.toInt());
}
void PropertySpinBox::updateRange()
{
setRange(
m_property.getAttributeInt(AttributeName::Min, std::numeric_limits<int>::min()),
m_property.getAttributeInt(AttributeName::Max, std::numeric_limits<int>::max()));
}

Datei anzeigen

@ -36,6 +36,8 @@ class PropertySpinBox : public QSpinBox
void cancelRequest();
void showError(const QString& error);
void updateRange();
protected:
void focusOutEvent(QFocusEvent* event) override;

Datei anzeigen

@ -0,0 +1,66 @@
/**
* client/src/widget/status/simulationstatuswidget.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "simulationstatuswidget.hpp"
#include <QResizeEvent>
#include <traintastic/locale/locale.hpp>
#include "../../network/object.hpp"
#include "../../network/property.hpp"
#include "../../theme/theme.hpp"
SimulationStatusWidget::SimulationStatusWidget(const ObjectPtr& object, QWidget* parent)
: QSvgWidget(parent)
, m_object{object}
{
assert(m_object);
assert(m_object->classId() == "status.simulation");
load(Theme::getIconFile("simulation"));
if(auto* property = m_object->getProperty("label"))
{
connect(property, &Property::valueChanged, this, &SimulationStatusWidget::labelChanged);
}
labelChanged();
}
void SimulationStatusWidget::labelChanged()
{
QString label;
if(auto* property = m_object->getProperty("label"))
{
label = Locale::instance->parse(property->toString());
}
setToolTip(label);
}
void SimulationStatusWidget::resizeEvent(QResizeEvent* event)
{
QSvgWidget::resizeEvent(event);
// force same width as height:
setMinimumWidth(event->size().height());
setMaximumWidth(event->size().height());
}

Datei anzeigen

@ -0,0 +1,44 @@
/**
* client/src/widget/status/simulationstatuswidget.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_STATUS_SIMULATIONSTATUSWIDGET_HPP
#define TRAINTASTIC_CLIENT_WIDGET_STATUS_SIMULATIONSTATUSWIDGET_HPP
#include <QSvgWidget>
#include "../../network/objectptr.hpp"
class SimulationStatusWidget : public QSvgWidget
{
private:
ObjectPtr m_object;
void labelChanged();
void stateChanged();
protected:
void resizeEvent(QResizeEvent* event) override;
public:
explicit SimulationStatusWidget(const ObjectPtr& object, QWidget* parent = nullptr);
};
#endif

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2021,2023 Reinder Feenstra
* Copyright (C) 2019-2021,2023-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -24,6 +24,8 @@
#include <QHeaderView>
#include <QScrollBar>
#include <QSettings>
#include <QMouseEvent>
#include <QApplication>
#include "../network/tablemodel.hpp"
TableWidget::TableWidget(QWidget* parent) :
@ -113,3 +115,23 @@ void TableWidget::updateRegion()
m_model->setRegion(columnMin, columnMax, rowMin, rowMax);
}
void TableWidget::mouseMoveEvent(QMouseEvent* event)
{
QTableView::mouseMoveEvent(event);
if(event->button() == Qt::LeftButton)
{
m_dragStartPosition = event->pos();
}
}
void TableWidget::mousePressEvent(QMouseEvent* event)
{
QTableView::mousePressEvent(event);
if((event->buttons() & Qt::LeftButton) && (event->pos() - m_dragStartPosition).manhattanLength() >= QApplication::startDragDistance())
{
emit rowDragged(indexAt(m_dragStartPosition).row());
}
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2020,2023 Reinder Feenstra
* Copyright (C) 2019-2020,2023-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -33,6 +33,10 @@ class TableWidget : public QTableView
protected:
TableModelPtr m_model;
int m_selectedRow = -1;
QPoint m_dragStartPosition;
void mouseMoveEvent(QMouseEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
protected slots:
void updateRegion();
@ -44,6 +48,9 @@ class TableWidget : public QTableView
QString getRowObjectId(int row) const;
void setTableModel(const TableModelPtr& model);
signals:
void rowDragged(int row);
};
#endif

Datei anzeigen

@ -0,0 +1,193 @@
/**
* client/src/widget/unitpropertycombobox.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "unitpropertycombobox.hpp"
#include <QHBoxLayout>
#include <QComboBox>
#include <traintastic/locale/locale.hpp>
#include "../network/unitproperty.hpp"
#include "../utils/internalupdateholder.hpp"
#include "../utils/enum.hpp"
UnitPropertyComboBox::UnitPropertyComboBox(UnitProperty& property, QWidget* parent)
: QWidget(parent)
, m_property{property}
, m_valueComboBox{new QComboBox(this)}
, m_unitComboBox{new QComboBox(this)}
{
setEnabled(m_property.getAttributeBool(AttributeName::Enabled, true));
setVisible(m_property.getAttributeBool(AttributeName::Visible, true));
connect(&m_property, &UnitProperty::attributeChanged, this,
[this](AttributeName name, const QVariant& value)
{
switch(name)
{
case AttributeName::Enabled:
setEnabled(value.toBool());
break;
case AttributeName::Visible:
setVisible(value.toBool());
break;
case AttributeName::AliasKeys:
case AttributeName::AliasValues:
case AttributeName::Values:
updateValues();
break;
default:
break;
}
});
connect(&m_property, &Property::valueChangedDouble, this,
[this](double value)
{
InternalUpdateHolder hold(m_internalUpdate);
if(int index = m_valueComboBox->findData(value); index != -1) // predefined value
{
m_valueComboBox->setCurrentIndex(index);
}
else // custom value
{
m_valueComboBox->setCurrentText(QString::number(value));
}
});
m_valueComboBox->setEditable(true); // FIXME
m_valueComboBox->setInsertPolicy(QComboBox::NoInsert);
if(m_valueComboBox->isEditable())
{
connect(m_valueComboBox, &QComboBox::currentTextChanged,
[this](const QString& value)
{
if(m_internalUpdate == 0)
{
const QVariant v = m_valueComboBox->currentData();
if(v.isValid()) // predefined value
{
m_property.setValueDouble(v.value<double>());
}
else // custom value
{
m_property.setValueString(value);
}
}
});
}
else
{
connect(m_valueComboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
[this](int)
{
if(m_internalUpdate == 0)
{
const QVariant v = m_valueComboBox->currentData();
m_property.setValueDouble(v.value<double>());
}
});
}
QHBoxLayout* l = new QHBoxLayout();
l->setContentsMargins(0, 0, 0, 0);
l->addWidget(m_valueComboBox, 1);
for(qint64 value : enumValues(m_property.unitName()))
{
m_unitComboBox->addItem(translateEnum(m_property.unitName(), value), value);
if(m_property.unitValue() == value)
m_unitComboBox->setCurrentIndex(m_unitComboBox->count() - 1);
}
l->addWidget(m_unitComboBox);
connect(m_unitComboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
[this](int)
{
if(QVariant v = m_unitComboBox->currentData(); v.canConvert<qint64>())
if(qint64 value = v.toLongLong(); value != m_property.unitValue())
m_property.setUnitValue(value);
});
setLayout(l);
updateValues();
}
void UnitPropertyComboBox::updateValues()
{
QVariant values = m_property.getAttribute(AttributeName::Values, QVariant());
if(Q_LIKELY(values.isValid()))
{
InternalUpdateHolder hold(m_internalUpdate);
m_valueComboBox->clear();
if(Q_LIKELY(values.userType() == QMetaType::QVariantList))
{
bool currentIndexSet = false;
switch(m_property.type())
{
case ValueType::Float:
{
const QVariantList aliasKeys = m_property.getAttribute(AttributeName::AliasKeys, QVariant()).toList();
const QVariantList aliasValues = m_property.getAttribute(AttributeName::AliasValues, QVariant()).toList();
for(QVariant& v : values.toList())
{
const auto value = v.toDouble();
if(int index = aliasKeys.indexOf(v); index != -1)
{
m_valueComboBox->addItem(Locale::instance->parse(aliasValues[index].toString()), value);
}
else
{
m_valueComboBox->addItem(QString::number(value), value);
}
if(qFuzzyCompare(m_property.toDouble(), value) || m_property.toDouble() == value)
{
m_valueComboBox->setCurrentIndex(m_valueComboBox->count() - 1);
currentIndexSet = true;
}
}
break;
}
case ValueType::Invalid:
case ValueType::Boolean:
case ValueType::Integer:
case ValueType::Enum:
case ValueType::String:
case ValueType::Object:
case ValueType::Set:
assert(false);
break;
}
if(m_valueComboBox->isEditable() && !currentIndexSet)
{
m_valueComboBox->setCurrentText(m_property.toString());
}
}
}
}

Datei anzeigen

@ -0,0 +1,47 @@
/**
* client/src/widget/unitpropertycombobox.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_UNITPROPERTYCOMBOBOX_HPP
#define TRAINTASTIC_CLIENT_WIDGET_UNITPROPERTYCOMBOBOX_HPP
#include <QWidget>
class UnitProperty;
class QComboBox;
class UnitPropertyComboBox : public QWidget
{
Q_OBJECT
private:
UnitProperty& m_property;
size_t m_internalUpdate = 0;
QComboBox* m_valueComboBox;
QComboBox* m_unitComboBox;
void updateValues();
public:
explicit UnitPropertyComboBox(UnitProperty& property, QWidget* parent = nullptr);
};
#endif

Datei anzeigen

@ -225,13 +225,17 @@ class RadioPageJSON : public RadioPage, public PageJSON
void initializePage() override
{
setTitleAndText(*static_cast<JSONWizard*>(wizard()), this, m_pageData);
auto* jsonWizard = static_cast<JSONWizard*>(wizard());
setTitleAndText(*jsonWizard, this, m_pageData);
for(const auto& option : m_pageData["options"].toArray())
{
auto item = option.toObject();
addItem(static_cast<JSONWizard*>(wizard())->translateAndReplaceVariables(item["name"].toString()), item["disabled"].toBool());
addItem(jsonWizard->translateAndReplaceVariables(item["name"].toString()), item["checked"].toBool(), item["disabled"].toBool());
}
setBottomText(jsonWizard->translateAndReplaceVariables(m_pageData["bottom_text"].toString()));
}
void cleanupPage() override
@ -469,4 +473,4 @@ Properties JSONWizard::toProperties(const QJsonObject& object)
}
}
return properties;
}
}

Datei anzeigen

@ -24,11 +24,19 @@
#include <QLayout>
#include <QButtonGroup>
#include <QRadioButton>
#include <QLabel>
RadioPage::RadioPage(QWidget* parent)
: TextPage(parent)
, m_group{new QButtonGroup(this)}
, m_bottomText{new QLabel(this)}
{
m_bottomText->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
m_bottomText->setWordWrap(true);
setBottomText({});
static_cast<QVBoxLayout*>(layout())->addStretch();
layout()->addWidget(m_bottomText);
}
int RadioPage::currentIndex() const
@ -36,23 +44,25 @@ int RadioPage::currentIndex() const
return m_group->id(m_group->checkedButton());
}
void RadioPage::addItem(const QString& label, bool disabled)
void RadioPage::addItem(const QString& label, bool checked, bool disabled)
{
auto* button = new QRadioButton(label);
button->setChecked(checked && !disabled);
button->setDisabled(disabled);
m_group->addButton(button, m_group->buttons().size());
layout()->addWidget(button);
static_cast<QVBoxLayout*>(layout())->insertWidget(layout()->count() - 2, button);
}
void RadioPage::clear()
{
while(layout()->count() > 1) // remove all but first (=text label)
for(auto* button : m_group->buttons())
{
auto* item = layout()->itemAt(layout()->count() - 1);
if(item->widget())
{
delete item->widget();
}
layout()->removeItem(item);
delete button;
}
}
void RadioPage::setBottomText(const QString& text)
{
m_bottomText->setText(text);
m_bottomText->setVisible(!text.isEmpty());
}

Datei anzeigen

@ -31,14 +31,17 @@ class RadioPage : public TextPage
{
protected:
QButtonGroup* m_group;
QLabel* m_bottomText;
public:
explicit RadioPage(QWidget* parent = nullptr);
int currentIndex() const;
void addItem(const QString& label, bool disabled = false);
void addItem(const QString& label, bool checked = false, bool disabled = false);
void clear();
void setBottomText(const QString& text);
};
#endif

Datei anzeigen

@ -20,6 +20,7 @@ class LuaDoc:
DEFAULT_LANGUAGE = 'en-us'
FILENAME_INDEX = 'index.html'
FILENAME_GLOBALS = 'globals.html'
FILENAME_PV = 'pv.html'
FILENAME_ENUM = 'enum.html'
FILENAME_SET = 'set.html'
FILENAME_OBJECT = 'object.html'
@ -30,6 +31,7 @@ class LuaDoc:
version = None
def __init__(self, project_root: str) -> None:
self._project_root = project_root
self._globals = LuaDoc._find_globals(project_root)
self._enums = LuaDoc._find_enums(project_root)
self._sets = LuaDoc._find_sets(project_root)
@ -52,6 +54,7 @@ class LuaDoc:
definition = self._terms[term]
definition = re.sub(r'`(.+?)`', r'<code>\1</code>', definition)
definition = re.sub(r'\[([^\]]+)]\(([^\)]+)\)', r'<a href="\2">\1</a>', definition)
definition = re.sub(r'{ref:([a-z0-9_\.]+?)(|#[a-z0-9_]+)(|\|.+?)}', self._ref_link, definition)
return definition
@ -100,6 +103,16 @@ class LuaDoc:
for object in self._objects:
if object['lua_name'] == id:
return '<a href="' + object['filename'] + fragment + '">' + (self._get_term(object['name']) if title == '' else title) + '</a>'
elif id == 'globals':
return '<a href="' + self.FILENAME_GLOBALS + fragment + '">' + (self._get_term('globals:title') if title == '' else title) + '</a>'
elif id == 'enum':
return '<a href="' + self.FILENAME_ENUM + fragment + '">' + (self._get_term('enum:title') if title == '' else title) + '</a>'
elif id == 'set':
return '<a href="' + self.FILENAME_SET + fragment + '">' + (self._get_term('set:title') if title == '' else title) + '</a>'
elif id == 'object':
return '<a href="' + self.FILENAME_OBJECT + fragment + '">' + (self._get_term('object:title') if title == '' else title) + '</a>'
elif id == 'pv':
return '<a href="' + self.FILENAME_PV + fragment + '">' + (self._get_term('pv:title') if title == '' else title) + '</a>'
return '<span style="color:red">' + m.group(0) + '</span>'
@ -347,7 +360,7 @@ class LuaDoc:
hpp = LuaDoc._read_file(filename_hpp)
cpp = LuaDoc._read_file(filename_cpp) if os.path.exists(filename_cpp) else hpp
for cpp_type, cpp_template_type, cpp_item_name in re.findall(r'(Property|VectorProperty|ObjectProperty|ObjectVectorProperty|Method|Event)<(.*?)>\s+([A-Za-z0-9_]+);', hpp):
m = re.search(cpp_item_name + r'({|\()\s*[\*]?this\s*,\s*"([a-z0-9_]+)"[^}]*(PropertyFlags::ScriptReadOnly|PropertyFlags::ScriptReadWrite|MethodFlags::ScriptCallable|EventFlags::Scriptable)[^}]*}', cpp)
m = re.search(cpp_item_name + r'({|\()\s*[\*]?this\s*,\s*"([a-z0-9_]+)".*?(PropertyFlags::ScriptReadOnly|PropertyFlags::ScriptReadWrite|MethodFlags::ScriptCallable|EventFlags::Scriptable)[^}]*}', cpp)
if m is None:
continue
@ -446,6 +459,7 @@ class LuaDoc:
self._build_index(output_dir)
self._build_globals(output_dir, nav)
self._build_pv(output_dir, nav)
self._build_enums(output_dir, nav)
self._build_sets(output_dir, nav)
for _, lib in self._libs.items():
@ -648,6 +662,30 @@ class LuaDoc:
html += self._build_items_html(self._globals, 'globals.')
LuaDoc._write_file(os.path.join(output_dir, LuaDoc.FILENAME_GLOBALS), self._add_toc(html))
def _build_pv(self, output_dir: str, nav: list) -> None:
title = self._get_term('pv:title')
html = self._get_header(title, nav + [{'title': title, 'href': LuaDoc.FILENAME_PV}])
html += '<p>' + self._get_term('pv:paragraph_1') + '</p>' + os.linesep
html += '<p>' + self._get_term('pv:paragraph_2') + '</p>' + os.linesep
html += '<h2 id="storing">' + self._get_term('pv.storing:title') + '</h2>' + os.linesep
html += '<p>' + self._get_term('pv.storing:paragraph_1') + '</p>' + os.linesep
html += '<pre lang="lua"><code>' + highlight_lua(LuaDoc._read_file(os.path.join(self._project_root, 'manual', 'luadoc', 'example', 'pv', 'storingpersistentdata.lua'))) + '</code></pre>'
html += '<h2 id="retrieving">' + self._get_term('pv.retrieving:title') + '</h2>' + os.linesep
html += '<p>' + self._get_term('pv.retrieving:paragraph_1') + '</p>' + os.linesep
html += '<pre lang="lua"><code>' + highlight_lua(LuaDoc._read_file(os.path.join(self._project_root, 'manual', 'luadoc', 'example', 'pv', 'retrievingpersistentdata.lua'))) + '</code></pre>'
html += '<h2 id="deleting">' + self._get_term('pv.deleting:title') + '</h2>' + os.linesep
html += '<p>' + self._get_term('pv.deleting:paragraph_1') + '</p>' + os.linesep
html += '<pre lang="lua"><code>' + highlight_lua(LuaDoc._read_file(os.path.join(self._project_root, 'manual', 'luadoc', 'example', 'pv', 'deletingpersistentdata.lua'))) + '</code></pre>'
html += '<h2 id="checking">' + self._get_term('pv.checking:title') + '</h2>' + os.linesep
html += '<p>' + self._get_term('pv.checking:paragraph_1') + '</p>' + os.linesep
html += '<pre lang="lua"><code>' + highlight_lua(LuaDoc._read_file(os.path.join(self._project_root, 'manual', 'luadoc', 'example', 'pv', 'checkingforpersistentdata.lua'))) + '</code></pre>'
LuaDoc._write_file(os.path.join(output_dir, LuaDoc.FILENAME_PV), self._add_toc(html))
def _build_enums(self, output_dir: str, nav: list) -> None:
title = self._get_term('enum:title')
nav_enums = nav + [{'title': title, 'href': LuaDoc.FILENAME_ENUM}]
@ -708,7 +746,6 @@ class LuaDoc:
category['title'] = self._get_term('object.category.' + key + ':title')
category['items'] = []
for object in category['objects']:
print(object)
for item in items:
if item['id'] == object:
category['items'].append(item)
@ -854,6 +891,7 @@ class LuaDoc:
def _get_header(self, title: str, nav: list) -> str:
menu = ' <li><a href="' + LuaDoc.FILENAME_GLOBALS + '">' + self._get_term('globals:title') + '</a></li>' + os.linesep
menu += ' <li><a href="' + LuaDoc.FILENAME_PV + '">' + self._get_term('pv:title') + '</a></li>' + os.linesep
for k in sorted(list(self._libs.keys()) + ['enum', 'set']):
if k == 'enum':
menu += ' <li><a href="' + LuaDoc.FILENAME_ENUM + '">' + self._get_term('enum:title') + '</a></li>' + os.linesep

Datei anzeigen

@ -0,0 +1,6 @@
if pv.freight_car_1 == nil then
pv.freight_car_1 = {
cargo = 'none',
destination = 'unset'
}
end

Datei anzeigen

@ -0,0 +1,4 @@
pv.number = nil
pv.title = nil
pv.very_cool = nil
pv.freight_car_1 = nil

Datei anzeigen

@ -0,0 +1,9 @@
log.debug(pv.number)
log.debug(pv.title)
log.debug(pv.very_cool)
log.debug(pv.freight_car_1.cargo)
for k, v in pairs(pv['freight_car_1']) do
log.debug(k, v)
end

Datei anzeigen

@ -0,0 +1,8 @@
pv.number = 42
pv.title = 'Traintastic is awesome!'
pv.very_cool = true
pv['freight_car_1'] = {
cargo = 'grain',
destination = 'upper yard'
}

Datei anzeigen

@ -67,6 +67,10 @@
"type": "constant",
"since": "0.1"
},
"pv": {
"type": "object",
"since": "0.3"
},
"world": {
"type": "object",
"since": "0.1"
@ -102,4 +106,4 @@
"type": "library",
"since": "0.1"
}
}
}

Datei anzeigen

@ -2,6 +2,7 @@
"name": {
"since": "0.3"
},
"trains": {},
"on_train_assigned": {
"parameters": [
{
@ -64,4 +65,4 @@
],
"since": "0.3"
}
}
}

Datei anzeigen

@ -1,3 +1,4 @@
{
"active_train": {}
}
"active_train": {},
"trains": {}
}

Datei anzeigen

@ -6,6 +6,7 @@
"powered": {},
"active": {},
"mode": {},
"blocks": {},
"on_block_assigned": {
"parameters": [
{

Datei anzeigen

@ -0,0 +1,3 @@
{
"dual_motor": {}
}

Datei anzeigen

@ -2006,5 +2006,77 @@
{
"term": "object.train.on_block_reserved.parameter.direction:description",
"definition": "Train direction from the block perspective, a {ref:enum.block_train_direction} value."
},
{
"term": "object.turnoutsliprailtile.dual_motor:description",
"definition": "`true` if the slip turnout has two motors/coils, `false` if it has one motor/coil."
},
{
"term": "object.turnoutrailtile.name:description",
"definition": "Turnout name."
},
{
"term": "object.turnoutrailtile.position:description",
"definition": "Current turnout position, a {ref:enum.turnout_position} value. To change the turnout position call [`set_position`](#set_position)."
},
{
"term": "object.turnoutrailtile.set_position:description",
"definition": "Change turnout position."
},
{
"term": "object.turnoutrailtile.set_position.parameter.position:description",
"definition": "Requested turnout position, a {ref:enum.turnout_position} value."
},
{
"term": "object.turnoutrailtile.set_position:return_values",
"definition": "`true` if the position is changed, `false` if position value is invalid or turnout is locked e.g. due to a reserved path."
},
{
"term": "globals.pv:description",
"definition": "The {ref:pv|persistent variable} table."
},
{
"term": "pv:title",
"definition": "Persistent variables"
},
{
"term": "pv:paragraph_1",
"definition": "Persistent variables allow you to store and retrieve data that remains available across multiple executions of the Lua script. This can be particularly useful for maintaining state information that needs to be retained beyond the current script's lifetime."
},
{
"term": "pv:paragraph_2",
"definition": "The {ref:globals#pv|`pv`} global provides a simple and efficient interface for interacting with persistent data. Any values stored in {ref:globals#pv|`pv`} are saved across script executions and world save and loads. Below is a detailed breakdown of how to use the {ref:globals#pv|`pv`} global."
},
{
"term": "pv.storing:title",
"definition": "Storing persistent data"
},
{
"term": "pv.storing:paragraph_1",
"definition": "You can store data in {ref:globals#pv|`pv`} just like you would with a regular Lua table. Supported data types are numbers, strings, booleans, tables, {ref:enum|enums}, {ref:set|sets}, {ref:object|objects} and object methods."
},
{
"term": "pv.retrieving:title",
"definition": "Retrieving persistent data"
},
{
"term": "pv.retrieving:paragraph_1",
"definition": "To retrieve a previously stored value, including tables, access the corresponding key in the {ref:globals#pv|`pv`} global:"
},
{
"term": "pv.deleting:title",
"definition": "Deleting persistent data"
},
{
"term": "pv.deleting:paragraph_1",
"definition": "To delete a stored persistent value, including tables, simply assign `nil` to the desired key:"
},
{
"term": "pv.checking:title",
"definition": "Checking for persistent data"
},
{
"term": "pv.checking:paragraph_1",
"definition": "To determine if a persistent variable has been set, use an `if` statement with `nil` checks. Variables in {ref:globals#pv|`pv`} that haven't been initialized or have been deleted will return `nil`. This pattern is useful for initializing default values or handling cases where the persistent variables are cleared."
}
]

Datei anzeigen

@ -320,7 +320,8 @@ ul.index-az-nav li a
/** Lua **********************************************************************/
pre[lang="lua"]
pre[lang="lua"],
pre[lang="bash"]
{
border: solid 1px darkgray;
background-color: #f8f8f8;
@ -423,7 +424,8 @@ pre[lang="lua"] code span.function
border-left-color: blue;
}
pre[lang="lua"]
pre[lang="lua"],
pre[lang="bash"]
{
border-color: gray;
background-color: #222;

Datei anzeigen

@ -4,7 +4,7 @@ When Traintastic server is running the Traintastic client can be started.
**Windows:** The Traintastic client can be started using the desktop icon (if installed) or by selecting *Traintastic* -> *Traintastic client* from the Windows start menu.
**Linux:** TODO
**Linux:** To start Traintastic client, open your desktop environment's application launcher, search for "Traintastic Client," and select it from the list.
## Connect to the server

Datei anzeigen

@ -2,13 +2,36 @@
When running Traintastic, the server should be started first.
**Windows:** The Traintastic server can be started using the desktop icon (if installed) or by selecting *Traintastic* -> *Traintastic server* from the Windows start menu.
## Windows
The Traintastic server can be started using the desktop icon (if installed) or by selecting *Traintastic* -> *Traintastic server* from the Windows start menu.
Traintastic server runs as background process, a Traintastic icon will appear in the system tray next to the clock.
A Windows notification is displayed when it is running in the background.
Traintastic server can be quit by clicking on the Traintastic icon and selecting *Quit* from the popup menu.
![](../../gfx/en-us/start/start-traintastic-server-windows.png "Traintastic server tray icon and notification")
**Linux:** TODO
## Linux
When installing Traintastic server using a Debian package it is installed as systemd service.
When Traintastic server is running proceed to [start Traintastic client](start-client.md).
To start the Traintastic server using systemd, open a terminal and run the command:
```bash
sudo systemctl start traintastic-server.service
```
To stop the service, use the command:
```bash
sudo systemctl stop traintastic-server.service
```
Ensure you have the necessary permissions (typically root) to manage systemd services.
### Auto start on system boot
To enable Traintastic server to start automatically at boot, run the following command in a terminal:
```bash
sudo systemctl enable traintastic-server.service
```
To disable automatic start at boot, run the following command in a terminal:
```bash
sudo systemctl disable traintastic-server.service
```

Datei anzeigen

@ -25,7 +25,7 @@ def highlight_replace(code: str, css_class: str, clickable_links: bool = False)
def highlight_lua(code: str) -> str:
code = re.sub(r'\b(math|table|string|class|enum|set|log|world)\b', r'<span class="global">\1</span>', code) # globals
code = re.sub(r'\b(math|table|string|class|enum|set|log|world|pv)\b', r'<span class="global">\1</span>', code) # globals
code = re.sub(r'\b([A-Z_][A-Z0-9_]*)\b', r'<span class="const">\1</span>', code) # CONSTANTS
code = re.sub(r'\b(and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b', r'<span class="keyword">\1</span>', code) # keywords
code = re.sub(r'\b((|-|\+)[0-9]+(\\.[0-9]*|)((e|E)(|-|\+)[0-9]+|))\b', r'<span class="number">\1</span>', code) # numbers: infloat, decimal

Datei anzeigen

@ -54,7 +54,7 @@ Name: "firewall_wlanmaus"; Description: "{cm:firewall_allow_wlanmaus_z21}"; Grou
[Files]
; Server
Source: "..\..\server\build\{#ServerExeName}"; DestDir: "{app}\server"; Flags: ignoreversion; Check: InstallServer
Source: "..\..\server\thirdparty\lua5.3\bin\win64\lua53.dll"; DestDir: "{app}\server"; Flags: ignoreversion; Check: InstallServer
Source: "..\..\server\thirdparty\lua5.4\bin\win64\lua54.dll"; DestDir: "{app}\server"; Flags: ignoreversion; Check: InstallServer
Source: "..\..\server\thirdparty\libarchive\bin\archive.dll"; DestDir: "{app}\server"; Flags: ignoreversion; Check: InstallServer
Source: "..\..\server\thirdparty\zlib\bin\zlib1.dll"; DestDir: "{app}\server"; Flags: ignoreversion; Check: InstallServer
; Client
@ -107,7 +107,7 @@ Root: HKLM; Subkey: "{#CompanySubKey}"; Flags: uninsdeletekeyifempty
Root: HKLM; Subkey: "{#AppSubKey}"; Flags: uninsdeletekey
[INI]
Filename: {commonappdata}\traintastic\traintastic-client.ini; Section: general_; Key: language; String: {code:GetTraintasticClientLanguage}; Flags: uninsdeleteentry uninsdeletesectionifempty;
Filename: {commonappdata}\traintastic\traintastic-client.ini; Section: general_; Key: language; String: {code:GetTraintasticLanguage}; Flags: uninsdeleteentry uninsdeletesectionifempty;
[Code]
const
@ -179,6 +179,7 @@ begin
ClientAndServerRadioButton.Checked := (Components = 'ClientAndServer');
ClientAndServerRadioButton.Font.Style := [fsBold];
ClientAndServerRadioButton.Height := ScaleY(23);
ClientAndServerRadioButton.Width := ComponentsPage.SurfaceWidth;
ClientAndServerRadioButton.Parent := ComponentsPage.Surface;
ClientAndServerRadioButton.OnClick := @ComponentRadioButtonClick;
@ -195,6 +196,7 @@ begin
ClientOnlyRadioButton.Font.Style := [fsBold];
ClientOnlyRadioButton.Top := Lbl.Top + Lbl.Height + ScaleY(10);
ClientOnlyRadioButton.Height := ScaleY(23);
ClientOnlyRadioButton.Width := ComponentsPage.SurfaceWidth;
ClientOnlyRadioButton.Parent := ComponentsPage.Surface;
ClientOnlyRadioButton.OnClick := @ComponentRadioButtonClick;
@ -206,7 +208,7 @@ begin
Lbl.Parent := ComponentsPage.Surface;
end;
function GetTraintasticClientLanguage(Param: String) : String;
function GetTraintasticLanguage(Param: String) : String;
begin
case ActiveLanguage of
'nl': Result := 'nl-nl';
@ -219,6 +221,19 @@ begin
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
ServerSettingsFile: String;
begin
if CurStep = ssPostInstall then begin
// Server: only write language if there is no setting file yet:
ServerSettingsFile := ExpandConstant('{localappdata}\traintastic\server\settings.json');
if not FileExists(ServerSettingsFile) then begin
SaveStringToFile(ServerSettingsFile, '{"language":"' + GetTraintasticLanguage('') + '"}', False);
end;
end
end;
function VC2019RedistNeedsInstall: Boolean;
var
Version: String;

Datei anzeigen

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.9)
cmake_minimum_required(VERSION 3.15)
include(../shared/traintastic.cmake)
project(traintastic-server VERSION ${TRAINTASTIC_VERSION} DESCRIPTION "Traintastic server")
include(GNUInstallDirs)
@ -32,16 +32,25 @@ target_include_directories(traintastic-server SYSTEM PRIVATE
thirdparty)
if(BUILD_TESTING)
add_subdirectory(thirdparty/catch2)
set_target_properties(Catch2 PROPERTIES
CXX_STANDARD 20
CXX_CLANG_TIDY ""
)
add_executable(traintastic-server-test test/main.cpp)
add_dependencies(traintastic-server-test traintastic-lang)
target_compile_definitions(traintastic-server-test PRIVATE -DTRAINTASTIC_TEST)
set_target_properties(traintastic-server-test PROPERTIES CXX_STANDARD 20)
set_target_properties(traintastic-server-test PROPERTIES
CXX_STANDARD 20
CXX_CLANG_TIDY ""
)
target_include_directories(traintastic-server-test PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
../shared/src)
target_include_directories(traintastic-server-test SYSTEM PRIVATE
../shared/thirdparty
thirdparty)
target_link_libraries(traintastic-server-test PRIVATE Catch2::Catch2WithMain)
endif()
file(GLOB SOURCES
@ -55,6 +64,7 @@ file(GLOB SOURCES
"src/board/nx/*.cpp"
"src/board/tile/*.hpp"
"src/board/tile/*.cpp"
"src/board/tile/hidden/*.cpp"
"src/board/tile/misc/*.hpp"
"src/board/tile/misc/*.cpp"
"src/board/tile/rail/*.hpp"
@ -184,6 +194,30 @@ file(GLOB TEST_SOURCES
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DENABLE_LOG_DEBUG")
### VCPKG
if(DEFINED ENV{VCPKG_ROOT})
message(STATUS "Using VCPKG (VCPKG_ROOT=$ENV{VCPKG_ROOT})")
if(WIN32)
set(VCPKG_TARGET_TRIPLET "x64-windows-static-md")
endif()
include($ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake)
endif()
### RESOURCES ###
include(cmake/add-resource.cmake)
add_resource(resource-shared
BASE_DIR ../
FILES
shared/gfx/appicon.ico
)
add_dependencies(traintastic-server resource-shared)
if(BUILD_TESTING)
add_dependencies(traintastic-server-test resource-shared)
endif()
### OPTIONS ###
if(NO_LOCALHOST_ONLY_SETTING)
@ -215,6 +249,9 @@ if(LINUX)
if(BUILD_TESTING)
target_link_libraries(traintastic-server-test PRIVATE PkgConfig::LIBSYSTEMD)
endif()
else()
# Use inotify for monitoring serial ports:
list(APPEND SOURCES "src/os/linux/serialportlistimplinotify.hpp" "src/os/linux/serialportlistimplinotify.cpp")
endif()
else()
# socket CAN is only available on linux:
@ -272,47 +309,12 @@ if(WIN32 AND NOT MSVC)
endif()
# boost
if(LINUX)
find_package(Boost 1.71 REQUIRED COMPONENTS program_options)
target_include_directories(traintastic-server SYSTEM PRIVATE ${Boost_INCLUDE_DIRS})
target_link_libraries(traintastic-server PRIVATE ${Boost_LIBRARIES})
if(BUILD_TESTING)
target_include_directories(traintastic-server-test SYSTEM PRIVATE ${Boost_INCLUDE_DIRS})
target_link_libraries(traintastic-server-test PRIVATE ${Boost_LIBRARIES})
endif()
else()
add_definitions(
-DBOOST_ALL_NO_LIB
-DBOOST_ERROR_CODE_HEADER_ONLY
-DBOOST_CHRONO_HEADER_ONLY
-DBOOST_ASIO_HEADER_ONLY
-DBOOST_SYSTEM_NO_DEPRECATED)
if(NOT MSVC)
set_source_files_properties(
thirdparty/boost/libs/program_options/src/cmdline.cpp
thirdparty/boost/libs/program_options/src/config_file.cpp
thirdparty/boost/libs/program_options/src/convert.cpp
thirdparty/boost/libs/program_options/src/options_description.cpp
thirdparty/boost/libs/program_options/src/parsers.cpp
thirdparty/boost/libs/program_options/src/positional_options.cpp
thirdparty/boost/libs/program_options/src/split.cpp
thirdparty/boost/libs/program_options/src/utf8_codecvt_facet.cpp
thirdparty/boost/libs/program_options/src/value_semantic.cpp
thirdparty/boost/libs/program_options/src/variables_map.cpp
thirdparty/boost/libs/program_options/src/winmain.cpp
PROPERTIES
COMPILE_FLAGS -Wno-shadow)
endif()
target_include_directories(traintastic-server SYSTEM PRIVATE thirdparty/boost)
if(BUILD_TESTING)
target_include_directories(traintastic-server-test SYSTEM PRIVATE thirdparty/boost)
endif()
file(GLOB SOURCES_BOOST "thirdparty/boost/libs/program_options/src/*.cpp")
list(APPEND SOURCES ${SOURCES_BOOST})
find_package(Boost 1.81 REQUIRED COMPONENTS program_options)
target_include_directories(traintastic-server SYSTEM PRIVATE ${Boost_INCLUDE_DIRS})
target_link_libraries(traintastic-server PRIVATE ${Boost_LIBRARIES})
if(BUILD_TESTING)
target_include_directories(traintastic-server-test SYSTEM PRIVATE ${Boost_INCLUDE_DIRS})
target_link_libraries(traintastic-server-test PRIVATE ${Boost_LIBRARIES})
endif()
# zlib
@ -384,41 +386,41 @@ if(BUILD_TESTING)
target_link_libraries(traintastic-server-test PRIVATE ${LibArchive_LIBRARIES})
endif()
# liblua5.3
# liblua5.4
if(WIN32)
add_definitions(-DLUA_BUILD_AS_DLL)
set(LUA_INCLUDE_DIR "thirdparty/lua5.3/include")
set(LUA_INCLUDE_DIR "thirdparty/lua5.4/include")
if(MSVC)
set(LUA_LIBRARIES lua53)
set(LUA_LIBRARIES lua54)
add_custom_command(TARGET traintastic-server PRE_LINK
COMMAND lib "/def:${PROJECT_SOURCE_DIR}/thirdparty/lua5.3/bin/win64/lua53.def" /out:lua53.lib /machine:x64)
COMMAND lib "/def:${PROJECT_SOURCE_DIR}/thirdparty/lua5.4/bin/win64/lua54.def" /out:lua54.lib /machine:x64)
add_custom_command(TARGET traintastic-server-test PRE_LINK
COMMAND lib "/def:${PROJECT_SOURCE_DIR}/thirdparty/lua5.3/bin/win64/lua53.def" /out:lua53.lib /machine:x64)
COMMAND lib "/def:${PROJECT_SOURCE_DIR}/thirdparty/lua5.4/bin/win64/lua54.def" /out:lua54.lib /machine:x64)
else()
# MinGW can directly link .dll without import lib
set(LUA_LIBRARIES "${PROJECT_SOURCE_DIR}/thirdparty/lua5.3/bin/win64/lua53.dll")
set(LUA_LIBRARIES "${PROJECT_SOURCE_DIR}/thirdparty/lua5.4/bin/win64/lua54.dll")
endif()
# copy lua53.dll to build directory, to be able to run the tests:
# copy lua54.dll to build directory, to be able to run the tests:
add_custom_command(TARGET traintastic-server-test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy "${PROJECT_SOURCE_DIR}/thirdparty/lua5.3/bin/win64/lua53.dll" .)
COMMAND ${CMAKE_COMMAND} -E copy "${PROJECT_SOURCE_DIR}/thirdparty/lua5.4/bin/win64/lua54.dll" .)
elseif(APPLE)
find_path(LUA_INCLUDE_DIR
NAMES lua.h
PATHS
"/usr/local/opt/lua@5.3/include/lua" # x86_64
"/opt/homebrew/opt/lua@5.3/include/lua" # arm64
"/usr/local/opt/lua@5.4/include/lua" # x86_64
"/opt/homebrew/opt/lua@5.4/include/lua" # arm64
)
find_library(LUA_LIBRARIES
NAMES lua5.3 liblua5.3
NAMES lua5.4 liblua5.4
PATHS
"/usr/local/opt/lua@5.3/lib" # x86_64
"/opt/homebrew/opt/lua@5.3/lib" # arm64
"/usr/local/opt/lua@5.4/lib" # x86_64
"/opt/homebrew/opt/lua@5.4/lib" # arm64
)
else()
find_package(Lua 5.3 REQUIRED)
find_package(Lua 5.4 REQUIRED)
endif()
target_include_directories(traintastic-server PRIVATE ${LUA_INCLUDE_DIR})
target_link_libraries(traintastic-server PRIVATE ${LUA_LIBRARIES})
@ -457,8 +459,7 @@ endif()
if(BUILD_TESTING)
include(Catch)
target_include_directories(traintastic-server-test PRIVATE thirdparty/catch2)
catch_discover_tests(traintastic-server-test)
catch_discover_tests(traintastic-server-test DISCOVERY_MODE PRE_TEST)
endif()
### Doxygen ###

Datei anzeigen

@ -17,6 +17,7 @@
# Copyright 2007-2009 Kitware, Inc.
# Modified to support Lua 5.2 by LuaDist 2012
# Modified to support Lua 5.3 by Reinder Feenstra 2019
# Modified to support Lua 5.4 by Reinder Feenstra 2024
#
# Distributed under the OSI-approved BSD License (the "License");
# see accompanying file Copyright.txt for details.
@ -41,7 +42,7 @@ SET(_POSSIBLE_LUA_LIBRARY lua)
IF(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR)
SET(_POSSIBLE_SUFFIXES "${Lua_FIND_VERSION_MAJOR}${Lua_FIND_VERSION_MINOR}" "${Lua_FIND_VERSION_MAJOR}.${Lua_FIND_VERSION_MINOR}" "-${Lua_FIND_VERSION_MAJOR}.${Lua_FIND_VERSION_MINOR}")
ELSE(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR)
SET(_POSSIBLE_SUFFIXES "53" "5.3" "-5.3" "52" "5.2" "-5.2" "51" "5.1" "-5.1")
SET(_POSSIBLE_SUFFIXES "54" "5.4" "-5.4" "53" "5.3" "-5.3" "52" "5.2" "-5.2" "51" "5.1" "-5.1")
ENDIF(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR)
# Set up possible search names and locations

Datei anzeigen

@ -1,206 +0,0 @@
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.
#[=======================================================================[.rst:
Catch
-----
This module defines a function to help use the Catch test framework.
The :command:`catch_discover_tests` discovers tests by asking the compiled test
executable to enumerate its tests. This does not require CMake to be re-run
when tests change. However, it may not work in a cross-compiling environment,
and setting test properties is less convenient.
This command is intended to replace use of :command:`add_test` to register
tests, and will create a separate CTest test for each Catch test case. Note
that this is in some cases less efficient, as common set-up and tear-down logic
cannot be shared by multiple test cases executing in the same instance.
However, it provides more fine-grained pass/fail information to CTest, which is
usually considered as more beneficial. By default, the CTest test name is the
same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``.
.. command:: catch_discover_tests
Automatically add tests with CTest by querying the compiled test executable
for available tests::
catch_discover_tests(target
[TEST_SPEC arg1...]
[EXTRA_ARGS arg1...]
[WORKING_DIRECTORY dir]
[TEST_PREFIX prefix]
[TEST_SUFFIX suffix]
[PROPERTIES name1 value1...]
[TEST_LIST var]
[REPORTER reporter]
[OUTPUT_DIR dir]
[OUTPUT_PREFIX prefix}
[OUTPUT_SUFFIX suffix]
)
``catch_discover_tests`` sets up a post-build command on the test executable
that generates the list of tests by parsing the output from running the test
with the ``--list-test-names-only`` argument. This ensures that the full
list of tests is obtained. Since test discovery occurs at build time, it is
not necessary to re-run CMake when the list of tests changes.
However, it requires that :prop_tgt:`CROSSCOMPILING_EMULATOR` is properly set
in order to function in a cross-compiling environment.
Additionally, setting properties on tests is somewhat less convenient, since
the tests are not available at CMake time. Additional test properties may be
assigned to the set of tests as a whole using the ``PROPERTIES`` option. If
more fine-grained test control is needed, custom content may be provided
through an external CTest script using the :prop_dir:`TEST_INCLUDE_FILES`
directory property. The set of discovered tests is made accessible to such a
script via the ``<target>_TESTS`` variable.
The options are:
``target``
Specifies the Catch executable, which must be a known CMake executable
target. CMake will substitute the location of the built executable when
running the test.
``TEST_SPEC arg1...``
Specifies test cases, wildcarded test cases, tags and tag expressions to
pass to the Catch executable with the ``--list-test-names-only`` argument.
``EXTRA_ARGS arg1...``
Any extra arguments to pass on the command line to each test case.
``WORKING_DIRECTORY dir``
Specifies the directory in which to run the discovered test cases. If this
option is not provided, the current binary directory is used.
``TEST_PREFIX prefix``
Specifies a ``prefix`` to be prepended to the name of each discovered test
case. This can be useful when the same test executable is being used in
multiple calls to ``catch_discover_tests()`` but with different
``TEST_SPEC`` or ``EXTRA_ARGS``.
``TEST_SUFFIX suffix``
Similar to ``TEST_PREFIX`` except the ``suffix`` is appended to the name of
every discovered test case. Both ``TEST_PREFIX`` and ``TEST_SUFFIX`` may
be specified.
``PROPERTIES name1 value1...``
Specifies additional properties to be set on all tests discovered by this
invocation of ``catch_discover_tests``.
``TEST_LIST var``
Make the list of tests available in the variable ``var``, rather than the
default ``<target>_TESTS``. This can be useful when the same test
executable is being used in multiple calls to ``catch_discover_tests()``.
Note that this variable is only available in CTest.
``REPORTER reporter``
Use the specified reporter when running the test case. The reporter will
be passed to the Catch executable as ``--reporter reporter``.
``OUTPUT_DIR dir``
If specified, the parameter is passed along as
``--out dir/<test_name>`` to Catch executable. The actual file name is the
same as the test name. This should be used instead of
``EXTRA_ARGS --out foo`` to avoid race conditions writing the result output
when using parallel test execution.
``OUTPUT_PREFIX prefix``
May be used in conjunction with ``OUTPUT_DIR``.
If specified, ``prefix`` is added to each output file name, like so
``--out dir/prefix<test_name>``.
``OUTPUT_SUFFIX suffix``
May be used in conjunction with ``OUTPUT_DIR``.
If specified, ``suffix`` is added to each output file name, like so
``--out dir/<test_name>suffix``. This can be used to add a file extension to
the output e.g. ".xml".
#]=======================================================================]
#------------------------------------------------------------------------------
function(catch_discover_tests TARGET)
cmake_parse_arguments(
""
""
"TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST;REPORTER;OUTPUT_DIR;OUTPUT_PREFIX;OUTPUT_SUFFIX"
"TEST_SPEC;EXTRA_ARGS;PROPERTIES"
${ARGN}
)
if(NOT _WORKING_DIRECTORY)
set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
endif()
if(NOT _TEST_LIST)
set(_TEST_LIST ${TARGET}_TESTS)
endif()
## Generate a unique name based on the extra arguments
string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS} ${_REPORTER} ${_OUTPUT_DIR} ${_OUTPUT_PREFIX} ${_OUTPUT_SUFFIX}")
string(SUBSTRING ${args_hash} 0 7 args_hash)
# Define rule to generate test list for aforementioned test executable
set(ctest_include_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_include-${args_hash}.cmake")
set(ctest_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_tests-${args_hash}.cmake")
get_property(crosscompiling_emulator
TARGET ${TARGET}
PROPERTY CROSSCOMPILING_EMULATOR
)
add_custom_command(
TARGET ${TARGET} POST_BUILD
BYPRODUCTS "${ctest_tests_file}"
COMMAND "${CMAKE_COMMAND}"
-D "TEST_TARGET=${TARGET}"
-D "TEST_EXECUTABLE=$<TARGET_FILE:${TARGET}>"
-D "TEST_EXECUTOR=${crosscompiling_emulator}"
-D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}"
-D "TEST_SPEC=${_TEST_SPEC}"
-D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}"
-D "TEST_PROPERTIES=${_PROPERTIES}"
-D "TEST_PREFIX=${_TEST_PREFIX}"
-D "TEST_SUFFIX=${_TEST_SUFFIX}"
-D "TEST_LIST=${_TEST_LIST}"
-D "TEST_REPORTER=${_REPORTER}"
-D "TEST_OUTPUT_DIR=${_OUTPUT_DIR}"
-D "TEST_OUTPUT_PREFIX=${_OUTPUT_PREFIX}"
-D "TEST_OUTPUT_SUFFIX=${_OUTPUT_SUFFIX}"
-D "CTEST_FILE=${ctest_tests_file}"
-P "${_CATCH_DISCOVER_TESTS_SCRIPT}"
VERBATIM
)
file(WRITE "${ctest_include_file}"
"if(EXISTS \"${ctest_tests_file}\")\n"
" include(\"${ctest_tests_file}\")\n"
"else()\n"
" add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n"
"endif()\n"
)
if(NOT ${CMAKE_VERSION} VERSION_LESS "3.10.0")
# Add discovered tests to directory TEST_INCLUDE_FILES
set_property(DIRECTORY
APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}"
)
else()
# Add discovered tests as directory TEST_INCLUDE_FILE if possible
get_property(test_include_file_set DIRECTORY PROPERTY TEST_INCLUDE_FILE SET)
if (NOT ${test_include_file_set})
set_property(DIRECTORY
PROPERTY TEST_INCLUDE_FILE "${ctest_include_file}"
)
else()
message(FATAL_ERROR
"Cannot set more than one TEST_INCLUDE_FILE"
)
endif()
endif()
endfunction()
###############################################################################
set(_CATCH_DISCOVER_TESTS_SCRIPT
${CMAKE_CURRENT_LIST_DIR}/CatchAddTests.cmake
CACHE INTERNAL "Catch2 full path to CatchAddTests.cmake helper file"
)

Datei anzeigen

@ -1,132 +0,0 @@
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.
set(prefix "${TEST_PREFIX}")
set(suffix "${TEST_SUFFIX}")
set(spec ${TEST_SPEC})
set(extra_args ${TEST_EXTRA_ARGS})
set(properties ${TEST_PROPERTIES})
set(reporter ${TEST_REPORTER})
set(output_dir ${TEST_OUTPUT_DIR})
set(output_prefix ${TEST_OUTPUT_PREFIX})
set(output_suffix ${TEST_OUTPUT_SUFFIX})
set(script)
set(suite)
set(tests)
function(add_command NAME)
set(_args "")
foreach(_arg ${ARGN})
if(_arg MATCHES "[^-./:a-zA-Z0-9_]")
set(_args "${_args} [==[${_arg}]==]") # form a bracket_argument
else()
set(_args "${_args} ${_arg}")
endif()
endforeach()
set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE)
endfunction()
# Run test executable to get list of available tests
if(NOT EXISTS "${TEST_EXECUTABLE}")
message(FATAL_ERROR
"Specified test executable '${TEST_EXECUTABLE}' does not exist"
)
endif()
execute_process(
COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-test-names-only
OUTPUT_VARIABLE output
RESULT_VARIABLE result
WORKING_DIRECTORY "${TEST_WORKING_DIR}"
)
# Catch --list-test-names-only reports the number of tests, so 0 is... surprising
if(${result} EQUAL 0)
message(WARNING
"Test executable '${TEST_EXECUTABLE}' contains no tests!\n"
)
elseif(${result} LESS 0)
message(FATAL_ERROR
"Error running test executable '${TEST_EXECUTABLE}':\n"
" Result: ${result}\n"
" Output: ${output}\n"
)
endif()
string(REPLACE "\n" ";" output "${output}")
# Run test executable to get list of available reporters
execute_process(
COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-reporters
OUTPUT_VARIABLE reporters_output
RESULT_VARIABLE reporters_result
WORKING_DIRECTORY "${TEST_WORKING_DIR}"
)
if(${reporters_result} EQUAL 0)
message(WARNING
"Test executable '${TEST_EXECUTABLE}' contains no reporters!\n"
)
elseif(${reporters_result} LESS 0)
message(FATAL_ERROR
"Error running test executable '${TEST_EXECUTABLE}':\n"
" Result: ${reporters_result}\n"
" Output: ${reporters_output}\n"
)
endif()
string(FIND "${reporters_output}" "${reporter}" reporter_is_valid)
if(reporter AND ${reporter_is_valid} EQUAL -1)
message(FATAL_ERROR
"\"${reporter}\" is not a valid reporter!\n"
)
endif()
# Prepare reporter
if(reporter)
set(reporter_arg "--reporter ${reporter}")
endif()
# Prepare output dir
if(output_dir AND NOT IS_ABSOLUTE ${output_dir})
set(output_dir "${TEST_WORKING_DIR}/${output_dir}")
if(NOT EXISTS ${output_dir})
file(MAKE_DIRECTORY ${output_dir})
endif()
endif()
# Parse output
foreach(line ${output})
set(test ${line})
# Escape characters in test case names that would be parsed by Catch2
set(test_name ${test})
foreach(char , [ ])
string(REPLACE ${char} "\\${char}" test_name ${test_name})
endforeach(char)
# ...add output dir
if(output_dir)
string(REGEX REPLACE "[^A-Za-z0-9_]" "_" test_name_clean ${test_name})
set(output_dir_arg "--out ${output_dir}/${output_prefix}${test_name_clean}${output_suffix}")
endif()
# ...and add to script
add_command(add_test
"${prefix}${test}${suffix}"
${TEST_EXECUTOR}
"${TEST_EXECUTABLE}"
"${test_name}"
${extra_args}
"${reporter_arg}"
"${output_dir_arg}"
)
add_command(set_tests_properties
"${prefix}${test}${suffix}"
PROPERTIES
WORKING_DIRECTORY "${TEST_WORKING_DIR}"
${properties}
)
list(APPEND tests "${prefix}${test}${suffix}")
endforeach()
# Create a list of all discovered tests, which users may use to e.g. set
# properties on the tests
add_command(set ${TEST_LIST} ${tests})
# Write CTest script
file(WRITE "${CTEST_FILE}" "${script}")

Datei anzeigen

@ -0,0 +1,40 @@
#
# This file is part of the traintastic source code.
# See <https://github.com/traintastic/traintastic>.
#
# Copyright (C) 2024 Reinder Feenstra
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
function(add_resource TARGET_NAME)
cmake_parse_arguments(PARSE_ARG "" "BASE_DIR" "FILES" ${ARGN})
if(PARSE_ARG_BASE_DIR)
set(PARSE_ARG_BASE_DIR "${CMAKE_SOURCE_DIR}/${PARSE_ARG_BASE_DIR}")
else()
set(PARSE_ARG_BASE_DIR "${CMAKE_SOURCE_DIR}")
endif()
foreach(INPUT_FILE ${PARSE_ARG_FILES})
set(OUTPUT_FILE ${CMAKE_BINARY_DIR}/resource/${INPUT_FILE}.hpp)
add_custom_command(
OUTPUT ${OUTPUT_FILE}
COMMAND Python3::Interpreter ${CMAKE_SOURCE_DIR}/cmake/generateresourceheader.py ${PARSE_ARG_BASE_DIR} ${INPUT_FILE} ${OUTPUT_FILE}
DEPENDS ${CMAKE_SOURCE_DIR}/cmake/generateresourceheader.py ${PARSE_ARG_BASE_DIR}/${INPUT_FILE}
COMMENT "Generating resource header resource/${INPUT_FILE}.hpp"
)
list(APPEND OUTPUT_HEADERS ${OUTPUT_FILE})
endforeach()
add_custom_target(${TARGET_NAME} ALL DEPENDS ${OUTPUT_HEADERS})
endfunction()

Datei anzeigen

@ -380,7 +380,7 @@ function(target_code_coverage TARGET_NAME)
COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --zerocounters
COMMAND $<TARGET_FILE:${TARGET_NAME}> ${target_code_coverage_ARGS}
COMMAND
${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --base-directory
${LCOV_PATH} --ignore-errors mismatch --directory ${CMAKE_BINARY_DIR} --base-directory
${CMAKE_SOURCE_DIR} --capture ${EXTERNAL_OPTION} --output-file
${COVERAGE_INFO}
COMMAND ${EXCLUDE_COMMAND}

Datei anzeigen

@ -0,0 +1,86 @@
#
# This file is part of the traintastic source code.
# See <https://github.com/traintastic/traintastic>.
#
# Copyright (C) 2024 Reinder Feenstra
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
import sys
import os
import re
import textwrap
if len(sys.argv) != 4:
print(f"Usage: {sys.argv[0]} <input dir> <input file> <output header>")
sys.exit(1)
input_file = os.path.join(sys.argv[1], sys.argv[2])
input_file_ext = os.path.splitext(input_file)[1]
namespaces = ['Resource'] + os.path.dirname(sys.argv[2]).replace('../', '').split('/')
variable = re.sub(r'[\.]+','_', os.path.basename(sys.argv[2]).lower())
guard = '_'.join(namespaces).upper() + '_' + re.sub(r'[\.]+','_', os.path.basename(sys.argv[3]).upper())
is_binary = input_file_ext not in ['html', 'css', 'js']
with open(input_file, 'rb') as f:
contents = f.read()
os.makedirs(os.path.dirname(sys.argv[3]), exist_ok=True)
if is_binary:
size = len(contents)
contents = ', '.join(['std::byte{' + str(by) + '}' for by in contents])
contents = '\n '.join(textwrap.wrap(contents, width=120))
with open(sys.argv[3], 'w') as f:
f.write(f'''// Auto-generated, do not edit, it will be overwritten
#ifndef {guard}
#define {guard}
#include <array>
namespace {'::'.join(namespaces)}
{{
constexpr std::array<std::byte, {size}> {variable}{{{{
{contents}
}}}};
}}
#endif
''')
else: # text
with open(sys.argv[3], 'w') as f:
f.write(f'''// Auto-generated, do not edit, it will be overwritten
#ifndef {guard}
#define {guard}
#include <string_view>
namespace {'::'.join(namespaces)}
{{
constexpr std::string_view {variable} = R"({contents})";
}}
#endif
''')

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2020-2023 Reinder Feenstra
* Copyright (C) 2020-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -25,6 +25,7 @@
#include "boardlisttablemodel.hpp"
#include "map/link.hpp"
#include "tile/tiles.hpp"
#include "tile/hidden/hiddencrossoverrailtile.hpp"
#include "../core/method.tpp"
#include "../core/objectproperty.tpp"
#include "../world/world.hpp"
@ -33,6 +34,8 @@
#include "../utils/displayname.hpp"
#include <cassert>
#include "../log/log.hpp"
CREATE_IMPL(Board)
Board::Board(World& world, std::string_view _id) :
@ -53,7 +56,7 @@ Board::Board(World& world, std::string_view _id) :
{
const TileRotate tileRotate = it->second->rotate;
if(it->second->tileId() == TileId::RailStraight && tileClassId == StraightRailTile::classId) // merge to bridge
if(it->second->tileId == TileId::RailStraight && tileClassId == StraightRailTile::classId) // merge to bridge
{
if((tileRotate == rotate + TileRotate::Deg90 || tileRotate == rotate - TileRotate::Deg90) && deleteTile(x, y))
{
@ -73,7 +76,7 @@ Board::Board(World& world, std::string_view _id) :
else
return false;
}
else if(it->second->tileId() == TileId::RailStraight && // replace straight by a straight with something extra
else if(it->second->tileId == TileId::RailStraight && // replace straight by a straight with something extra
Tiles::canUpgradeStraightRail(tileClassId) &&
(tileRotate == rotate || (tileRotate + TileRotate::Deg180) == rotate) &&
deleteTile(x, y))
@ -385,6 +388,43 @@ void Board::modified()
Connector connector{startConnector.opposite()};
while(auto nextTile = getTile(connector.location))
{
if(isIntercardinal(connector.direction)) // check for crossover
{
auto prevTile = getTile(TileLocation{nextTile->x, nextTile->y} + connector.direction);
assert(prevTile);
auto otherTile1 = getTile({prevTile->x, nextTile->y});
auto otherTile2 = getTile({nextTile->x, prevTile->y});
if(otherTile1 && otherTile2)
{
const auto perpendicular =
(connector.direction == Connector::Direction::NorthEast) || (connector.direction == Connector::Direction::SouthWest)
? ~rotate90cw(connector.direction) : rotate90cw(connector.direction);
auto otherConnector1 = otherTile1->getConnector(perpendicular);
auto otherConnector2 = otherTile2->getConnector(~perpendicular);
if(otherConnector1 && otherConnector2) // crossover found!
{
const TileLocation topLeft{std::min<int16_t>(prevTile->x, nextTile->x), std::min<int16_t>(prevTile->y, nextTile->y)};
auto it = m_railCrossOver.find(topLeft);
if(it == m_railCrossOver.end())
{
it = m_railCrossOver.emplace(topLeft, std::make_shared<HiddenCrossOverRailTile>(world())).first;
it->second->x.setValueInternal(topLeft.x);
it->second->y.setValueInternal(topLeft.y);
}
auto& crossOver = it->second;
auto crossOverConnector = crossOver->getConnector(connector.direction);
assert(crossOverConnector);
auto link = std::make_shared<Link>(std::move(tiles));
link->connect(*startTile->node(), startConnector, *crossOver->node(), *crossOverConnector);
return;
}
}
}
if(nextTile->node())
{
auto link = std::make_shared<Link>(std::move(tiles));
@ -429,6 +469,30 @@ void Board::modified()
}
}
}
// remove unconnected crossovers:
auto it = m_railCrossOver.begin();
while(it != m_railCrossOver.end())
{
bool remove = false;
assert(it->second->node());
for(const auto& link : (*it->second->node()).get().links())
{
if(!link)
{
remove = true;
break;
}
}
if(remove)
{
it = m_railCrossOver.erase(it);
}
else
{
it++;
}
}
}
// notify board changed:

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2020-2023 Reinder Feenstra
* Copyright (C) 2020-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -31,6 +31,7 @@
class Tile;
struct TileData;
class HiddenCrossOverRailTile;
class Board : public IdObject
{
@ -41,6 +42,7 @@ class Board : public IdObject
private:
bool m_modified = false;
std::unordered_map<TileLocation, std::shared_ptr<HiddenCrossOverRailTile>, TileLocationHash> m_railCrossOver;
void modified();
void removeTile(int16_t x, int16_t y);
@ -101,6 +103,13 @@ class Board : public IdObject
return {};
}
#ifdef TRAINTASTIC_TEST
const auto& railCrossOver() const
{
return m_railCrossOver;
}
#endif
};
#endif

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2022-2023 Reinder Feenstra
* Copyright (C) 2022-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -84,7 +84,7 @@ bool AbstractSignalPath::requireReservation() const
return (m_signal.requireReservation == AutoYesNo::Yes || (m_signal.requireReservation == AutoYesNo::Auto && m_requireReservation));
}
const AbstractSignalPath::BlockItem* AbstractSignalPath::nextBlock(const Item* item) const
const AbstractSignalPath::BlockItem* AbstractSignalPath::nextBlock(const Item* item)
{
while(item)
{
@ -97,7 +97,7 @@ const AbstractSignalPath::BlockItem* AbstractSignalPath::nextBlock(const Item* i
return nullptr;
}
std::tuple<const AbstractSignalPath::BlockItem*, const AbstractSignalPath::SignalItem*> AbstractSignalPath::nextBlockOrSignal(const Item* item) const
std::tuple<const AbstractSignalPath::BlockItem*, const AbstractSignalPath::SignalItem*> AbstractSignalPath::nextBlockOrSignal(const Item* item)
{
while(item)
{
@ -238,7 +238,7 @@ std::unique_ptr<const AbstractSignalPath::Item> AbstractSignalPath::findBlocks(c
if(nextNode.getLink(0).get() == &link)
return findBlocks(nextNode, *nextLink, blocksAhead);
}
else if(isRailBridge(tile->tileId()) || isRailCross(tile->tileId()))
else if(isRailBridge(tile->tileId) || isRailCross(tile->tileId) || tile->tileId == TileId::HiddenRailCrossOver)
{
// 2 1 2 2 3
// | \| |/
@ -257,7 +257,7 @@ std::unique_ptr<const AbstractSignalPath::Item> AbstractSignalPath::findBlocks(c
if(auto linkNode = linkTile->link->node())
return findBlocks(linkNode->get(), linkNode->get().getLink(0), blocksAhead);
}
else if(tile->tileId() != TileId::RailBufferStop)
else if(tile->tileId != TileId::RailBufferStop)
{
if(const auto& nextLink = otherLink(nextNode, link))
{

Datei anzeigen

@ -181,13 +181,13 @@ class AbstractSignalPath : public Path
return m_root.get();
}
const BlockItem* nextBlock(const Item* item) const;
static const BlockItem* nextBlock(const Item* item);
inline const BlockItem* nextBlock() const
{
return nextBlock(root());
}
std::tuple<const BlockItem*, const SignalItem*> nextBlockOrSignal(const Item* item) const;
static std::tuple<const BlockItem*, const SignalItem*> nextBlockOrSignal(const Item* item);
inline std::tuple<const BlockItem*, const SignalItem*> nextBlockOrSignal() const
{
return nextBlockOrSignal(root());

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2023 Reinder Feenstra
* Copyright (C) 2023-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -25,6 +25,7 @@
#include <traintastic/enum/crossstate.hpp>
#include "node.hpp"
#include "link.hpp"
#include "../tile/hidden/hiddencrossoverrailtile.hpp"
#include "../tile/rail/blockrailtile.hpp"
#include "../tile/rail/bridgerailtile.hpp"
#include "../tile/rail/crossrailtile.hpp"
@ -33,6 +34,8 @@
#include "../tile/rail/turnout/turnoutrailtile.hpp"
#include "../tile/rail/linkrailtile.hpp"
#include "../tile/rail/nxbuttonrailtile.hpp"
#include "../../train/trainblockstatus.hpp"
#include "../../core/eventloop.hpp"
#include "../../core/objectproperty.tpp"
#include "../../enum/bridgepath.hpp"
@ -101,11 +104,11 @@ std::vector<std::shared_ptr<BlockPath>> BlockPath::find(BlockRailTile& startBloc
const auto& nextNode = current.link->getNext(*current.node);
auto& tile = nextNode.tile();
switch(tile.tileId())
switch(tile.tileId.value())
{
case TileId::RailBlock:
{
if(current.node->tile().tileId() == TileId::RailNXButton)
if(current.node->tile().tileId == TileId::RailNXButton)
{
current.path->m_nxButtonTo = current.node->tile().shared_ptr<NXButtonRailTile>();
}
@ -282,6 +285,31 @@ std::vector<std::shared_ptr<BlockPath>> BlockPath::find(BlockRailTile& startBloc
current.link = otherLink(nextNode, *current.link).get();
break;
case TileId::HiddenRailCrossOver:
{
// 1 2
// X
// 0 3
auto crossOver = tile.shared_ptr<HiddenCrossOverRailTile>();
if(contains(current.path->m_crossOvers, crossOver))
{
todo.pop(); // drop it, can't pass crossover twice
break;
}
for(size_t i = 0; i < 4; i++)
{
if(nextNode.getLink(i).get() == current.link)
{
current.node = &nextNode;
current.link = nextNode.getLink((i + 2) % 4).get(); // opposite
current.path->m_crossOvers.emplace_back(crossOver, i % 2 == 0 ? CrossState::AC : CrossState::BD);
break;
}
}
break;
}
default: // passive or non rail tiles
assert(false); // this should never happen
todo.pop(); // drop it in case it does, however that is a bug!
@ -296,9 +324,34 @@ BlockPath::BlockPath(BlockRailTile& block, BlockSide side)
: m_fromBlock{block}
, m_fromSide{side}
, m_toSide{static_cast<BlockSide>(-1)}
, m_delayReleaseTimer{EventLoop::ioContext}
, m_isReserved(false)
, m_delayedReleaseScheduled(false)
{
}
BlockPath::BlockPath(const BlockPath &other)
: Path(other)
, std::enable_shared_from_this<BlockPath>() // NOLINT(readability-redundant-member-init) -Wextra requires this
, m_fromBlock(other.m_fromBlock)
, m_fromSide(other.m_fromSide)
, m_toBlock(other.m_toBlock)
, m_toSide(other.m_toSide)
, m_tiles(other.m_tiles)
, m_turnouts(other.m_turnouts)
, m_directionControls(other.m_directionControls)
, m_crossings(other.m_crossings)
, m_bridges(other.m_bridges)
, m_signals(other.m_signals)
, m_nxButtonFrom(other.m_nxButtonFrom)
, m_nxButtonTo(other.m_nxButtonTo)
, m_delayReleaseTimer{EventLoop::ioContext}
, m_isReserved(false)
, m_delayedReleaseScheduled(false)
{
}
bool BlockPath::operator ==(const BlockPath& other) const noexcept
{
return
@ -331,7 +384,7 @@ bool BlockPath::isReady() const
}
}
for(const auto& [directionControlWeak, state] : m_directionControls)
for(const auto& [directionControlWeak, state] : m_directionControls) // NOLINT(readability-use-anyofallof)
{
auto directionControl = directionControlWeak.lock();
if(!directionControl) /*[[unlikely]]*/
@ -387,7 +440,7 @@ bool BlockPath::reserve(const std::shared_ptr<Train>& train, bool dryRun)
{
if(auto turnout = turnoutWeak.lock())
{
if(!turnout->reserve(position, dryRun))
if(!turnout->reserve(shared_from_this(), position, dryRun))
{
assert(dryRun);
return false;
@ -434,6 +487,23 @@ bool BlockPath::reserve(const std::shared_ptr<Train>& train, bool dryRun)
}
}
for(const auto& [crossOverWeak, state] : m_crossOvers)
{
if(auto crossOver = crossOverWeak.lock())
{
if(!crossOver->reserve(state, dryRun))
{
assert(dryRun);
return false;
}
}
else /*[[unlikely]]*/
{
assert(dryRun);
return false;
}
}
for(const auto& [bridgeWeak, path] : m_bridges)
{
if(auto bridge = bridgeWeak.lock())
@ -489,6 +559,9 @@ bool BlockPath::reserve(const std::shared_ptr<Train>& train, bool dryRun)
}
}
if(!dryRun)
m_isReserved = true;
return true;
}
@ -499,22 +572,47 @@ bool BlockPath::release(bool dryRun)
return false;
}
if(!dryRun)
m_delayReleaseTimer.cancel();
auto toBlock = m_toBlock.lock();
if(!toBlock) /*[[unlikely]]*/
return false;
BlockState fromState = m_fromBlock.state.value();
BlockState toState = toBlock->state.value();
if((fromState == BlockState::Occupied || fromState == BlockState::Unknown)
&& (toState == BlockState::Occupied || toState == BlockState::Unknown)
&& !m_fromBlock.trains.empty() && !toBlock->trains.empty())
{
// Check if train head is beyond toBlock while its end is still in fromBlock
const auto& status1 = fromSide() == BlockSide::A ? m_fromBlock.trains.front() : m_fromBlock.trains.back();
const auto& status2 = toSide() == BlockSide::A ? toBlock->trains.front() : toBlock->trains.back();
if(status1->train.value() == status2->train.value())
return false;
}
if(!m_fromBlock.release(m_fromSide, dryRun))
{
assert(dryRun);
return false;
}
if(auto toBlock = m_toBlock.lock()) /*[[likely]]*/
if(!dryRun && toBlock->state.value() == BlockState::Reserved)
{
if(!toBlock->release(m_toSide, dryRun))
if(toBlock->trains.size() == 1)
{
assert(dryRun);
return false;
//TODO: this bypasses some checks
toBlock->removeTrainInternal(toBlock->trains[0]);
//TODO: dryRun? what if it fails?
}
}
else
if(!toBlock->release(m_toSide, dryRun))
{
assert(dryRun);
return false;
}
@ -597,5 +695,28 @@ bool BlockPath::release(bool dryRun)
}
}
if(!dryRun)
m_isReserved = false;
return true;
}
bool BlockPath::delayedRelease(uint16_t timeoutMillis)
{
if(m_delayedReleaseScheduled)
return false;
m_delayedReleaseScheduled = true;
m_delayReleaseTimer.expires_after(boost::asio::chrono::milliseconds(timeoutMillis));
m_delayReleaseTimer.async_wait([this](const boost::system::error_code& ec)
{
m_delayedReleaseScheduled = false;
if(ec)
return;
release();
});
return true;
}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2023 Reinder Feenstra
* Copyright (C) 2023-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -29,6 +29,7 @@
#include <array>
#include <vector>
#include <utility>
#include <boost/asio/steady_timer.hpp>
#include "../../enum/blockside.hpp"
class RailTile;
@ -36,6 +37,7 @@ class BlockRailTile;
class BridgeRailTile;
enum class BridgePath : uint8_t;
class CrossRailTile;
class HiddenCrossOverRailTile;
enum class CrossState : uint8_t;
class DirectionControlRailTile;
enum class DirectionControlState : uint8_t;
@ -59,15 +61,21 @@ class BlockPath : public Path, public std::enable_shared_from_this<BlockPath>
std::vector<std::pair<std::weak_ptr<TurnoutRailTile>, TurnoutPosition>> m_turnouts; //!< required turnout positions for the path
std::vector<std::pair<std::weak_ptr<DirectionControlRailTile>, DirectionControlState>> m_directionControls; //!< required direction control states for the path
std::vector<std::pair<std::weak_ptr<CrossRailTile>, CrossState>> m_crossings; //!< required crossing states for the path
std::vector<std::pair<std::weak_ptr<HiddenCrossOverRailTile>, CrossState>> m_crossOvers; //!< required crossing states for the path
std::vector<std::pair<std::weak_ptr<BridgeRailTile>, BridgePath>> m_bridges; //!< bridges to reserve
std::vector<std::weak_ptr<SignalRailTile>> m_signals; //!< signals in path
std::weak_ptr<NXButtonRailTile> m_nxButtonFrom;
std::weak_ptr<NXButtonRailTile> m_nxButtonTo;
boost::asio::steady_timer m_delayReleaseTimer;
bool m_isReserved;
bool m_delayedReleaseScheduled;
public:
static std::vector<std::shared_ptr<BlockPath>> find(BlockRailTile& block);
BlockPath(BlockRailTile& block, BlockSide side);
BlockPath(const BlockPath& other);
bool operator ==(const BlockPath& other) const noexcept;
@ -99,11 +107,17 @@ class BlockPath : public Path, public std::enable_shared_from_this<BlockPath>
return m_toSide;
}
inline bool isReserved() const
{
return m_isReserved;
}
std::shared_ptr<NXButtonRailTile> nxButtonFrom() const;
std::shared_ptr<NXButtonRailTile> nxButtonTo() const;
bool reserve(const std::shared_ptr<Train>& train, bool dryRun = false);
bool release(bool dryRun = false);
bool delayedRelease(uint16_t timeoutMillis);
};
#endif

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2022 Reinder Feenstra
* Copyright (C) 2022,2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -72,6 +72,17 @@ constexpr Connector::Direction operator ~(Connector::Direction value)
return static_cast<Connector::Direction>(n <= 4 ? n + 4 : n - 4);
}
constexpr Connector::Direction rotate90cw(Connector::Direction value)
{
const auto n = static_cast<std::underlying_type_t<Connector::Direction>>(value);
return static_cast<Connector::Direction>(n <= 6 ? n + 2 : n - 6);
}
constexpr bool isIntercardinal(Connector::Direction value)
{
return (static_cast<std::underlying_type_t<Connector::Direction>>(value) & 1) == 0;
}
constexpr Connector::Direction toConnectorDirection(TileRotate value)
{
const auto r = static_cast<std::underlying_type_t<TileRotate>>(value);

Datei anzeigen

@ -24,9 +24,7 @@
#include <cassert>
#include "node.hpp"
Link::Link()
{
}
Link::Link() = default;
Link::Link(std::vector<std::shared_ptr<Tile>> tiles)
: m_tiles{std::move(tiles)}

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2023 Reinder Feenstra
* Copyright (C) 2023-2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -45,7 +45,7 @@ tcb::span<const Path::TurnoutPositionLink> Path::getTurnoutLinks(TurnoutRailTile
const auto& node = turnout.node()->get();
switch(turnout.tileId())
switch(turnout.tileId.value())
{
case TileId::RailTurnoutLeft45:
case TileId::RailTurnoutLeft90:

Datei anzeigen

@ -92,12 +92,21 @@ void NXManager::released(NXButtonRailTile& button)
bool NXManager::selectPath(const NXButtonRailTile& from, const NXButtonRailTile& to)
{
for(auto& path : from.block->paths())
for(const auto& path : from.block->paths())
{
if(path->nxButtonTo().get() == &to && path->nxButtonFrom().get() == &from)
{
LOG_DEBUG("Path found:", path->fromBlock().name.value(), "->", path->toBlock()->name.value());
if(path->isReserved())
{
// If user clicked an already reserved path we release it.
// TODO: make some logic to prevent releasing a path while train is inside it?
// Also releasing a path when we already set signal to "Proceed" is dangerous because train might
// have already started moving towards out path
return path->release();
}
if(from.block->trains.empty())
{
continue; // no train in from block

Datei anzeigen

@ -37,15 +37,15 @@ class NXManager : public SubObject
private:
std::list<std::weak_ptr<NXButtonRailTile>> m_pressedButtons;
bool selectPath(const NXButtonRailTile& from, const NXButtonRailTile& to);
static bool selectPath(const NXButtonRailTile& from, const NXButtonRailTile& to);
public:
Method<void(const std::shared_ptr<NXButtonRailTile>&, const std::shared_ptr<NXButtonRailTile>&)> select;
NXManager(Object& parent_, std::string_view parentPropertyName);
void pressed(NXButtonRailTile& tile);
void released(NXButtonRailTile& tile);
void pressed(NXButtonRailTile& button);
void released(NXButtonRailTile& button);
};
#endif

Datei anzeigen

@ -0,0 +1,78 @@
/**
* server/src/board/tile/hidden/hiddencrossoverrailtile.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "hiddencrossoverrailtile.hpp"
#include <traintastic/enum/crossstate.hpp>
HiddenCrossOverRailTile::HiddenCrossOverRailTile(World& world)
: HiddenTile(world, TileId::HiddenRailCrossOver)
, m_node{*this, 4}
, m_crossState{CrossState::Unset}
{
}
std::string_view HiddenCrossOverRailTile::getClassId() const
{
assert(false);
return {};
}
void HiddenCrossOverRailTile::getConnectors(std::vector<Connector>& connectors) const
{
// x x+1
// +--+--+
// y | | |
// +--X--+
// y+1 | | |
// +--+--+
//
// The hidden crossing is actually at (x+0.5, y+0.5), but that can't be stored.
// So we store (x, y) and trick it a bit.
connectors.emplace_back(location().adjusted(0, 1), Connector::Direction::NorthEast, Connector::Type::Rail);
connectors.emplace_back(location(), Connector::Direction::SouthEast, Connector::Type::Rail);
connectors.emplace_back(location().adjusted(1, 0), Connector::Direction::SouthWest, Connector::Type::Rail);
connectors.emplace_back(location().adjusted(1, 1), Connector::Direction::NorthWest, Connector::Type::Rail);
}
bool HiddenCrossOverRailTile::reserve(CrossState crossState, bool dryRun)
{
if(m_crossState != CrossState::Unset)
{
return false;
}
if(!dryRun)
{
m_crossState = crossState;
}
return true;
}
bool HiddenCrossOverRailTile::release(bool dryRun)
{
if(!dryRun)
{
m_crossState = CrossState::Unset;
}
return true;
}

Datei anzeigen

@ -0,0 +1,50 @@
/**
* server/src/board/tile/hidden/hiddencrossoverrailtile.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_BOARD_TILE_HIDDEN_HIDDENCROSSOVERRAILTILE_HPP
#define TRAINTASTIC_SERVER_BOARD_TILE_HIDDEN_HIDDENCROSSOVERRAILTILE_HPP
#include "hiddentile.hpp"
#include "../../map/node.hpp"
enum class CrossState : uint8_t;
class HiddenCrossOverRailTile : public HiddenTile
{
private:
Node m_node;
CrossState m_crossState; //!< indicates which path is reserved
public:
HiddenCrossOverRailTile(World& world);
std::string_view getClassId() const final;
std::optional<std::reference_wrapper<const Node>> node() const final { return m_node; }
std::optional<std::reference_wrapper<Node>> node() final { return m_node; }
void getConnectors(std::vector<Connector>& connectors) const final;
bool reserve(CrossState crossState, bool dryRun = false);
bool release(bool dryRun = false);
};
#endif

Datei anzeigen

@ -1,9 +1,9 @@
/**
* server/src/enum/worldevent.hpp
* server/src/board/tile/hidden/hiddentile.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2019-2020 Reinder Feenstra
* Copyright (C) 2024 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -20,9 +20,18 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_ENUM_WORLDEVENT_HPP
#define TRAINTASTIC_SERVER_ENUM_WORLDEVENT_HPP
#ifndef TRAINTASTIC_SERVER_BOARD_TILE_HIDDEN_HIDDENTILE_HPP
#define TRAINTASTIC_SERVER_BOARD_TILE_HIDDEN_HIDDENTILE_HPP
#include <traintastic/enum/worldevent.hpp>
#include "../tile.hpp"
class HiddenTile : public Tile
{
protected:
HiddenTile(World& world, TileId tileId_)
: Tile(world, {}, tileId_)
{
}
};
#endif

Datei anzeigen

@ -89,6 +89,18 @@ SwitchTile::SwitchTile(World& world, std::string_view _id)
m_interfaceItems.add(onValueChanged);
}
void SwitchTile::destroying()
{
outputMap->parentObject.setValueInternal(nullptr);
Tile::destroying();
}
void SwitchTile::addToWorld()
{
outputMap->parentObject.setValueInternal(shared_from_this());
Tile::addToWorld();
}
void SwitchTile::worldEvent(WorldState worldState, WorldEvent worldEvent)
{
Tile::worldEvent(worldState, worldEvent);

Datei anzeigen

@ -38,6 +38,8 @@ class SwitchTile : public Tile
CREATE_DEF(SwitchTile)
protected:
void destroying() override;
void addToWorld() final;
void worldEvent(WorldState worldState, WorldEvent worldEvent) final;
public:

Datei anzeigen

@ -133,35 +133,7 @@ BlockRailTile::BlockRailTile(World& world, std::string_view _id) :
throw LogMessageException(LogMessage::E3006_CANT_REMOVE_TRAIN_TRAIN_CAN_ONLY_BE_REMOVED_FROM_HEAD_OR_TAIL_BLOCK);
}
status->destroy();
status.reset();
updateTrainMethodEnabled();
if(state == BlockState::Reserved)
updateState();
Log::log(*this, LogMessage::N3002_REMOVED_TRAIN_X_FROM_BLOCK_X, oldTrain->name.value(), name.value());
if(oldTrain->blocks.empty())
{
oldTrain->active = false;
}
if(m_world.simulation)
{
for(const auto& item : *inputMap)
{
if(item->input && item->input->interface)
{
if(item->type == SensorType::OccupancyDetector)
item->input->simulateChange(item->invert.value() ? SimulateInputAction::SetTrue : SimulateInputAction::SetFalse);
else
assert(false); // not yet implemented
}
}
}
oldTrain->fireBlockRemoved(shared_ptr<BlockRailTile>());
fireEvent(onTrainRemoved, oldTrain, self);
removeTrainInternal(status);
}
}}
, flipTrain{*this, "flip_train",
@ -285,11 +257,9 @@ void BlockRailTile::inputItemValueChanged(BlockInputMapItem& item)
//! \todo log something (at least in debug)
break;
}
else
{
train = status->train.value();
direction = path->toSide() == BlockSide::A ? BlockTrainDirection::TowardsB : BlockTrainDirection::TowardsA;
}
train = status->train.value();
direction = path->toSide() == BlockSide::A ? BlockTrainDirection::TowardsB : BlockTrainDirection::TowardsA;
}
}
}
@ -312,7 +282,7 @@ void BlockRailTile::inputItemValueChanged(BlockInputMapItem& item)
if(enterA != enterB)
{
auto& blockStatus = enterA ? trains.front() : trains.back();
const auto& blockStatus = enterA ? trains.front() : trains.back();
TrainTracking::enter(blockStatus);
}
else
@ -374,6 +344,7 @@ void BlockRailTile::identificationEvent(BlockInputMapItem& /*item*/, Identificat
case IdentificationEventType::Present:
//!< \todo assign train (if allowed and possible)
trains.appendInternal(TrainBlockStatus::create(*this, std::string("#").append(std::to_string(identifier)), blockDirection));
updateTrainMethodEnabled();
if(state == BlockState::Free || state == BlockState::Unknown)
updateState();
break;
@ -409,7 +380,7 @@ void BlockRailTile::identificationEvent(BlockInputMapItem& /*item*/, Identificat
}
}
const std::shared_ptr<BlockPath> BlockRailTile::getReservedPath(BlockSide side) const
std::shared_ptr<BlockPath> BlockRailTile::getReservedPath(BlockSide side) const
{
assert(side == BlockSide::A || side == BlockSide::B);
return m_reservedPaths[static_cast<uint8_t>(side)].lock();
@ -475,6 +446,46 @@ bool BlockRailTile::release(BlockSide side, bool dryRun)
return true;
}
bool BlockRailTile::removeTrainInternal(const std::shared_ptr<TrainBlockStatus> &status)
{
if(!status)
return false;
const auto self = shared_ptr<BlockRailTile>();
const std::shared_ptr<Train> oldTrain = status->train.value();
status->destroy();
updateTrainMethodEnabled();
if(state == BlockState::Reserved)
updateState();
Log::log(*this, LogMessage::N3002_REMOVED_TRAIN_X_FROM_BLOCK_X, oldTrain->name.value(), name.value());
if(oldTrain->blocks.empty())
{
oldTrain->active = false;
}
if(m_world.simulation)
{
for(const auto& item : *inputMap)
{
if(item->input && item->input->interface)
{
if(item->type == SensorType::OccupancyDetector)
item->input->simulateChange(item->invert.value() ? SimulateInputAction::SetTrue : SimulateInputAction::SetFalse);
else
assert(false); // not yet implemented
}
}
}
oldTrain->fireBlockRemoved(shared_ptr<BlockRailTile>());
fireEvent(onTrainRemoved, oldTrain, self);
return true;
}
void BlockRailTile::updateState()
{
if(!inputMap->items.empty())

Datei anzeigen

@ -107,9 +107,11 @@ class BlockRailTile : public RailTile
void inputItemValueChanged(BlockInputMapItem& item);
void identificationEvent(BlockInputMapItem& item, IdentificationEventType eventType, uint16_t identifier, Direction direction, uint8_t category);
const std::shared_ptr<BlockPath> getReservedPath(BlockSide side) const;
std::shared_ptr<BlockPath> getReservedPath(BlockSide side) const;
bool reserve(const std::shared_ptr<BlockPath>& blockPath, const std::shared_ptr<Train>& train, BlockSide side, bool dryRun = false);
bool release(BlockSide side, bool dryRun = false);
bool removeTrainInternal(const std::shared_ptr<TrainBlockStatus>& status);
};
#endif

Datei anzeigen

@ -53,6 +53,7 @@ bool CrossRailTile::release(bool dryRun)
if(!dryRun)
{
m_crossState = CrossState::Unset;
RailTile::release();
}
return true;

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden Mehr anzeigen