Repository: MiniZinc/MiniZincIDE Branch: master Commit: b6fea3f06081 Files: 267 Total size: 1.2 MB Directory structure: gitextract_jgwfs6f5/ ├── .envrc ├── .gitignore ├── .gitlab-ci.yml ├── LICENSE.txt ├── MiniZincIDE/ │ ├── CHANGES │ ├── MiniZincIDE.pri │ ├── MiniZincIDE.pro │ ├── README.txt │ ├── cheat_sheet.mzn │ ├── checkupdatedialog.cpp │ ├── checkupdatedialog.h │ ├── checkupdatedialog.ui │ ├── codechecker.cpp │ ├── codechecker.h │ ├── codeeditor.cpp │ ├── codeeditor.h │ ├── configwindow.cpp │ ├── configwindow.h │ ├── configwindow.ui │ ├── dark_mode.css │ ├── darkmodenotifier.cpp │ ├── darkmodenotifier.h │ ├── darkmodenotifier_macos.mm │ ├── elapsedtimer.cpp │ ├── elapsedtimer.h │ ├── esclineedit.cpp │ ├── esclineedit.h │ ├── exception.h │ ├── extraparamdialog.cpp │ ├── extraparamdialog.h │ ├── extraparamdialog.ui │ ├── fzndoc.cpp │ ├── fzndoc.h │ ├── gotolinedialog.cpp │ ├── gotolinedialog.h │ ├── gotolinedialog.ui │ ├── highlighter.cpp │ ├── highlighter.h │ ├── history.cpp │ ├── history.h │ ├── ide.cpp │ ├── ide.h │ ├── ideutils.cpp │ ├── ideutils.h │ ├── images/ │ │ └── about.html │ ├── main.cpp │ ├── mainwindow.cpp │ ├── mainwindow.h │ ├── mainwindow.ui │ ├── minizincide.qrc │ ├── moocsubmission.cpp │ ├── moocsubmission.h │ ├── moocsubmission.ui │ ├── mznide-makefile.plist │ ├── mznide-xcode.plist │ ├── mznide.icns │ ├── outputdockwidget.cpp │ ├── outputdockwidget.h │ ├── outputwidget.cpp │ ├── outputwidget.h │ ├── outputwidget.ui │ ├── paramdialog.cpp │ ├── paramdialog.h │ ├── paramdialog.ui │ ├── preferencesdialog.cpp │ ├── preferencesdialog.h │ ├── preferencesdialog.ui │ ├── process.cpp │ ├── process.h │ ├── profilecompilation.cpp │ ├── profilecompilation.h │ ├── project.cpp │ ├── project.h │ ├── projectbrowser.cpp │ ├── projectbrowser.h │ ├── projectbrowser.ui │ ├── server/ │ │ ├── connector.js │ │ └── index.html │ ├── server.cpp │ ├── server.h │ ├── solver.cpp │ ├── solver.h │ ├── theme.cpp │ └── theme.h ├── MiniZincIDE.pro ├── README.md ├── TODO.txt ├── cp-profiler/ │ ├── README.md │ ├── cp-profiler.pri │ ├── cp-profiler.pro │ └── src/ │ ├── cpp-integration/ │ │ ├── README.md │ │ ├── connector.hpp │ │ └── message.hpp │ ├── cpprofiler/ │ │ ├── analysis/ │ │ │ ├── histogram_scene.cpp │ │ │ ├── histogram_scene.hh │ │ │ ├── merge_window.cpp │ │ │ ├── merge_window.hh │ │ │ ├── merging/ │ │ │ │ ├── merge_result.hh │ │ │ │ ├── pentagon_list_widget.hh │ │ │ │ ├── pentagon_rect.cpp │ │ │ │ └── pentagon_rect.hh │ │ │ ├── nogood_analysis_dialog.hh │ │ │ ├── path_comp.cpp │ │ │ ├── path_comp.hh │ │ │ ├── pattern_rect.cpp │ │ │ ├── pattern_rect.hh │ │ │ ├── pentagon_counter.hpp │ │ │ ├── similar_subtree_analysis.cpp │ │ │ ├── similar_subtree_analysis.hh │ │ │ ├── similar_subtree_window.cpp │ │ │ ├── similar_subtree_window.hh │ │ │ ├── subtree_pattern.hh │ │ │ ├── tree_merger.cpp │ │ │ └── tree_merger.hh │ │ ├── command_line_parser.cpp │ │ ├── command_line_parser.hh │ │ ├── conductor.cpp │ │ ├── conductor.hh │ │ ├── config.hh │ │ ├── core.cpp │ │ ├── core.hh │ │ ├── db_handler.cpp │ │ ├── db_handler.hh │ │ ├── execution.cpp │ │ ├── execution.hh │ │ ├── execution_list.cpp │ │ ├── execution_list.hh │ │ ├── execution_window.cpp │ │ ├── execution_window.hh │ │ ├── message_wrapper.hh │ │ ├── name_map.cpp │ │ ├── name_map.hh │ │ ├── nogood_dialog.cpp │ │ ├── nogood_dialog.hh │ │ ├── options.hh │ │ ├── pixel_views/ │ │ │ ├── icicle_canvas.cpp │ │ │ ├── icicle_canvas.hh │ │ │ ├── pixel_image.cpp │ │ │ ├── pixel_image.hh │ │ │ ├── pixel_item.hh │ │ │ ├── pixel_widget.cpp │ │ │ ├── pixel_widget.hh │ │ │ ├── pt_canvas.cpp │ │ │ └── pt_canvas.hh │ │ ├── receiver_thread.cpp │ │ ├── receiver_thread.hh │ │ ├── receiver_worker.cpp │ │ ├── receiver_worker.hh │ │ ├── settings.hh │ │ ├── solver_data.cpp │ │ ├── solver_data.hh │ │ ├── solver_id.hh │ │ ├── stats_bar.hpp │ │ ├── tcp_server.cpp │ │ ├── tcp_server.hh │ │ ├── tests/ │ │ │ ├── execution_test.cpp │ │ │ ├── execution_test.hh │ │ │ ├── tree_test.cpp │ │ │ └── tree_test.hh │ │ ├── tree/ │ │ │ ├── cursors/ │ │ │ │ ├── drawing_cursor.cpp │ │ │ │ ├── drawing_cursor.hh │ │ │ │ ├── hide_failed_cursor.cpp │ │ │ │ ├── hide_failed_cursor.hh │ │ │ │ ├── hide_not_highlighted_cursor.cpp │ │ │ │ ├── hide_not_highlighted_cursor.hh │ │ │ │ ├── layout_cursor.cpp │ │ │ │ ├── layout_cursor.hh │ │ │ │ ├── node_cursor.cpp │ │ │ │ ├── node_cursor.hh │ │ │ │ ├── nodevisitor.hh │ │ │ │ └── nodevisitor.hpp │ │ │ ├── layout.cpp │ │ │ ├── layout.hh │ │ │ ├── layout_computer.cpp │ │ │ ├── layout_computer.hh │ │ │ ├── node.cpp │ │ │ ├── node.hh │ │ │ ├── node_drawing.cpp │ │ │ ├── node_drawing.hh │ │ │ ├── node_id.cpp │ │ │ ├── node_id.hh │ │ │ ├── node_info.cpp │ │ │ ├── node_info.hh │ │ │ ├── node_stats.hh │ │ │ ├── node_tree.cpp │ │ │ ├── node_tree.hh │ │ │ ├── node_widget.hh │ │ │ ├── shape.cpp │ │ │ ├── shape.hh │ │ │ ├── structure.cpp │ │ │ ├── structure.hh │ │ │ ├── subtree_view.hh │ │ │ ├── traditional_view.cpp │ │ │ ├── traditional_view.hh │ │ │ ├── tree_scroll_area.cpp │ │ │ ├── tree_scroll_area.hh │ │ │ ├── visual_flags.cpp │ │ │ └── visual_flags.hh │ │ ├── tree_builder.cpp │ │ ├── tree_builder.hh │ │ ├── user_data.cpp │ │ ├── user_data.hh │ │ └── utils/ │ │ ├── array.cpp │ │ ├── array.hh │ │ ├── debug.hh │ │ ├── debug_mutex.hh │ │ ├── maybe_caller.cpp │ │ ├── maybe_caller.hh │ │ ├── path_utils.cpp │ │ ├── path_utils.hh │ │ ├── perf_helper.cpp │ │ ├── perf_helper.hh │ │ ├── std_ext.cpp │ │ ├── std_ext.hh │ │ ├── string_utils.cpp │ │ ├── string_utils.hh │ │ ├── tree_utils.cpp │ │ ├── tree_utils.hh │ │ ├── utils.cpp │ │ └── utils.hh │ └── main_cpprofiler.cpp ├── default.nix ├── flake.nix ├── resources/ │ ├── README.md │ ├── misc/ │ │ ├── COMBINED_LICENSE.txt │ │ ├── MiniZincIDE.desktop │ │ ├── README │ │ ├── entitlements.xml │ │ ├── minizinc.desktop │ │ ├── osx-gecode-qt.conf │ │ └── win-gecode-qt.conf │ ├── pkg_config/ │ │ ├── Dockerfile │ │ ├── minizinc-bundle.iss │ │ └── snapcraft.yaml │ └── scripts/ │ ├── AppRun │ └── MiniZincIDE.sh └── tests/ ├── data/ │ ├── mooc/ │ │ ├── TestProject/ │ │ │ ├── TestProject.mzp │ │ │ ├── _mooc │ │ │ ├── data/ │ │ │ │ ├── n1.dzn │ │ │ │ └── n2.dzn │ │ │ └── models/ │ │ │ ├── submission.mzc │ │ │ └── submission.mzn │ │ └── TestTerms/ │ │ ├── TestTerms.mzp │ │ ├── _mooc │ │ ├── data/ │ │ │ ├── n1.dzn │ │ │ └── n2.dzn │ │ └── models/ │ │ ├── submission.mzc │ │ └── submission.mzn │ └── project/ │ ├── configs/ │ │ ├── solver1.mpc │ │ ├── solver2.mpc │ │ └── solver3.mpc │ ├── data/ │ │ ├── data1.dzn │ │ └── data2.dzn │ ├── models/ │ │ ├── model1.mzn │ │ └── model2.mzn │ ├── project-105-bad.mzp │ ├── project-105-good.mzp │ ├── project-106-bad.mzp │ └── project-106-good.mzp ├── testcpprofier.cpp ├── testdiff.cpp ├── testeditor.cpp ├── testide.cpp ├── testide.h ├── testmooc.cpp ├── testproject.cpp └── tests.pro ================================================ FILE CONTENTS ================================================ ================================================ FILE: .envrc ================================================ use flake ================================================ FILE: .gitignore ================================================ *.pro.user result ================================================ FILE: .gitlab-ci.yml ================================================ stages: - build - test - package - publish default: interruptible: true .cache: variables: CCACHE_DIR: "$CI_PROJECT_DIR/.ccache" CCACHE_MAXSIZE: "100M" cache: key: "$CI_JOB_STAGE:$CI_JOB_NAME" paths: [.ccache] # ----------- Build MiniZincIDE ----------- build:linux: extends: .cache stage: build image: ghcr.io/minizinc/docker-build-environment:qt variables: GIT_SUBMODULE_STRATEGY: recursive GIT_STRATEGY: clone DISABLE_COPYRIGHT_FILES_DEPLOYMENT: "1" DEBUG: "1" script: - mkdir -p build; cd build - qmake -makefile "CONFIG+=bundled" "CXX_PREFIX=ccache" "DEFINES+=MINIZINC_IDE_BUILD=\\\\\\\"\"${CI_PIPELINE_ID}\\\\\\\"\"" PREFIX=/usr ../MiniZincIDE/MiniZincIDE.pro - make -j4 - make -j4 INSTALL_ROOT=../ide install; find ../ide/ - cd .. # Download Gecode Gist so linuxdeploy can include its dependencies - sh ${VENDOR_SCRIPT} minizinc-vendor master gecode_gist:linux vendor.zip - unzip -q vendor.zip - linuxdeploy --appdir ide --executable vendor/gecode_gist/bin/fzn-gecode --plugin qt -v0 - rm -rf ide/usr/share ide/usr/translations ide/usr/bin/fzn-gecode tags: [linux, docker] artifacts: paths: [ide/] build:osx: extends: .cache stage: build variables: GIT_SUBMODULE_STRATEGY: recursive GIT_STRATEGY: clone script: - mkdir -p build; cd build - qmake -makefile "CONFIG+=bundled" "CXX_PREFIX=ccache" "DEFINES+=MINIZINC_IDE_BUILD=\\\\\\\"\"${CI_PIPELINE_ID}\\\\\\\"\"" QMAKE_APPLE_DEVICE_ARCHS="x86_64 arm64" ../MiniZincIDE/MiniZincIDE.pro - make -j4 - cp -r MiniZincIDE.app .. tags: [osx, cpp, qt] artifacts: paths: [MiniZincIDE.app] build:win64: stage: build variables: GIT_SUBMODULE_STRATEGY: recursive MZNARCH: "win64" BUILDCACHE_DIR: "$CI_PROJECT_DIR/.ccache" BUILDCACHE_MAX_CACHE_SIZE: "104857600" script: - if not exist "build" mkdir build - if not exist "ide" mkdir ide - cd build - qmake "CONFIG+=bundled" "CXX_PREFIX=buildcache" "DEFINES+=MINIZINC_IDE_BUILD=\\\\\\\"\"%CI_PIPELINE_ID%\\\\\\\"\"" ../MiniZincIDE/MiniZincIDE.pro - jom -j4 - cp release/MiniZincIDE.exe ../ide - cd ../ # Download Gecode Gist so windeployqt can include its dependencies - curl --silent -o vendor.zip --location --header "PRIVATE-TOKEN:%ACCESS_TOKEN%" "https://gitlab.com/api/v4/projects/minizinc%%2Fminizinc-vendor/jobs/artifacts/master/download?job=gecode_gist:%MZNARCH%" - unzip -q vendor.zip - cd ide - windeployqt --no-translations --no-compiler-runtime --no-system-d3d-compiler MiniZincIDE.exe ../vendor/gecode_gist/bin/fzn-gecode.exe tags: [win64, cpp, qt] artifacts: paths: [ide/] cache: key: "build_win64" paths: [.ccache] # ----------- Test MiniZincIDE ----------- .tests_template: image: ghcr.io/minizinc/docker-build-environment:qt variables: GIT_SUBMODULE_STRATEGY: recursive GIT_STRATEGY: clone QT_QPA_PLATFORM: offscreen MZN_SOLVER_PATH: ${CI_PROJECT_DIR}/vendor/gecode/share/minizinc/solvers/:${CI_PROJECT_DIR}/vendor/chuffed/share/minizinc/solvers before_script: - 'if [ -n "$CI_COMMIT_TAG" ]; then MZNREF="$CI_COMMIT_TAG"; elif [ "$CI_COMMIT_REF_NAME" = "master" ]; then MZNREF="master"; else MZNREF="develop"; fi' ### Download Dependencies - curl --silent -o minizinc.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2Fminizinc/jobs/artifacts/$MZNREF/download?job=build:$MZNARCH" - unzip -q minizinc.zip - curl --silent -o vendor.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2Fminizinc-vendor/jobs/artifacts/master/download?job=bundle:$MZNARCH" - unzip -q vendor.zip ### Add MiniZinc to path - export PATH=$CI_PROJECT_DIR/minizinc/bin:$PATH script: ### Build tests - mkdir -p test; cd test - qmake -makefile "CXX_PREFIX=ccache" ../tests/tests.pro - make -j4 ### Run tests - make check needs: [] test:linux: extends: [.cache, .tests_template] variables: MZNARCH: linux tags: [linux, docker] test:osx: extends: [.cache, .tests_template] variables: MZNARCH: osx tags: [osx, cpp, qt] test:win64: extends: [.cache, .tests_template] variables: MZNARCH: win64 MZN_SOLVER_PATH: ${CI_PROJECT_DIR}/vendor/gecode/share/minizinc/solvers/;${CI_PROJECT_DIR}/vendor/chuffed/share/minizinc/solvers BUILDCACHE_DIR: "$CI_PROJECT_DIR/.ccache" BUILDCACHE_MAX_CACHE_SIZE: "104857600" before_script: ### Choose the MiniZinc compiler branch - if defined CI_COMMIT_TAG (set MZNREF=%CI_COMMIT_TAG%) else if %CI_COMMIT_REF_NAME%==master (set MZNREF=master) else (set MZNREF=develop) ### Download Dependencies - curl --silent -o minizinc.zip --location --header "PRIVATE-TOKEN:%ACCESS_TOKEN%" "https://gitlab.com/api/v4/projects/minizinc%%2Fminizinc/jobs/artifacts/%MZNREF%/download?job=build:%MZNARCH%" - unzip -q minizinc.zip - curl --silent -o vendor.zip --location --header "PRIVATE-TOKEN:%ACCESS_TOKEN%" "https://gitlab.com/api/v4/projects/minizinc%%2Fminizinc-vendor/jobs/artifacts/master/download?job=bundle:%MZNARCH%" - unzip -q vendor.zip ### Add MiniZinc to path - set PATH=%CI_PROJECT_DIR%/minizinc/bin;%PATH% script: ### Build tests - if not exist "test" mkdir test - cd test - qmake "CXX_PREFIX=buildcache" ../tests/tests.pro - jom -j4 ### Run tests - jom check tags: [win64, cpp, qt] cache: key: "test_win64" paths: [.ccache] # ----------- MiniZinc Packaging ----------- .packaging_setup: &packaging_setup before_script: ### Set the MZNVERSION variable - 'if [ -n "$CI_COMMIT_TAG" ]; then MZNVERSION="$CI_COMMIT_TAG"; else MZNVERSION="build$CI_PIPELINE_ID"; fi' ### Choose the MiniZinc compiler branch - 'if [ -n "$CI_COMMIT_TAG" ]; then MZNREF="$CI_COMMIT_TAG"; elif [ "$CI_COMMIT_REF_NAME" = "master" ]; then MZNREF="master"; else MZNREF="develop"; fi' ### Choose the FindMUS branch - 'if [ -n "$CI_COMMIT_TAG" ] || [ "$CI_COMMIT_REF_NAME" = "master" ]; then FINDMUSREF="master"; else FINDMUSREF="develop"; fi' ### Choose the FindMUS branch - 'if [ -n "$CI_COMMIT_TAG" ] || [ "$CI_COMMIT_REF_NAME" = "master" ]; then ANALYSEREF="master"; else ANALYSEREF="develop"; fi' ### Download Dependencies - curl --silent -o minizinc.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2Fminizinc/jobs/artifacts/$MZNREF/download?job=build:$MZNARCH" - unzip -q minizinc.zip - '[ ${DOWNLOAD_SOLVERS:-1} -eq 1 ] && curl --silent -o vendor.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2Fminizinc-vendor/jobs/artifacts/master/download?job=bundle:$MZNARCH" && unzip -q vendor.zip' - '[ ${DOWNLOAD_GLOBALIZER:-0} -eq 1 ] && curl --silent -o globalizer.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2FGlobalizer/jobs/artifacts/master/download?job=build:$MZNARCH" && unzip -q globalizer.zip' - '[ ${DOWNLOAD_FINDMUS:-0} -eq 1 ] && curl --silent -o findmus.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2FFindMUS/jobs/artifacts/$FINDMUSREF/download?job=build:$MZNARCH" && unzip -q findmus.zip' - '[ ${DOWNLOAD_ANALYSE:-0} -eq 1 ] && curl --silent -o mzn-analyse.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2Fmzn-analyse/jobs/artifacts/$ANALYSEREF/download?job=build:$MZNARCH" && unzip -q mzn-analyse.zip' package:check_version: stage: package image: ghcr.io/minizinc/docker-build-environment:qt variables: MZNARCH: "linux" DOWNLOAD_SOLVERS: 0 <<: *packaging_setup script: - mkdir -p build; cd build - qmake -makefile "CONFIG+=output_version" ../MiniZincIDE/MiniZincIDE.pro - IDE_VERSION=$(tr -s ' ' < version) - MZN_VERSION=$(../minizinc/bin/minizinc --version | grep -Po '(?<=version )[^,]+') - echo IDE is version ${IDE_VERSION} - echo MiniZinc is version ${MZN_VERSION} - if [ "$IDE_VERSION" != "$MZN_VERSION" ]; then echo 'Version mismatch!'; exit 1; fi - if [ -n "$CI_COMMIT_TAG" ]; then echo Tag is "$CI_COMMIT_TAG"; if [ "$CI_COMMIT_TAG" != "$MZN_VERSION" ]; then echo 'Version mismatch!'; exit 1; fi; fi needs: [] tags: [linux, docker] package:linux: stage: package image: ghcr.io/minizinc/docker-build-environment:package variables: MZNARCH: "linux" DOWNLOAD_GLOBALIZER: 1 DOWNLOAD_FINDMUS: 1 DOWNLOAD_ANALYSE: 1 <<: *packaging_setup script: - PACKAGE=MiniZincIDE-${MZNVERSION}-bundle-linux-x86_64 - mkdir -p $PACKAGE/lib/ ### Package IDE - mv ide/usr/* $PACKAGE/ - cp resources/scripts/MiniZincIDE.sh $PACKAGE/ ### Package MiniZinc - mv minizinc/bin/* $PACKAGE/bin/ - mv minizinc/share $PACKAGE/share ### Package vendor solvers - mv vendor/gecode_gist/bin/fzn-gecode $PACKAGE/bin/ - patchelf --set-rpath '$ORIGIN/../lib' $PACKAGE/bin/fzn-gecode - cp -r vendor/gecode_gist/share/minizinc/* $PACKAGE/share/minizinc/ - mv vendor/chuffed/bin/fzn-chuffed $PACKAGE/bin/ - cp -r vendor/chuffed/share/minizinc/* $PACKAGE/share/minizinc/ - mv vendor/or-tools/bin/fzn-cp-sat $PACKAGE/bin/ - cp -r vendor/or-tools/share/minizinc/* $PACKAGE/share/minizinc/ - cp vendor/highs/lib64/libhighs.so $PACKAGE/lib/ ### Package Globalizer - mv globalizer/bin/minizinc-globalizer $PACKAGE/bin/ - cp -r globalizer/share/minizinc/* $PACKAGE/share/minizinc/ ### Package findMUS - mv findMUS/bin/findMUS $PACKAGE/bin/ - cp -r findMUS/share/minizinc/* $PACKAGE/share/minizinc/ ### Package mzn-analyse - mv mzn-analyse/bin/mzn-analyse $PACKAGE/bin/ ### Strip included binaries - (cd $PACKAGE/bin; strip minizinc fzn-gecode fzn-chuffed fzn-cp-sat findMUS mzn-analyse minizinc-globalizer mzn2doc) - cp resources/misc/README $PACKAGE ### Compress package - tar -czf $PACKAGE.tgz $PACKAGE ### Generate checksum - sha256sum $PACKAGE.tgz > $PACKAGE.sha256 artifacts: name: "minizinc_bundle_linux_${CI_PIPELINE_ID}" paths: [MiniZincIDE*.tgz, MiniZincIDE*.sha256] needs: ["build:linux"] tags: [linux, docker] package:osx: stage: package variables: MZNARCH: "osx" DOWNLOAD_GLOBALIZER: 1 DOWNLOAD_FINDMUS: 1 DOWNLOAD_ANALYSE: 1 <<: *packaging_setup script: - "DIR=MiniZincIDE.app/Contents/Resources; MZNDIR=$DIR/share/minizinc" - mkdir -p $MZNDIR/solvers ### Package MiniZinc - mv minizinc/bin/* $DIR/ - mv minizinc/share/minizinc/* $MZNDIR/ ### Package vendor solvers - mkdir -p $DIR/bin/ - mkdir -p $DIR/lib/ - mv vendor/gecode_gist/bin/fzn-gecode $DIR/bin/fzn-gecode - cp -r vendor/gecode_gist/share/minizinc/* $MZNDIR/ - cp resources/misc/osx-gecode-qt.conf $DIR/bin/qt.conf - mv vendor/chuffed/bin/fzn-chuffed $DIR/bin/ - cp -r vendor/chuffed/share/minizinc/* $MZNDIR/ - mv vendor/or-tools/bin/fzn-cp-sat $DIR/bin/ - cp -r vendor/or-tools/share/minizinc/* $MZNDIR/ - cp vendor/highs/lib/libhighs.dylib $DIR/lib/ ### Package Globalizer - mv globalizer/bin/minizinc-globalizer $DIR/bin/ - cp -r globalizer/share/minizinc/* $MZNDIR/ ### Package findMUS - mv findMUS/bin/findMUS $DIR/bin/ - cp -r findMUS/share/minizinc/* $MZNDIR/ ### Package mzn-analyse - mv mzn-analyse/bin/mzn-analyse $DIR/bin/ ### Strip included binaries - (cd $DIR; strip minizinc mzn2doc) - (cd $DIR/bin; strip fzn-gecode fzn-chuffed fzn-cp-sat findMUS mzn-analyse minizinc-globalizer) ### Run automated Qt deployment tool - macdeployqt ./MiniZincIDE.app -executable=$DIR/bin/fzn-gecode ### Sign package - if [ -z "$OSX_DEVELOPER_ID" ]; then - exit 0 - fi - security unlock-keychain -p "$OSX_KEYCHAIN_PASSWORD" "$OSX_KEYCHAIN_PATH" - security set-key-partition-list -S 'apple-tool:,apple:' -s -k "$OSX_KEYCHAIN_PASSWORD" "$OSX_KEYCHAIN_PATH" - security list-keychains -d user -s "$OSX_KEYCHAIN_PATH" login.keychain - for f in MiniZincIDE.app/Contents/Frameworks/*.framework; do \ - codesign --options runtime --force --sign "$OSX_DEVELOPER_ID" $f/Versions/*/`basename $f .framework`; \ - done - find MiniZincIDE.app/Contents/PlugIns MiniZincIDE.app/Contents/Frameworks -type f -name '*.dylib' -exec codesign --options runtime --force --sign "$OSX_DEVELOPER_ID" {} \; - find MiniZincIDE.app/Contents/Resources -type f -perm +111 -exec codesign --options runtime --entitlements resources/misc/entitlements.xml --force --sign "$OSX_DEVELOPER_ID" {} \; - codesign --options runtime --force --sign "$OSX_DEVELOPER_ID" MiniZincIDE.app - ditto -c -k --sequesterRsrc --keepParent MiniZincIDE.app signed_bundle.zip - xcrun notarytool submit signed_bundle.zip --team-id "$OSX_NOTARY_TOOL_TEAM_ID" --apple-id "$OSX_NOTARY_TOOL_APPLE_ID" --password "$OSX_NOTARY_TOOL_PASSWORD" --wait - xcrun stapler staple MiniZincIDE.app - cp "$OSX_TEMPLATE_IMAGE" signed_bundle.sparseimage - hdiutil detach -quiet signed_bundle || true - hdiutil attach -mountpoint signed_bundle signed_bundle.sparseimage - diskutil rename signed_bundle "MiniZinc IDE $MZNVERSION (bundled)" - rm -rf signed_bundle/MiniZincIDE.app - mv MiniZincIDE.app signed_bundle/ - hdiutil detach signed_bundle - hdiutil convert -format UDZO -o MiniZincIDE-$MZNVERSION-bundled.dmg signed_bundle.sparseimage after_script: - hdiutil detach -quiet signed_bundle || true - security lock-keychain "$OSX_KEYCHAIN_PATH" artifacts: name: "minizinc_bundle_mac_${CI_PIPELINE_ID}" paths: [MiniZincIDE.app, MiniZincIDE-*-bundled.dmg] needs: ["build:osx"] tags: [osx, qt] package:osx:ide_only: stage: package variables: MZNARCH: "osx" script: ### Run automated Qt deployment tool - macdeployqt ./MiniZincIDE.app artifacts: name: "minizinc_ide_mac_${CI_PIPELINE_ID}" paths: [MiniZincIDE.app] needs: ["build:osx"] tags: [osx, qt] package:win64: stage: package variables: MZNARCH: "win64" ISSARCH: "x64" ISSARCHALLOWED: "x64" CODESIGN_CMD: signtool.exe sign /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 /dlib $WIN_CODESIGN_DLIB /dmdf $WIN_CODESIGN_METADATA before_script: ### Set redist variables - for /d %%a in ("%VCToolsRedistDir%\\x64\\*.CRT") do set MSVCREDIST=%%a - set UCRTREDIST=%WindowsSdkDir%\Redist\ucrt\DLLs\x64 ### Set the MZNVERSION variable - if defined CI_COMMIT_TAG (set MZNVERSION=%CI_COMMIT_TAG%) else (set MZNVERSION=%CI_PIPELINE_ID%) ### Choose the MiniZinc compiler branch - if defined CI_COMMIT_TAG (set MZNREF=%CI_COMMIT_TAG%) else if %CI_COMMIT_REF_NAME%==master (set MZNREF=master) else (set MZNREF=develop) - if defined CI_COMMIT_TAG (set FINDMUSREF=master) else if %CI_COMMIT_REF_NAME%==master (set FINDMUSREF=master) else (set FINDMUSREF=develop) - if defined CI_COMMIT_TAG (set ANALYSEREF=master) else if %CI_COMMIT_REF_NAME%==master (set ANALYSEREF=master) else (set ANALYSEREF=develop) ### Download Dependencies - curl --silent -o minizinc.zip --location --header "PRIVATE-TOKEN:%ACCESS_TOKEN%" "https://gitlab.com/api/v4/projects/minizinc%%2Fminizinc/jobs/artifacts/%MZNREF%/download?job=build:%MZNARCH%" - unzip -q minizinc.zip - curl --silent -o vendor.zip --location --header "PRIVATE-TOKEN:%ACCESS_TOKEN%" "https://gitlab.com/api/v4/projects/minizinc%%2Fminizinc-vendor/jobs/artifacts/master/download?job=bundle:%MZNARCH%" - unzip -q vendor.zip - curl --silent -o globalizer.zip --location --header "PRIVATE-TOKEN:%ACCESS_TOKEN%" "https://gitlab.com/api/v4/projects/minizinc%%2Fglobalizer/jobs/artifacts/master/download?job=build:%MZNARCH%" - unzip -q globalizer.zip - curl --silent -o findmus.zip --location --header "PRIVATE-TOKEN:%ACCESS_TOKEN%" "https://gitlab.com/api/v4/projects/minizinc%%2FFindMus/jobs/artifacts/%FINDMUSREF%/download?job=build:%MZNARCH%" - unzip -q findmus.zip - curl --silent -o mzn-analyse.zip --location --header "PRIVATE-TOKEN:%ACCESS_TOKEN%" "https://gitlab.com/api/v4/projects/minizinc%%2Fmzn-analyse/jobs/artifacts/%ANALYSEREF%/download?job=build:%MZNARCH%" - unzip -q mzn-analyse.zip ### Turn on code signing if variables present - if defined WIN_CODESIGN_METADATA (set DOCODESIGN=1) else (set DOCODESIGN=0) script: - '"C:/Program Files (x86)/Inno Setup 6/ISCC.exe" /dMyAppVersion="%MZNVERSION%" /dMyMZNVersion="%MZNVERSION%" /dMyAppDirectory="%CI_PROJECT_DIR%" /dMyMSVCRedist="%MSVCREDIST%" /dMyUCRTRedist="%UCRTREDIST%" /dMyAPPArch="%MZNARCH%" /dMyApp64Bit="%ISSARCH%" /dMyAppArchitectures="%ISSARCHALLOWED%" /dDoCodeSign="%DOCODESIGN%" /SMyCodeSignTool="%CODESIGN_CMD% $f" /O"%CI_PROJECT_DIR%" resources/pkg_config/minizinc-bundle.iss' ### Generate checksum - certutil -hashfile MiniZincIDE-%MZNVERSION%-bundled-setup-%MZNARCH%.exe SHA256 > MiniZincIDE-%MZNVERSION%-bundled-setup-%MZNARCH%.sha256 artifacts: name: "minizinc_bundle_windows_%CI_PIPELINE_ID%" paths: [MiniZincIDE*.exe, MiniZincIDE*.sha256] needs: ["build:win64"] tags: [win64] package:appimage: stage: package image: ghcr.io/minizinc/docker-build-environment:qt variables: MZNARCH: "linux" DOWNLOAD_GLOBALIZER: 1 DOWNLOAD_FINDMUS: 1 DOWNLOAD_ANALYSE: 1 PACKAGE: "MiniZinc.AppDir" <<: *packaging_setup script: - mkdir -p $PACKAGE/usr/lib ### Package IDE - mv ide/usr/* $PACKAGE/usr/ ### Package MiniZinc - mv minizinc/bin/* $PACKAGE/usr/bin/ - mv minizinc/share $PACKAGE/usr/share ### Package vendor solvers - mv vendor/gecode_gist/bin/fzn-gecode $PACKAGE/usr/bin/ - cp -r vendor/gecode_gist/share/minizinc/* $PACKAGE/usr/share/minizinc/ - mv vendor/chuffed/bin/fzn-chuffed $PACKAGE/usr/bin/ - cp -r vendor/chuffed/share/minizinc/* $PACKAGE/usr/share/minizinc/ - mv vendor/or-tools/bin/fzn-cp-sat $PACKAGE/usr/bin/ - cp -r vendor/or-tools/share/minizinc/* $PACKAGE/usr/share/minizinc/ - cp vendor/highs/lib64/libhighs.so $PACKAGE/usr/lib/ ### Package Globalizer - mv globalizer/bin/minizinc-globalizer $PACKAGE/usr/bin/ - cp -r globalizer/share/minizinc/* $PACKAGE/usr/share/minizinc/ ### Package findMUS - mv findMUS/bin/findMUS $PACKAGE/usr/bin/ - cp -r findMUS/share/minizinc/* $PACKAGE/usr/share/minizinc/ ### Package mzn-analyse - mv mzn-analyse/bin/mzn-analyse $PACKAGE/usr/bin/ ### Strip included binaries - (cd $PACKAGE/usr/bin; strip minizinc fzn-gecode fzn-chuffed fzn-cp-sat findMUS mzn-analyse minizinc-globalizer mzn2doc) - cp resources/misc/README $PACKAGE ### Assemble AppImage - cp resources/scripts/AppRun $PACKAGE - cp resources/misc/minizinc.desktop $PACKAGE/minizinc.desktop - cp resources/icon.png $PACKAGE/minizinc.png - ARCH=x86_64 appimagetool $PACKAGE MiniZincIDE-${MZNVERSION}-x86_64.AppImage ### Generate checksum - sha256sum MiniZincIDE*.AppImage > MiniZincIDE-${MZNVERSION}-x86_64.sha256 artifacts: name: "minizinc_appimage_${CI_PIPELINE_ID}" paths: [MiniZincIDE*.AppImage, MiniZincIDE*.sha256] needs: ["build:linux"] tags: [linux, docker] .docker_setup: &docker_setup image: ghcr.io/minizinc/docker-build-environment:docker-cli before_script: ### Set the MZNVERSION variable - 'if [ -n "$CI_COMMIT_TAG" ]; then MZNVERSION="$CI_COMMIT_TAG"; else MZNVERSION="build$CI_PIPELINE_ID"; fi' ### Choose the MiniZinc compiler branch - 'if [ -n "$CI_COMMIT_TAG" ]; then MZNREF="$CI_COMMIT_TAG"; elif [ "$CI_COMMIT_REF_NAME" = "master" ]; then MZNREF="master"; else MZNREF="develop"; fi' ### Download Dependencies - mkdir -p linux/amd64 && cd linux/amd64 - curl --silent -o minizinc.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2Fminizinc/jobs/artifacts/$MZNREF/download?job=build:$MZNARCH" && unzip -q minizinc.zip - curl --silent -o vendor.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2Fminizinc-vendor/jobs/artifacts/master/download?job=bundle:$MZNARCH" && unzip -q vendor.zip - mkdir -p ../arm64 && cd ../arm64 - curl --silent -o minizinc.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2Fminizinc/jobs/artifacts/$MZNREF/download?job=build:$MZNARCH-arm64" && unzip -q minizinc.zip - curl --silent -o vendor.zip --location --header "PRIVATE-TOKEN:$ACCESS_TOKEN" "https://gitlab.com/api/v4/projects/minizinc%2Fminizinc-vendor/jobs/artifacts/master/download?job=bundle:$MZNARCH-arm64" && unzip -q vendor.zip - cd ../../ package:docker_alpine: stage: package variables: MZNARCH: "musl" <<: *docker_setup script: - echo "Building image based on \"alpine:latest\" with tag extension \"-alpine\"" - if [ -n "$CI_COMMIT_TAG" ]; then IMAGE_TAG="minizinc/minizinc:${CI_COMMIT_TAG}-alpine"; fi - if [ "$CI_COMMIT_REF_NAME" = "master" ]; then IMAGE_TAG=minizinc/minizinc:latest-alpine; fi - if [ "$CI_COMMIT_REF_NAME" = "develop" ]; then IMAGE_TAG=minizinc/minizinc:edge-alpine; fi - if [ -n "$IMAGE_TAG" ]; then BUILDX_ARGS="--push -t $IMAGE_TAG"; fi - docker buildx build $BUILDX_ARGS --pull --builder=container --platform=linux/amd64,linux/arm64 -f resources/pkg_config/Dockerfile --build-arg BASE='alpine:latest' . needs: [] tags: [linux-arm64, mac-mini] package:docker_ubuntu: stage: package parallel: 3 variables: MZNARCH: "linux" <<: *docker_setup script: - BASES=("null" "ubuntu:latest" "ubuntu:noble" "ubuntu:jammy") - EXTS=("null" "" "-noble" "-jammy") - echo "Building image based on \"${BASES[$CI_NODE_INDEX]}\" with tag extension \"${EXTS[$CI_NODE_INDEX]}\"" - if [ -n "$CI_COMMIT_TAG" ]; then IMAGE_TAG=minizinc/minizinc:${CI_COMMIT_TAG}${EXTS[$CI_NODE_INDEX]}; fi - if [ "$CI_COMMIT_REF_NAME" = "master" ]; then IMAGE_TAG=minizinc/minizinc:latest${EXTS[$CI_NODE_INDEX]}; fi - if [ "$CI_COMMIT_REF_NAME" = "develop" ]; then IMAGE_TAG=minizinc/minizinc:edge${EXTS[$CI_NODE_INDEX]}; fi - if [ -n "$IMAGE_TAG" ]; then BUILDX_ARGS="--push -t $IMAGE_TAG"; fi - docker buildx build $BUILDX_ARGS --pull --builder=container --platform=linux/amd64,linux/arm64 -f resources/pkg_config/Dockerfile --build-arg BASE=${BASES[$CI_NODE_INDEX]} . needs: [] tags: [linux-arm64, mac-mini] minizinc:linux:nogui: image: ghcr.io/minizinc/docker-build-environment:package stage: package variables: MZNARCH: "linux" <<: *packaging_setup script: - PACKAGE=MiniZinc-${MZNVERSION}-linux-x86_64 - mkdir -p $PACKAGE/bin - mkdir -p $PACKAGE/lib ### Package MiniZinc - mv minizinc/bin/* $PACKAGE/bin/ - mv minizinc/share $PACKAGE/share ### Package vendor solvers - mv vendor/gecode/bin/fzn-gecode $PACKAGE/bin/ - cp -r vendor/gecode/share/minizinc/* $PACKAGE/share/minizinc/ - mv vendor/chuffed/bin/fzn-chuffed $PACKAGE/bin/ - cp -r vendor/chuffed/share/minizinc/* $PACKAGE/share/minizinc/ - mv vendor/or-tools/bin/fzn-cp-sat $PACKAGE/bin/ - cp -r vendor/or-tools/share/minizinc/* $PACKAGE/share/minizinc/ - cp vendor/highs/lib64/libhighs.so $PACKAGE/lib/ ### Strip included binaries - (cd $PACKAGE/bin; strip minizinc fzn-gecode fzn-chuffed fzn-cp-sat mzn2doc) ### Compress package - tar -czf $PACKAGE.tar.gz $PACKAGE artifacts: name: "minizinc_linux_nogui_${CI_PIPELINE_ID}" paths: [MiniZinc*.tar.gz] needs: [] tags: [linux, docker] .snap_job: &snap_job stage: package image: ghcr.io/minizinc/docker-build-environment:snap variables: GIT_SUBMODULE_STRATEGY: recursive MZNARCH: "linux" DOWNLOAD_GLOBALIZER: 1 DOWNLOAD_FINDMUS: 1 DOWNLOAD_ANALYSE: 1 <<: *packaging_setup script: - strip minizinc/bin/minizinc vendor/gecode_gist/bin/fzn-gecode vendor/chuffed/bin/fzn-chuffed vendor/or-tools/bin/fzn-cp-sat findMUS/bin/findMUS mzn-analyse/bin/mzn-analyse globalizer/bin/minizinc-globalizer minizinc/bin/mzn2doc - cp resources/pkg_config/snapcraft.yaml . - apt-get update -y - snapcraft --destructive-mode artifacts: name: "minizinc_snap_${CI_PIPELINE_ID}" paths: [minizinc*.snap] needs: ["build:linux"] tags: [linux, docker] package:snap: <<: *snap_job only: [tags, master, develop] package:snap_manual: <<: *snap_job except: [tags, master, develop] when: manual .snap_publish: &snap_publish stage: publish image: ghcr.io/minizinc/docker-build-environment:snap tags: [linux, docker] script: - snapcraft upload minizinc*.snap --release $SNAPCRAFT_CHANNEL needs: ["package:snap"] publish:snap_edge: variables: SNAPCRAFT_CHANNEL: edge <<: *snap_publish only: [develop] publish:snap_stable: variables: SNAPCRAFT_CHANNEL: stable <<: *snap_publish only: [tags] when: manual # Don't publish to stable channel automatically publish:github_edge: stage: publish image: ghcr.io/minizinc/docker-build-environment:package variables: REMOTE_URL: https://minizinc-ci:${ACCESS_TOKEN}@gitlab.com/minizinc/minizinc-ide.git script: # Update edge tag - git config user.name "$GITLAB_USER_NAME" - git config user.email "$GITLAB_USER_EMAIL" - git remote add gitlab_remote "${REMOTE_URL}" || git remote set-url gitlab_remote "${REMOTE_URL}" - git tag -f -a -m "Development build of MiniZinc" edge - git push -f gitlab_remote edge -o ci.skip # Remove old assets - gh release --repo MiniZinc/MiniZincIDE view edge --json assets --jq '.assets[].name' | xargs -n 1 -r gh release --repo MiniZinc/MiniZincIDE delete-asset -y edge # Upload new assets - gh release --repo MiniZinc/MiniZincIDE upload edge MiniZincIDE*.dmg MiniZincIDE*.exe MiniZincIDE*.AppImage MiniZincIDE*.tgz MiniZincIDE*.sha256 needs: ["package:osx", "package:win64", "package:appimage", "package:linux"] only: [develop] tags: [linux, docker] publish:github_stable: stage: publish image: ghcr.io/minizinc/docker-build-environment:package script: - NOTES="This release" - '[ "$(echo "$CI_COMMIT_TAG" | cut -d. -f3)" == "0" ] && NOTES="$NOTES adds several new features and"' - NOTES="$NOTES fixes a number of bugs, see https://docs.minizinc.dev/en/${CI_COMMIT_TAG}/changelog.html for a full change log. Binary releases for different platforms are available in the bundled packages of the MiniZinc IDE at https://github.com/minizinc/minizincide/releases." - gh release --repo MiniZinc/libminizinc create --draft --title "MiniZinc $CI_COMMIT_TAG" --notes "$NOTES" "$CI_COMMIT_TAG" - echo "Please refer to the change log for details:" > ide_notes - echo "https://docs.minizinc.dev/en/${CI_COMMIT_TAG}/changelog.html" >> ide_notes - gh release --repo MiniZinc/MiniZincIDE create --draft --title "MiniZinc $CI_COMMIT_TAG" --notes-file ide_notes "$CI_COMMIT_TAG" MiniZincIDE*.dmg MiniZincIDE*.exe MiniZincIDE*.AppImage MiniZincIDE*.tgz MiniZincIDE*.sha256 needs: ["package:osx", "package:win64", "package:appimage", "package:linux"] only: [tags] except: [edge] tags: [linux, docker] ================================================ FILE: LICENSE.txt ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: MiniZincIDE/CHANGES ================================================ 2026-04-30 v2.9.7 - Update to MiniZinc 2.9.7. 2026-04-24 v2.9.6 - Fix syntax highlighting of the `list` keyword (#227). 2026-01-23 v2.9.5 - Update to MiniZinc 2.9.5. 2025-09-29 v2.9.4 - Update to MiniZinc 2.9.4. 2025-05-23 v2.9.3 - Suppress warnings generated when running compiled solution checkers. 2025-03-06 v2.9.2 - Fix the packaging of the OR Tools solver on linux distributions. 2025-03-03 v2.9.1 - Ensure locations for warnings and errors are shown when the stack dump is empty (#212). - Use scroll buttons when the tab bar overflows to ensure the side panels can be resized (#213). 2025-02-11 v2.9.0 - Fix broken documentation link in the help menu item. 2024-10-02 v2.8.7 - Update to MiniZinc 2.8.7. 2024-09-25 v2.8.6 - Update to MiniZinc 2.8.6. 2024-05-03 v2.8.5 - Update to MiniZinc 2.8.5. 2024-05-10 v2.8.4 - Fix bundled OpenSSL libraries on Windows. - Allow MOOC submission window to be scrolled. - Increase hard process termination timeout to 1s. 2024-02-01 v2.8.3 - Increase maximum number of threads from default Qt limit (#196). 2023-12-15 v2.8.2 - Make process handling more robust to failures. 2023-11-27 v2.8.1 - Fix command used to run findMUS and Globalizer. - Add ability to set the ports used for the visualisation server. - Add option for printing the visualisation server URL for debugging purposes. - Add more information to subprocess error messages. 2023-11-16 v2.8.0 - Fix unreadable cheat sheet font colour in dark mode (#191). - Add option to output objective value and enable by default. - Show manually input parameters in output window. - Fix missing checker messages (#192). - Fix incorrect OpenSSL version in Linux packages (#189). 2023-05-20 v2.7.6 - Update to MiniZinc 2.7.6. 2023-06-07 v2.7.5 - Use native Qt dark mode on Windows where supported. - Improve behaviour of the custom solver parameter dialog. 2023-05-11 v2.7.4 - Update to MiniZinc 2.7.4. 2023-04-20 v2.7.3 - Only show MOOC error code when response is actually an error (#176). 2023-04-05 v2.7.2 - Fix patching of `RPATH` for binaries and libraries in Snap package. 2023-03-31 v2.7.1 - Fix highlighting of multiline comments starting with /*/ (#172). - Fix bundling of incompatible OpenSSL version in linux packages. - Remove support for glibc 2.27 and earlier from AppImage and tarball linux packages. The Snap package may be used instead on such systems. 2023-02-23 v2.7.0 - Fix a bug where model selection dialog could run the wrong model. - Fix a bug where the same data file could be added to the MiniZinc command twice. - Ensure user config directory is created when modifying solver search paths (#167). - Ensure that IDE windows cannot spawn off-screen. - Add tooltips to the CP-Profiler status bar legend. - Add support for mooc submissions which include file editing history. 2022-06-23 v2.6.4 - Ensure the extra parameter filter is cleared when the textbox is cleared. 2022-05-06 v2.6.3 - Improve UI and dark mode for CP Profiler. - Fix CP Profiler tree-builder signal/slot connection (#160). - Fix deadlock in CP Profiler tree building (#162). - Make project loading more resilient to errors (#165). 2022-03-22 v2.6.2 - Don't print expected error messages for MOOC submissions. - Fix custom parameter widget dark mode CSS. 2022-03-03 v2.6.1 - Fix crash when the solver for an unsaved configuration is removed. - Fix bug where the selected solver could incorrectly change when a configuration option is altered. 2022-02-18 v2.6.0 - Add support for specifying submission terms for MOOC. - Ensure newly loaded configs override synced options (#144). - Fix check for empty project to avoid incorrect warnings when closing. - Maintain modified solver configurations when using preferences dialog. - Support using arm64 version of MiniZinc with x86_64 IDE build on macOS. - Fix crash when no solver configurations are available. - Remove WebEngine-based visualisation feature and implement HTTP/WebSocket server based visualisations. - Add support for dark mode detection on Windows. - Implement foldable output widget supporting output sections. - Support both Qt 5 and Qt 6. - Allow tab to shift multiple lines right. - Re-implement support for detached solver processes. - Allow the project/solver configuration panes to take the full height of the main window. - Implement new multi-tabbed preferences dialog. - Ignore errors in non-current files during background code checking. - Fix undefined behaviour in main window event filter (#154). - Fix crash when terminating solvers due to closing the main window. - Confirm before removing files from project (#149). 2021-03-19 v2.5.5 - Fix editing of custom string parameters so they don't get converted to floats. - Fix crash on Windows caused when the PATH environment contains unicode characters. 2021-03-16 v2.5.4 - Fix possible crash due to incorrect use of WriteFile on Windows. - Ensure Gecode Gist dependencies are present in the Linux bundle and AppImage (#132). - Fix crash when stopping solver during exit. - Don't show irrelevant context menu entries in the project explorer. - Add support for HTTP/S links in the output pane. - Fix crash when saving CP Profiler executions where there is no info associated with a node. - Show a warning when there are open files which are not part of a MOOC submission. - Fix double spinbox precision issues (#134). - Include Gecode Gist and CP Profiler dependencies in Snap package. - Allow opening of multiple files through the open file menu option. - Ensure file dialogs save last path when opening files. - Make the escape key close the find/replace dialog when focussed on any child widget. - Allow setting MOOC submission items as mandatory. 2020-24-06 v2.5.3 - Only reset config window item focus if it is still focused, preventing spurious changes in focus during code checking. - Fix handling of final statuses, including UNSAT (#123). - Remove -s flag support from Gecode Gist solver configuration (#125). - Fix crash when saving a project with no solver selected (#127). - Correctly remove temporary parameter configuration files after use (#128, #129). - Fix the time limit readout in the status bar when solving. 2020-11-06 v2.5.2 - Properly resize extra flags table after adding parameters (#119). - Use the minimal configuration to check the model interface (#118). - Allow omitting builtin solver version in project JSON. - Don't mark as modified when loading non-synced solver configurations. - Ensure the last open configuration in a project is selected when loaded. - Fix the default values of solution truncation and output window clearing. - Process unrecognised extra flags from old project configurations. - Fix watching for modification of the additional data box. - Fix the alignment of line numbers. - Make behaviour controls more narrow to accommodate smaller window sizes. - Defocus config window widgets when updating solver config so values of currently edited fields are updated. - Pass user input data correctly during compilation. - Remove solns2out options from MiniZinc call when compiling. 2020-10-22 v2.5.1 - Fix typo when passing solver statistics option to minizinc (#112). - Fix missing statistics output (#112). - Add support for colour themes (#110). - Don't prompt for saving after adding/removing files from the Untitled project. - Fix running of compiled FlatZinc files. - Show error message when trying to load an invalid configuration file. - Ensure all output is sent to the output console, and that fragments in standard error output appear when a newline is written to standard output (#114). - Fix running of solver configurations from the project explorer. - Improve performance of adding a large number of extra flags at once. - Add support for 64-bit integer extra flags. - Add support for setting both solver backend flags and MiniZinc command flags (#113). - Improve interface for adding extra parameters, allowing search/filter and multiselection of known parameters. 2020-09-25 v2.5.0 - Add fallback libraries if user does not have libnss3. - Remove support for the old binary storage format of projects used prior to version 2.2.0. These must be opened and re-saved with version 2.4.3 to remain compatible. - Include experimental CP-profiler through the *MiniZinc* > *Profile search* option for supported solvers. - Redesign the solver configuration window. - Use parameter configuration files rather than passing command-line options directly. - Show solver configurations and checkers in their own sections in the project explorer. - Allow multiselection in the project explorer for running particular sets of files. - Allow MiniZinc to manage subprocesses by itself. - Allow non-privileged installs of the IDE on Windows. - Correctly remove files from old installations of the IDE on Windows. - Enable scroll bars in the preferences dialog to allow for low resolution displays. - Prompt to save modified files before performing MOOC submissions or running models. - Fix infinite recursion when a model file for a MOOC submission doesn't exist. - Use --output-mode checker for MOOC solution submission where supported. - Fully support unicode on Windows. 2020-03-03 v2.4.3 - Disable menu items that don't make sense when all tabs are closed, fix behaviour of stop button when all tabs closed (fixes several crashes). - Add x86_64 suffix to linux package name (#96). - Make boolean extra solver options with a default of true functional. - Only read linter results if it exited normally (#97). - Update alpine version to 3.11 - Resolve paths in _mooc to paths (allowing submission of models in subdirectories). 2020-01-10 v2.4.2 - Fix syntax highlighting of keywords, and add syntax highlighting for interpolated strings. - Redraw when switching to/from dark mode, and fix dark mode header colours. - Fix "Select all" menu item. 2019-12-09 v2.4.1 - Display error message when submission to MOOC provider fails. - Fix shift left and shift right indentation behaviour when selecting text backwards. - Make "previous tab" and "next tab" actions cycle rather than stop at first/last tab. - Fix OpenSSL library in binary distribution to enable update checks and submission to MOOCs again. v2.4.0 - Parse timing and statistics output produced by compiler, and display as profiling information next to each line in the model. - Enable run/compile action on data files. This automatically selects the model file if there is only one, or presents a dialog for selecting the model if there are multiple. - Select first data file in parameter dialog if there was no previous selection, and always focus parameter dialog. - Fix dark mode detection on macOS 10.15, improve dark mode colors a bit and fixed some dark mode bugs. - Make background compilation of a model (used to display syntax and type errors) a bit more stable. - Highlight current line. - Support .json as file extension for data files. - Remember whether wrap around, case sensitivity and regular expression was selected in find/replace dialog, pre-select the find/replace text when find/replace widget is openend, and close find/replace widget when ESC is pressed while editor has focus. - Avoid infinite loop in wrap around replace all. - Fix memory management for HTML visualisation windows, and resize docked HTML visualisation widgets to take up equal space. 2019-09-12 v2.3.2 - Update to MiniZinc 2.3.2. 2019-07-10 v2.3.1 - Remove incorrect symbolic link and fix qt.conf for some bundled distributions. - Fix check for availability of dark mode on older versions of macOS. - Fix a typo in the cheat sheet. - Provide more robust solution for checking the model parameters, which will get rid of some "internal error" messages. - Always show directory selection dialog in the Windows installer. Addresses #89. - Improved the configuration files for some bundled solvers, provides nicer configuration interface. 2019-06-26 v2.3.0 - The IDE will now check MiniZinc code for syntax and type errors - The editor performs simple code completion for MiniZinc keywords - Ensure cursor is visible (editor scrolls to cursor position) when pressing tab or enter. Fixes #71. - Replace find dialog with inline widget and incremental search. - Support dark mode on macOS. - Add support for extra solver flags (parsed from solver configuration). - IDE now only uses minizinc executable (not mzn2fzn and solns2out). - Re-dock configuration editor when closing un-docked window. - Handle quotes when parsing additional solver command line arguments. Fixes #77. - Add workaround for the missing libnss requirements - Allow spaces in $DIR in MiniZincIDE.sh (Fixes #81) 2018-10-31 v2.2.3 - Only run solution checker if it is enabled in the solver configuration dialog. 2018-10-26 v2.2.2 - Add line/column display in status bar. Fixes #65. - Optional parameters don't have to be defined in input dialog. - Fix race condition in constructor of HTMLWindow. Fixes #64. - Provide mzn-json-init / mzn-json-init-end handlers to initialise HTML window before first solution is produced. - Add version information and minimum system version into Info.plist on macOS. Fixes #66. - Manage multiple open visualisation windows, and implement re-solve function that can be initiated from a visualisation. 2018-09-06 v2.2.1 - Improve dark mode by changing line numbers to dark background. - Make parameter input dialog scrollable. - Fix solution compression limit, and output one solution per block of compressed solutions. 2018-08-24 v2.2.0 - Update to MiniZinc 2.2.0. - Change solver configuration interface to work with new MiniZinc 2.2.0 solver configurations. - Add support for solution checker models. - Better process management (to make sure solvers are terminated). - Change project files to be based on JSON format. - Better support for solver HTML output (used e.g. for Globalizer and FindMUS). - Fix shift left/shift right functionality. - Support for running models without saving them first. - Fix file dialogs to use correct file extensions. 2018-01-10 v2.1.7 - Update to MiniZinc 2.1.7. - Fix problem where files with a . in the filename could not be run (bug #44). - Fix font settings (were not saved reliably on some platforms). - Enable generic interface for submitting assignments (not just to Coursera). - Fix output handling for solvers that do not run mzn2fzn. - Fix hidden solution display when there are exactly as many solutions as the configured threshold for hiding solutions (bug #42). - Add configuration option to print timing information for each solution. 2017-09-22 v2.1.6 - Update to MiniZinc 2.1.6. 2017-05-17 v2.1.5 - Update to MiniZinc 2.1.5. - Fix an issue where solver output may not get printed if it occurs too quickly after the solver has started. 2017-03-16 v2.1.4 - Update to MiniZinc 2.1.4. - Fix major race condition that would crash the IDE when it didn't detect that a solver process had finished. - Improve HTML output by making sure every line is terminated by a newline. 2017-02-06 v2.1.3 - Update to MiniZinc 2.1.3. - Avoid crashes and print error messages when mzn2fzn subprocess crashes. - Changed meaning of "User-defined behavior" options, to have a clear distinction between optimisation and satisfaction problems. - Fix buffering of error output from mzn2fzn process (which would sometimes not be printed to the output window). - Suppress output after configurable number of solutions (to avoid overloading the IDE output box). 2016-12-20 v2.1.2 - Update to MiniZinc 2.1.2. 2016-12-14 v2.1.1 - Add option to print mzn2fzn statistics to project configuration. - Update to MiniZinc 2.1.1. 2016-11-16 v2.1.0 - Add new bundled solvers: Chuffed, CBC, Gurobi - Change update check to use Google Analytics (opt-in) - Fix a crash in the syntax highlighter when changing documents (e.g when saving a previously unsaved file). - Fix buffering problems on Windows (could lead to solver output not being shown). - Fix a crash when stopping a long-running compilation. 2016-08-30 v2.0.97 - Update to include MiniZinc 2.0.97 beta release. 2016-07-31 v2.0.14 - Implement new Coursera submission system. - Reload list of data files after removing a file through the project view, and use persistent indices to fix file removal. Fixes #11. - Fix renaming of files through the project explorer. Renaming should now work using the usual platform editing key, and using the context menu option. Fixes #12. - Don't add empty file name to list of data files when user cancels file dialog. Fixes #13. - Add same leading white space as on current line when pressing return (maintain indenting). - Add support for new Qt WebEngine framework (since Qt WebKit is not available in Qt 5.6). 2016-03-26 v2.0.13 - Flush output more consistently when process finished, hopefully fixing problem where solutions were missing from output. - Updated to include MiniZinc 2.0.13 2016-02-26 v2.0.12 - Fix link to MiniZinc issue tracker. - Add configuration option to clear solver output for each run. - Remember whether previous run used data file or manual data input. - Updated to include MiniZinc 2.0.12 2016-01-15 v2.0.11 - Updated to include MiniZinc 2.0.11 2015-12-10 v2.0.10 - Updated to include MiniZinc 2.0.10 2015-12-07 v2.0.9 - Remove (unimplemented) menu item for adding files to a project. - Fix version number comparison to work for multi-digit minor and patch versions. 2015-10-19 v2.0.8 - Only disable the run and compile actions when a solving process is currently running (keep editor and rest of the user interface enabled). - Keep editor font setting synchronised across different IDE windows. 2015-10-06 v2.0.7 - Changed version number scheme to coincide with MiniZinc version. - Disable all editing while the solver is running (avoids race conditions). - Fix behaviour of stop button: avoid race condition when pressing it twice, and signal that process has stopped when the initial SIGINT was successful. - Split up extra mzn2fzn command line arguments so they get passed correctly. - Changed behaviour of MiniZinc path input (now doesn't check for presence of mzn2fzn every time the cursor leaves the input box). - Add configuration option to use "default behaviour" when running models, which is to output all intermediate solutions for optimisation problems, and stop after one solution for satisfaction problems. - Fix "Add solver" dialog (sometimes the option to add solvers would disappear from the drop-down menu). - On Windows, configure Gecode/Gist to run the correct batch file. - Fix a crash when activating the (un)comment or go to line actions while on the configuration tab. - Avoid opening multiple "File modified" dialogs for the same file. 2015-07-30 v0.9.9 - Fix for clicking on error messages on Windows - Fix syntax highlighting, used to turn itself off when saving file under a different name - Set default font more consistently on different platforms 2015-07-01 v0.9.8 - Add "dark mode" to change text colours - Various bug fixes - Add integration with Coursera MiniZinc course 2015-05-12 v0.9.7 - Various bug fixes - Improved compatibility with MiniZinc 2.0.2 - Bundled binary release (includes MiniZinc and some solvers) 2015-01-10 v0.9.6 - When killing a process (using the Stop action or timeout), send CTRL-C first, which allows the solver to exit gracefully. Solvers that do not react to CTRL-C are killed as before. - Fix copy/paste actions to also work in output window and to create rich text (including syntax colouring) - Avoid creating an empty .fzn file during compilation - Accept drag-and-drop to main window, opens dragged files - Quit menu action on Mac OS actually quits instead of just closing all open windows - Warn when loading large FlatZinc files - Assume UTF8 text encoding when loading and saving files - Keep selection of .dzn file in configuration tab when opening another .dzn file 2014-09-18 v0.9.5 - load new projects or files into existing project if it is empty and unmodified (avoids opening a new window) - fixed a bug when right-clicking into an empty area in the project explorer - fixed a bug when renaming files (in particular data files) in the project explorer - add support for multi-line (C-style) comments in the syntax highlighting - on Mac OS, show a Window menu to make it easy to switch between open projects, and open all files dragged onto the dock icon in the same project - fixed the hard-coded library path for the G12 lazyfd solver - fixed a bug that could cause a crash when closing and re-opening a project 2014-07-31 v0.9.4 - Project "drawer", better handling of projects - Tool bar - Limit additional arguments dialog to at most 10 parameters - Show MiniZinc version information in configuration dialog - More consistent handling of PATHs to MiniZinc and solvers - Print name of data files and additional arguments when running a solver - On Mac OS, enable QuickLook for MiniZinc files - Add recent files and projects to file menu - File dialogs open in the previously used directory 2014-04-01 v0.9.3 - Add elapsed time to output - Fix crash for files given as command line arguments - Make output window undockable - Add check for updates on startup - Fix bugs #4, #5, #8, #9 2014-02-03 v0.9.2 - Add verbose solver option - Save currently open project tab in project file - Ignore empty additional model parameters - Fix crash when closing modified Untitled tabs without saving - Fix crash after error message from solver - Fix project file format - Ask for parameter input when compiling (not just when solving) 2014-01-30 v0.9.1 Initial release ================================================ FILE: MiniZincIDE/MiniZincIDE.pri ================================================ QT += core gui widgets websockets VERSION = 2.9.7 DEFINES += MINIZINC_IDE_VERSION=\\\"$$VERSION\\\" bundled { DEFINES += MINIZINC_IDE_BUNDLED } output_version { write_file($$OUT_PWD/version, VERSION) } !isEmpty(CXX_PREFIX) { message(Using CXX_PREFIX = $$CXX_PREFIX) QMAKE_CXX = $$CXX_PREFIX $$QMAKE_CXX } CONFIG += c++11 macx { ICON = $$PWD/mznide.icns OBJECTIVE_SOURCES += \ $$PWD/darkmodenotifier_macos.mm LIBS += -framework Cocoa macx-xcode { QMAKE_INFO_PLIST = $$PWD/mznide-xcode.plist } else { QMAKE_INFO_PLIST = $$PWD/mznide-makefile.plist } } win32 { LIBS += -ladvapi32 } RC_ICONS = $$PWD/mznide.ico CONFIG += embed_manifest_exe SOURCES += \ $$PWD/codechecker.cpp \ $$PWD/configwindow.cpp \ $$PWD/darkmodenotifier.cpp \ $$PWD/elapsedtimer.cpp \ $$PWD/extraparamdialog.cpp \ $$PWD/history.cpp \ $$PWD/ide.cpp \ $$PWD/ideutils.cpp \ $$PWD/mainwindow.cpp \ $$PWD/codeeditor.cpp \ $$PWD/highlighter.cpp \ $$PWD/fzndoc.cpp \ $$PWD/outputwidget.cpp \ $$PWD/preferencesdialog.cpp \ $$PWD/process.cpp \ $$PWD/profilecompilation.cpp \ $$PWD/projectbrowser.cpp \ $$PWD/server.cpp \ $$PWD/gotolinedialog.cpp \ $$PWD/paramdialog.cpp \ $$PWD/outputdockwidget.cpp \ $$PWD/checkupdatedialog.cpp \ $$PWD/project.cpp \ $$PWD/moocsubmission.cpp \ $$PWD/esclineedit.cpp \ $$PWD/solver.cpp \ $$PWD/theme.cpp HEADERS += \ $$PWD/history.h \ $$PWD/mainwindow.h \ $$PWD/codechecker.h \ $$PWD/codeeditor.h \ $$PWD/configwindow.h \ $$PWD/darkmodenotifier.h \ $$PWD/elapsedtimer.h \ $$PWD/exception.h \ $$PWD/extraparamdialog.h \ $$PWD/highlighter.h \ $$PWD/fzndoc.h \ $$PWD/ide.h \ $$PWD/ideutils.h \ $$PWD/outputwidget.h \ $$PWD/preferencesdialog.h \ $$PWD/process.h \ $$PWD/profilecompilation.h \ $$PWD/projectbrowser.h \ $$PWD/server.h \ $$PWD/gotolinedialog.h \ $$PWD/paramdialog.h \ $$PWD/outputdockwidget.h \ $$PWD/checkupdatedialog.h \ $$PWD/project.h \ $$PWD/moocsubmission.h \ $$PWD/esclineedit.h \ $$PWD/solver.h \ $$PWD/theme.h FORMS += \ $$PWD/configwindow.ui \ $$PWD/extraparamdialog.ui \ $$PWD/mainwindow.ui \ $$PWD/outputwidget.ui \ $$PWD/preferencesdialog.ui \ $$PWD/projectbrowser.ui \ $$PWD/gotolinedialog.ui \ $$PWD/paramdialog.ui \ $$PWD/checkupdatedialog.ui \ $$PWD/moocsubmission.ui RESOURCES += \ $$PWD/minizincide.qrc include($$PWD/../cp-profiler/cp-profiler.pri) QT += network sql ================================================ FILE: MiniZincIDE/MiniZincIDE.pro ================================================ TARGET = MiniZincIDE TEMPLATE = app INCLUDEPATH += $$PWD/../ SOURCES += main.cpp include($$PWD/MiniZincIDE.pri) target.path = $$PREFIX/bin INSTALLS += target ================================================ FILE: MiniZincIDE/README.txt ================================================ ================================================================ MiniZinc IDE ================================================================ http://www.minizinc.org The MiniZinc IDE is copyright 2013-2019 Monash University, NICTA, Data61/CSIRO. Please see LICENSE.txt for license information. Detailed installation instructions for Windows, Linux and macOS can be found at https://www.minizinc.org/doc-latest/en/installation.html. ================================================ FILE: MiniZincIDE/cheat_sheet.mzn ================================================ %%%%%%%%%%%%%%%%%%%%%%%% % % % MiniZinc cheat sheet % % % %%%%%%%%%%%%%%%%%%%%%%%% /****************** * Model structure * ******************/ include "globals.mzn"; % use globals library int: n; % fixed integer parameter var 1..n: x; % integer decision variable array[1..n] of var 1..n: y; % array of integer decision variables constraint sum(y) <= x; % constraint solve satisfy; % find satisfying solution output ["Solution:\n", "x = ", show(x), % output list of strings "y = \(y)"]; % interpolation \(expression) /*********************** * Basic types * ***********************/ int: i; 3..5: j; % integer float: f; 3.0..5.0: g; % floating point number bool: b; % boolean set of int: s; % set set of 3..5: t; enum E = { Boat, Airplane }; % enumerated type /*********************** * Type-insts * ***********************/ var int: x; % declare variable % (also with float, bool, and set of int) var 3..7: y; % declare variable with domain (also 3.0..7.0) var set of 10..20: s; % declare set variable (only fixed set of int!) array[1..4,1..10] of var 0.0..100.0: f; % declare 2d array of float variables var opt 1..10: ox; % declare optional int variable (can be 1..10 or % "absent", written <>) /********************* * Basic Constraints * *********************/ constraint x = y; constraint x < y; constraint x <= y; constraint x > y; constraint x >= y; constraint x != y; % not equals /********************** * Logical Connectives * **********************/ % conditionals int: d = if i > 10 then a elseif i > 0 then b else c endif; constraint if x < y then y < z else y > z endif; constraint x < y \/ y != z; % logical "or" constraint x < y /\ y != z; % logical "and" constraint x < y -> y != z; % logical implication constraint not (x < y /\ y > z); % logical negation /****************** * Set Constraints * ******************/ constraint s subset t; % non-strict subset relation constraint s intersect t subset w; % intersection constraint s union t subset w; % union /*************************** * Predicates and Functions * ***************************/ constraint all_different(x); % predicate call constraint mydiv(x,y) = 2; % function call /******************************** * Comprehensions and generators * ********************************/ array[int] of int: a = [ i | i in 1..10]; % create array [1,2,3,4,5,6,7,8,9,10] array[int] of int: a = [ i | i in 1..10 where i mod 3=0]; % create array [3, 6, 9] constraint forall (i,j in 1..n where i CheckUpdateDialog::CheckUpdateDialog(QWidget *parent) : QDialog(parent), ui(new Ui::CheckUpdateDialog) { ui->setupUi(this); } CheckUpdateDialog::~CheckUpdateDialog() { delete ui; } ================================================ FILE: MiniZincIDE/checkupdatedialog.h ================================================ #ifndef CHECKUPDATEDIALOG_H #define CHECKUPDATEDIALOG_H #include namespace Ui { class CheckUpdateDialog; } class CheckUpdateDialog : public QDialog { Q_OBJECT public: explicit CheckUpdateDialog(QWidget *parent = 0); ~CheckUpdateDialog(); private: Ui::CheckUpdateDialog *ui; }; #endif // CHECKUPDATEDIALOG_H ================================================ FILE: MiniZincIDE/checkupdatedialog.ui ================================================ CheckUpdateDialog Qt::ApplicationModal 0 0 480 299 0 0 387 226 Automatic check for updates true QLayout::SetFixedSize QLayout::SetMaximumSize QLayout::SetMinimumSize 4 0 0 100 100 100 100 :/images/mznicon.png true Qt::Vertical 20 40 Qt::Horizontal QSizePolicy::Preferred 10 10 QLayout::SetMinimumSize 0 4 4 4 0 0 <html><head/><body><p>The MiniZinc IDE can check automatically once a day whether any updates are available.</p><p>Please note that every update check sends anonymised statistics to Google Analytics. We only use these statistics to better understand our user base and to justify funding for further development!</p><p><span style=" font-weight:600;">Do you want to enable the automatic update check?</span></p></body></html> true QLayout::SetMinimumSize Qt::Horizontal 40 20 No Yes true 0 0 <html><head/><body><p>You can disable the update check at any time in the preferences dialog.</p></body></html> true yesbutton clicked() CheckUpdateDialog accept() 485 215 270 119 nobutton clicked() CheckUpdateDialog reject() 415 215 270 119 ================================================ FILE: MiniZincIDE/codechecker.cpp ================================================ #include "codechecker.h" #include "process.h" CodeChecker::~CodeChecker() { cancel(); } void CodeChecker::connectSignals() { connect(&p, &MznProcess::started, this, &CodeChecker::onStarted); connect(&p, &MznProcess::outputStdOut, this, &CodeChecker::onLine); connect(&p, &MznProcess::outputStdError, this, &CodeChecker::onLine); connect(&p, &MznProcess::finished, this, &CodeChecker::onFinished); } void CodeChecker::start(const QString& modelContents, SolverConfiguration& sc, const QString& wd) { cancel(); connectSignals(); inRelevantError = false; curError = MiniZincError(); mznErrors.clear(); SolverConfiguration checkSc(sc.solverDefinition); checkSc.additionalData = sc.additionalData; checkSc.extraOptions = sc.extraOptions; QStringList args; args << "--model-check-only" << "-"; input = modelContents; p.start(checkSc, args, wd); } void CodeChecker::cancel() { p.disconnect(); p.terminate(); } void CodeChecker::onStarted() { p.writeStdIn(input); p.closeStdIn(); } void CodeChecker::onLine(const QString& l) { QJsonParseError error; auto json = QJsonDocument::fromJson(l.toUtf8(), &error); if (json.isNull()) { return; } auto msg = json.object(); if (msg["type"] != "error" && msg["type"] != "warning") { return; } if (!msg["location"].isObject()) { return; } auto loc = msg["location"].toObject(); MiniZincError e; e.isWarning = msg["type"] == "warning"; e.filename = loc["filename"].toString(); e.first_line = loc["firstLine"].toInt(); e.first_col = loc["firstColumn"].toInt(); e.last_line = loc["lastLine"].toInt(); e.last_col = loc["lastColumn"].toInt(); e.msg = msg["message"].toString(); if (e.filename == "stdin") { // Ignore errors that aren't from this file mznErrors.push_back(e); } } void CodeChecker::onFinished() { emit finished(mznErrors); } ================================================ FILE: MiniZincIDE/codechecker.h ================================================ #ifndef CHECKCODE_H #define CHECKCODE_H #include #include "codeeditor.h" #include "process.h" class CodeChecker : public QObject { Q_OBJECT public: explicit CodeChecker(QObject *parent = nullptr) : QObject(parent), p(this) {} ~CodeChecker(); void start(const QString& modelContents, SolverConfiguration& sc, const QString& wd); void cancel(void); signals: void finished(const QVector& mznErrors); private slots: void onStarted(void); void onLine(const QString& data); void onFinished(); private: MznProcess p; QString input; bool inRelevantError = false; MiniZincError curError; QVector mznErrors; void connectSignals(); }; #endif // CHECKCODE_H ================================================ FILE: MiniZincIDE/codeeditor.cpp ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include "codeeditor.h" #include "ide.h" #include "mainwindow.h" void CodeEditor::initUI(QFont& font) { setFont(font); QFontMetrics metrics(font); setTabStopDistance(metrics.horizontalAdvance(' ') * indentSize); lineNumbers= new LineNumbers(this); debugInfo = new DebugInfo(this); editorHeader = new EditorHeader(this); debugInfo->hide(); connect(this, &QPlainTextEdit::blockCountChanged, this, &CodeEditor::setViewportWidth); connect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::setLineNumbers); connect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::setDebugInfoPos); connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CodeEditor::cursorChange); connect(document(), &QTextDocument::modificationChanged, this, &CodeEditor::docChanged); connect(document(), &QTextDocument::contentsChanged, this, &CodeEditor::contentsChanged); setViewportWidth(0); cursorChange(); highlighter = new Highlighter(font,theme,darkMode,document()); setTheme(theme, darkMode); QTextCursor cursor(textCursor()); cursor.movePosition(QTextCursor::Start); setTextCursor(cursor); ensureCursorVisible(); setFocus(); } CodeEditor::CodeEditor(QTextDocument* doc, const QString& path, bool isNewFile, bool large, QFont& font, int indentSize0, bool useTabs0, const Theme& theme0, bool darkMode0, QTabWidget* t, QWidget *parent) : QPlainTextEdit(parent), loadContentsButton(nullptr), tabs(t), indentSize(indentSize0), useTabs(useTabs0), theme(theme0), darkMode(darkMode0) { if (doc) { QPlainTextEdit::setDocument(doc); } initUI(font); if (isNewFile) { filepath = ""; filename = path; } else { filepath = QFileInfo(path).absoluteFilePath(); filename = QFileInfo(path).fileName(); } if (large) { setReadOnly(true); QPushButton* pb = new QPushButton("Big file. Load contents?", this); connect(pb, &QPushButton::clicked, this, &CodeEditor::loadContents); loadContentsButton = pb; } completer = new QCompleter(this); QStringList completionList; completionList << "annotation" << "array" << "bool" << "constraint" << "diff" << "else" << "elseif" << "endif" << "enum" << "float" << "function" << "include" << "intersect" << "maximize" << "minimize" << "output" << "predicate" << "satisfy" << "solve" << "string" << "subset" << "superset" << "symdiff" << "test" << "then" << "union" << "where"; completionList.sort(); completionModel.setStringList(completionList); completer->setModel(&completionModel); completer->setCaseSensitivity(Qt::CaseSensitive); completer->setModelSorting(QCompleter::CaseSensitivelySortedModel); completer->setWrapAround(false); completer->setWidget(this); completer->setCompletionMode(QCompleter::PopupCompletion); QObject::connect(completer, QOverload::of(&QCompleter::activated), this, &CodeEditor::insertCompletion); modificationTimer.setSingleShot(true); QObject::connect(&modificationTimer, &QTimer::timeout, this, &CodeEditor::contentsChangedWithTimeout); auto* mw = qobject_cast(parent); if (mw != nullptr) { QObject::connect(this, &CodeEditor::escPressed, mw, &MainWindow::closeFindWidget); } setAcceptDrops(false); installEventFilter(this); } void CodeEditor::loadContents() { static_cast(qApp)->loadLargeFile(filepath,this); } void CodeEditor::insertCompletion(const QString &completion) { QTextCursor tc = textCursor(); int extra = completion.length() - completer->completionPrefix().length(); tc.movePosition(QTextCursor::Left); tc.movePosition(QTextCursor::EndOfWord); tc.insertText(completion.right(extra)); setTextCursor(tc); } void CodeEditor::loadedLargeFile() { setReadOnly(false); delete loadContentsButton; loadContentsButton = nullptr; } void CodeEditor::setDocument(QTextDocument *document) { if (document) { delete highlighter; highlighter = nullptr; } disconnect(this, &QPlainTextEdit::blockCountChanged, this, &CodeEditor::setViewportWidth); disconnect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::setLineNumbers); disconnect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::setDebugInfoPos); disconnect(this, &QPlainTextEdit::cursorPositionChanged, this, &CodeEditor::cursorChange); disconnect(this->document(), &QTextDocument::modificationChanged, this, &CodeEditor::docChanged); QList noSelections; setExtraSelections(noSelections); QPlainTextEdit::setDocument(document); if (document) { QFont f= font(); highlighter = new Highlighter(f, theme, darkMode, document); } connect(this, &QPlainTextEdit::blockCountChanged, this, &CodeEditor::setViewportWidth); connect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::setLineNumbers); connect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::setDebugInfoPos); connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CodeEditor::cursorChange); connect(this->document(), &QTextDocument::modificationChanged, this, &CodeEditor::docChanged); } void CodeEditor::setTheme(const Theme& _theme, bool _darkMode) { darkMode = _darkMode; theme = _theme; highlighter->setTheme(theme, darkMode); highlighter->rehighlight(); auto palette = this->palette(); palette.setColor(QPalette::Text, theme.textColor.get(darkMode)); palette.setColor(QPalette::Base, theme.backgroundColor.get(darkMode)); palette.setColor(QPalette::Highlight, theme.textHighlightColor.get(darkMode)); palette.setColor(QPalette::HighlightedText, theme.textColor.get(darkMode)); this->setPalette(palette); viewport()->setStyleSheet(theme.styleSheet(darkMode)); cursorChange(); // Ensure extra selections are the correct colours } void CodeEditor::setIndentSize(int size) { indentSize = size; QFontMetrics metrics(font()); setTabStopDistance(metrics.horizontalAdvance(' ') * size); } Highlighter& CodeEditor::getHighlighter() { return *highlighter; } void CodeEditor::docChanged(bool c) { int t = tabs == nullptr ? -1 : tabs->indexOf(this); if (t != -1) { QString title = tabs->tabText(t); title = title.mid(0, title.lastIndexOf(" *")); if (c) title += " *"; tabs->setTabText(t,title); } } void CodeEditor::contentsChanged() { modificationTimer.start(500); } void CodeEditor::contentsChangedWithTimeout() { emit changedDebounced(); } void CodeEditor::keyPressEvent(QKeyEvent *e) { if (completer->popup()->isVisible()) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: case Qt::Key_Escape: case Qt::Key_Tab: case Qt::Key_Backtab: e->ignore(); return; // let the completer do default behavior default: break; } } if (e->key() == Qt::Key_Backtab) { e->accept(); shiftLeft(); ensureCursorVisible(); } else if (e->key() == Qt::Key_Tab) { e->accept(); auto cursor = textCursor(); auto startBlock = document()->findBlock(cursor.selectionStart()); auto endBlock = document()->findBlock(cursor.selectionEnd()); auto partialLineSelection = startBlock.position() != cursor.selectionStart() || endBlock.position() + endBlock.length() - 1 != cursor.selectionEnd(); if (!cursor.hasSelection() || (startBlock == endBlock && partialLineSelection)) { if (useTabs) { cursor.insertText("\t"); } else { auto posInLine = cursor.selectionStart() - startBlock.position(); auto distanceFromTabStop = 0; auto line = startBlock.text(); for (int i = 0; i < posInLine; i++) { if (line[i] == '\t') { distanceFromTabStop = 0; } else if (line[i].isSpace()) { distanceFromTabStop++; } else { break; } } auto toAdd = distanceFromTabStop % indentSize; if (toAdd == 0) { toAdd = indentSize; } cursor.insertText(QString(" ").repeated(toAdd)); } } else { shiftRight(); } ensureCursorVisible(); } else if (e->key() == Qt::Key_Return) { e->accept(); QTextCursor cursor(textCursor()); QString curLine = cursor.block().text(); QRegularExpression leadingWhitespace("^(\\s*)"); cursor.insertText("\n"); auto leadingWhitespace_match = leadingWhitespace.match(curLine); if (leadingWhitespace_match.hasMatch()) { cursor.insertText(leadingWhitespace_match.captured(1)); } ensureCursorVisible(); } else if (e->key() == Qt::Key_Escape) { e->accept(); emit escPressed(); } else { bool isShortcut = ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_E); // CTRL+E if (!isShortcut) // do not process the shortcut when we have a completer QPlainTextEdit::keyPressEvent(e); const bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier); if (ctrlOrShift && e->text().isEmpty()) return; static QString eow("~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-="); // end of word bool hasModifier = (e->modifiers() != Qt::NoModifier) && !ctrlOrShift; QTextCursor tc = textCursor(); tc.select(QTextCursor::WordUnderCursor); QString completionPrefix = tc.selectedText(); if (!isShortcut && (hasModifier || e->text().isEmpty()|| completionPrefix.length() < 3 || eow.contains(e->text().right(1)))) { completer->popup()->hide(); return; } if (completionPrefix != completer->completionPrefix()) { completer->setCompletionPrefix(completionPrefix); completer->popup()->setCurrentIndex(completer->completionModel()->index(0, 0)); } QRect cr = cursorRect(); cr.setWidth(completer->popup()->sizeHintForColumn(0) + completer->popup()->verticalScrollBar()->sizeHint().width()); completer->complete(cr); // popup it up! } } int CodeEditor::lineNumbersWidth() { int width = 1; int bc = blockCount(); while (bc >= 10) { bc /= 10; ++width; } width = std::max(width,3); return 3 + fontMetrics().boundingRect(QLatin1Char('9')).width() * width; } int CodeEditor::debugInfoWidth() { return !debugInfo->isVisible()?0:(3*DEBUG_TAB_SIZE); } int CodeEditor::debugInfoOffset() { int heightOffset = 0; if(debugInfo->isVisible()){ QFont lineNoFont = font(); QFontMetrics fm(lineNoFont); heightOffset = fm.height(); } return heightOffset; } void CodeEditor::showDebugInfo(bool show) { if (filepath!="" && !filepath.endsWith(".mzn")) show = false; if (show) { if(debugInfo->isHidden()){ debugInfo->show(); setViewportWidth(0); } } else { if(!debugInfo->isHidden()){ debugInfo->hide(); setViewportWidth(0); } } } void CodeEditor::setViewportWidth(int) { setViewportMargins(lineNumbersWidth(), debugInfoOffset(), debugInfoWidth(), 0); } void CodeEditor::setLineNumbers(const QRect &rect, int dy) { if (dy) lineNumbers->scroll(0, dy); else lineNumbers->update(0, rect.y(), lineNumbers->width(), rect.height()); if (rect.contains(viewport()->rect())) setViewportWidth(0); } void CodeEditor::setDebugInfoPos(const QRect &rect, int dy) { if (dy) debugInfo->scroll(0, dy); else debugInfo->update(0, rect.y(), debugInfo->width(), rect.height()); if (rect.contains(viewport()->rect())) setViewportWidth(0); } void CodeEditor::resizeEvent(QResizeEvent *e) { QPlainTextEdit::resizeEvent(e); QRect cr = contentsRect(); lineNumbers->setGeometry(QRect(cr.left(), cr.top()+debugInfoOffset(), lineNumbersWidth(), cr.height())); if (loadContentsButton) { loadContentsButton->move(cr.left()+lineNumbersWidth(), cr.top()); } debugInfo->setGeometry(QRect(cr.right()-debugInfoWidth(), cr.top()+debugInfoOffset(), debugInfoWidth(), cr.height())); editorHeader->setGeometry(QRect(cr.left(), cr.top(), cr.width(), debugInfoOffset())); } void CodeEditor::showEvent(QShowEvent *event) { setViewportWidth(0); } void CodeEditor::cursorChange() { QList allExtraSels = extraSelections(); BracketData* bd = static_cast(textCursor().block().userData()); QList extraSelections; { QTextEdit::ExtraSelection highlightLineSelection; QColor lineColor = theme.lineHighlightColor.get(darkMode); highlightLineSelection.format.setBackground(lineColor); highlightLineSelection.format.setProperty(QTextFormat::FullWidthSelection, true); highlightLineSelection.cursor = textCursor(); highlightLineSelection.cursor.clearSelection(); extraSelections.append(highlightLineSelection); } auto ec = theme.errorColor.get(darkMode); auto wc = theme.warningColor.get(darkMode); foreach (QTextEdit::ExtraSelection sel, allExtraSels) { if (sel.format.underlineColor() == ec || sel.format.underlineColor() == wc) { extraSelections.append(sel); } } if (bd) { QVector& brackets = bd->brackets; int pos = textCursor().block().position(); for (int i=0; i(block.userData()); QVector& brackets = bd->brackets; int docPos = block.position(); for (; i(block.userData()); QVector& brackets = bd->brackets; if (i==-1) i = brackets.size()-1; int docPos = block.position(); for (; i>-1 && brackets.size()>0; i--) { Bracket& b = brackets[i]; if (b.b==')' || b.b=='}' || b.b==']') { nRight++; } else if (b.b==match && nRight==0) { return docPos+b.pos; } else { nRight--; } } block = block.previous(); i = -1; } return -1; } void CodeEditor::paintLineNumbers(QPaintEvent *event) { QPainter painter(lineNumbers); QFont lineNoFont = font(); QFontMetrics fm(lineNoFont); lineNoFont.setPointSizeF(lineNoFont.pointSizeF()*0.8); QFontMetrics fm2(lineNoFont); int ascentDiff = fontMetrics().ascent() - fm2.ascent(); painter.setFont(lineNoFont); painter.fillRect(event->rect(), theme.lineNumberbackground.get(darkMode)); QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = static_cast(blockBoundingGeometry(block).translated(contentOffset()).top()); int bottom = top + static_cast(blockBoundingRect(block).height()); // painter.fillRect(event->rect(), QColor::fromRgb(QRandomGenerator::global()->generate())); int curLine = textCursor().blockNumber(); while (block.isValid() && top <= event->rect().bottom()) { if (block.isVisible() && bottom >= event->rect().top()) { QString number = QString::number(blockNumber + 1); int textTop = top + ascentDiff; if (errorLines.contains(blockNumber)) { painter.setPen(theme.errorColor.get(darkMode)); } else if (warningLines.contains(blockNumber)) { painter.setPen(theme.warningColor.get(darkMode)); } else if (blockNumber == curLine) { painter.setPen(theme.foregroundActiveColor.get(darkMode)); } else { painter.setPen(theme.foregroundInactiveColor.get(darkMode)); } painter.drawText(0, textTop, lineNumbers->width(), fm2.height(), Qt::AlignRight, number); } block = block.next(); top = bottom; bottom = top + static_cast(blockBoundingRect(block).height()); ++blockNumber; } } QColor CodeEditor::interpolate(QColor start,QColor end,double ratio) { //From https://stackoverflow.com/questions/3306786/get-intermediate-color-from-a-gradient int r = static_cast(ratio*start.red() + (1-ratio)*end.red()); int g = static_cast(ratio*start.green() + (1-ratio)*end.green()); int b = static_cast(ratio*start.blue() + (1-ratio)*end.blue()); int a = static_cast(ratio*start.alpha() + (1-ratio)*end.alpha()); QColor c = QColor::fromRgb(r,g,b); c.setAlpha(a); // Strangely there seems to be no clean way of creating a rgba color, fromRgba does not take 4 parameters as one would expect. return c; } QColor CodeEditor::heatColor(double ratio) { //return interpolate(darkMode?QColor(191, 0, 0):QColor(Qt::red).lighter(110), QColor(Qt::transparent), ratio); // QColor bg = darkMode?QColor(0x26, 0x26, 0x26):Qt::white; QColor bg = theme.backgroundColor.get(darkMode); bg.setAlpha(50); return interpolate(darkMode?QColor(191, 0, 0):QColor(Qt::red).lighter(110), bg, ratio); } void CodeEditor::paintDebugInfo(QPaintEvent *event) { QPainter painter(debugInfo); QFont lineNoFont = font(); QFontMetrics fm(lineNoFont); int origFontHeight = fm.height(); lineNoFont.setPointSizeF(lineNoFont.pointSizeF()*0.8); QFontMetrics fm2(lineNoFont); int heightDiff = (origFontHeight-fm2.height()); painter.setFont(lineNoFont); painter.fillRect(event->rect(), theme.backgroundColor.get(darkMode)); // TODO: This should be pre-computed only once and stored in each block. // Statistics should not be recounted at eack redraw... QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = static_cast(blockBoundingGeometry(block).translated(contentOffset()).top()); int bottom = top + static_cast(blockBoundingRect(block).height()); int curLine = textCursor().blockNumber(); painter.setPen(theme.foregroundActiveColor.get(darkMode)); while (block.isValid() && top <= event->rect().bottom()) { BracketData* bd = static_cast(block.userData()); if (block.isVisible() && bottom >= event->rect().top() && bd->d.hasData()) { // int textTop = top+fontMetrics().leading()+heightDiff; int textTop = top+heightDiff; // num constraints painter.fillRect(0, top, DEBUG_TAB_SIZE, static_cast(blockBoundingRect(block).height()), heatColor(static_cast(bd->d.con)/bd->d.totalCon)); QString numConstraints = QString().number(bd->d.con); painter.drawText(0, textTop, DEBUG_TAB_SIZE, fm2.height(), Qt::AlignCenter, numConstraints); // num vars painter.fillRect(DEBUG_TAB_SIZE, top, DEBUG_TAB_SIZE, static_cast(blockBoundingRect(block).height()), heatColor(static_cast(bd->d.var)/bd->d.totalVar)); QString numVars = QString().number(bd->d.var); painter.drawText(DEBUG_TAB_SIZE, textTop, DEBUG_TAB_SIZE, fm2.height(), Qt::AlignCenter, numVars); // flatten time painter.fillRect(DEBUG_TAB_SIZE*2, top, DEBUG_TAB_SIZE, static_cast(blockBoundingRect(block).height()), heatColor(static_cast(bd->d.ms)/bd->d.totalMs)); QString flattenTime = QString().number(bd->d.ms); painter.drawText(DEBUG_TAB_SIZE*2, textTop, DEBUG_TAB_SIZE, fm2.height(), Qt::AlignCenter, flattenTime+"ms"); // painter.drawText(0, textTop, debugInfo->width(), fm2.height(), // Qt::AlignLeft, number); } block = block.next(); top = bottom; bottom = top + static_cast(blockBoundingRect(block).height()); ++blockNumber; } painter.setPen(theme.foregroundInactiveColor.get(darkMode)); painter.drawLine(0,0,0,event->rect().bottom()); } void CodeEditor::paintHeader(QPaintEvent *event) { QPainter painter(editorHeader); QFont lineNoFont = font(); QFontMetrics fm(lineNoFont); int origFontHeight = fm.height(); lineNoFont.setPointSizeF(lineNoFont.pointSizeF()*0.8); QFontMetrics fm2(lineNoFont); int heightDiff = (origFontHeight-fm2.height()); painter.setFont(lineNoFont); painter.fillRect(event->rect(), theme.backgroundColor.get(darkMode)); int baseX = debugInfo->geometry().x(); painter.setPen(theme.textColor.get(darkMode)); painter.drawText(baseX, heightDiff/2, DEBUG_TAB_SIZE, debugInfoOffset(), Qt::AlignCenter, "Cons"); painter.drawText(baseX + DEBUG_TAB_SIZE, heightDiff/2, DEBUG_TAB_SIZE, debugInfoOffset(), Qt::AlignCenter, "Vars"); painter.drawText(baseX + DEBUG_TAB_SIZE*2, heightDiff/2, DEBUG_TAB_SIZE, debugInfoOffset(), Qt::AlignCenter, "Time"); } void CodeEditor::setEditorFont(QFont& font) { setFont(font); document()->setDefaultFont(font); highlighter->setEditorFont(font); setIndentSize(indentSize); highlighter->rehighlight(); } bool CodeEditor::eventFilter(QObject *, QEvent *ev) { if (ev->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(ev); if (keyEvent == QKeySequence::Copy) { copy(); return true; } else if (keyEvent == QKeySequence::Cut) { cut(); return true; } } else if (ev->type() == QEvent::ToolTip) { QHelpEvent* helpEvent = static_cast(ev); QPoint evPos = helpEvent->pos(); evPos = QPoint(evPos.x()-lineNumbersWidth(),evPos.y()); if (evPos.x() >= 0) { QTextCursor cursor = cursorForPosition(evPos); bool foundError = false; foreach (CodeEditorError cee, errors) { if (cursor.position() >= cee.startPos && cursor.position() <= cee.endPos) { QToolTip::showText(helpEvent->globalPos(), cee.msg); foundError = true; break; } } if (!foundError) { cursor.select(QTextCursor::WordUnderCursor); QString loc(filename+":"); loc += QString().number(cursor.block().blockNumber()+1); loc += "."; loc += QString().number(cursor.selectionStart()-cursor.block().position()+1); QHash::iterator idMapIt = idMap.find(loc); if (idMapIt != idMap.end()) { QToolTip::showText(helpEvent->globalPos(), idMapIt.value()); } else { QToolTip::hideText(); } } } else { QToolTip::hideText(); } return true; } return false; } void CodeEditor::copy() { highlighter->copyHighlightedToClipboard(textCursor()); } void CodeEditor::cut() { highlighter->copyHighlightedToClipboard(textCursor()); textCursor().removeSelectedText(); } void CodeEditor::checkFile(const QVector& mznErrors) { auto errorColor = theme.errorColor.get(darkMode); auto warningColor = theme.warningColor.get(darkMode); QList allExtraSels = extraSelections(); QList extraSelections; foreach (QTextEdit::ExtraSelection sel, allExtraSels) { if (sel.format.underlineColor() != errorColor && sel.format.underlineColor() != warningColor) { extraSelections.append(sel); } } errors.clear(); errorLines.clear(); warningLines.clear(); for (auto& it : mznErrors) { QTextEdit::ExtraSelection sel; QTextCharFormat format = sel.format; format.setUnderlineStyle(QTextCharFormat::WaveUnderline); format.setUnderlineColor(it.isWarning ? warningColor : errorColor); QTextBlock block = document()->findBlockByNumber(it.first_line-1); QTextBlock endblock = document()->findBlockByNumber(it.last_line-1); if (block.isValid() && endblock.isValid()) { QTextCursor cursor = textCursor(); cursor.setPosition(block.position()); int firstCol = it.first_col < it.last_col ? (it.first_col-1) : (it.last_col-1); int lastCol = it.first_col < it.last_col ? (it.last_col) : (it.first_col); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, firstCol); int startPos = cursor.position(); cursor.setPosition(endblock.position(), QTextCursor::KeepAnchor); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, lastCol); int endPos = cursor.position(); sel.cursor = cursor; sel.format = format; extraSelections.append(sel); CodeEditorError cee(startPos, endPos, QString(it.isWarning ? "Warning: %1" : "Error: %1").arg(it.msg)); errors.append(cee); for (int j=it.first_line; j<=it.last_line; j++) { if (it.isWarning) { warningLines.insert(j - 1); } else { errorLines.insert(j - 1); } } } } setExtraSelections(extraSelections); } void CodeEditor::shiftLeft() { shiftSelected(-1); } void CodeEditor::shiftRight() { shiftSelected(1); } void CodeEditor::shiftSelected(int amount) { auto origCursor = textCursor(); origCursor.setKeepPositionOnInsert(true); QTextCursor cursor = textCursor(); QTextBlock block = document()->findBlock(cursor.selectionStart()); QTextBlock endblock = document()->findBlock(cursor.selectionEnd()); bool atBlockStart = cursor.selectionEnd() == endblock.position(); if (block==endblock || !atBlockStart) endblock = endblock.next(); cursor.beginEditBlock(); do { auto position = block.position(); cursor.setPosition(position); auto indentation = 0; for (auto c : block.text()) { if (c == '\t') { indentation = indentSize * ((indentation + indentSize) / indentSize); } else if (c.isSpace()) { indentation++; } else { break; } position++; } cursor.setPosition(position, QTextCursor::KeepAnchor); auto newIndent = std::max(0, indentation + indentSize * amount); if (useTabs) { cursor.insertText( QString("\t").repeated(newIndent / indentSize) + QString(" ").repeated(newIndent % indentSize)); } else { cursor.insertText(QString(" ").repeated(newIndent)); } block = block.next(); } while (block.isValid() && block != endblock); cursor.endEditBlock(); origCursor.setKeepPositionOnInsert(false); setTextCursor(origCursor); } ================================================ FILE: MiniZincIDE/codeeditor.h ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef CODEEDITOR_H #define CODEEDITOR_H #include #include #include #include #include #include "highlighter.h" #include "theme.h" class CodeEditorError { public: int startPos; int endPos; QString msg; CodeEditorError(int startPos0, int endPos0, const QString& msg0) : startPos(startPos0), endPos(endPos0), msg(msg0) {} }; struct MiniZincError { bool isWarning; QString filename; int first_line; int last_line; int first_col; int last_col; QString msg; }; class EditorHeader; class CodeEditor : public QPlainTextEdit { Q_OBJECT public: explicit CodeEditor(QTextDocument* doc, const QString& path, bool isNewFile, bool large, QFont& font, int indentSize, bool useTabs, const Theme& theme, bool darkMode, QTabWidget* tabs, QWidget *parent); void paintLineNumbers(QPaintEvent *event); int lineNumbersWidth(); void paintDebugInfo(QPaintEvent *event); void paintHeader(QPaintEvent *event); int debugInfoWidth(); int debugInfoOffset(); QString filepath; QString filename; QString playgroundTempFile; void setEditorFont(QFont& font); void setDocument(QTextDocument *document); void setTheme(const Theme& t, bool d); void setIndentSize(int size); void setIndentTab(bool useTabs0) { useTabs = useTabs0; } Highlighter& getHighlighter(); void checkFile(const QVector& errors); void showDebugInfo(bool show); const static int DEBUG_TAB_SIZE = 70; void shiftLeft(); void shiftRight(); protected: void resizeEvent(QResizeEvent *event); void showEvent(QShowEvent *event); void initUI(QFont& font); virtual void keyPressEvent(QKeyEvent *e); bool eventFilter(QObject *, QEvent *); private slots: void setViewportWidth(int newBlockCount); void cursorChange(); void setLineNumbers(const QRect &, int); void setDebugInfoPos(const QRect &, int); void docChanged(bool); void contentsChanged(); void contentsChangedWithTimeout(); void loadContents(); void insertCompletion(const QString& completion); private: QWidget* lineNumbers; QWidget* debugInfo; EditorHeader* editorHeader; QWidget* loadContentsButton; QTabWidget* tabs; Highlighter* highlighter; QCompleter* completer; QStringListModel completionModel; int indentSize; bool useTabs; QList errors; QSet errorLines; QSet warningLines; QHash idMap; QTimer modificationTimer; Theme theme; bool darkMode; int matchLeft(QTextBlock block, QChar b, int i, int n); int matchRight(QTextBlock block, QChar b, int i, int n); void shiftSelected(int amount); QColor interpolate(QColor start,QColor end,double ratio); // This should not go here QColor heatColor(double ratio); // This should not go here signals: void escPressed(); void changedDebounced(); public slots: void loadedLargeFile(); void copy(); void cut(); }; class LineNumbers: public QWidget { public: LineNumbers(CodeEditor *e) : QWidget(e), codeEditor(e) {} QSize sizeHint() const { return QSize(codeEditor->lineNumbersWidth(), 0); } protected: void paintEvent(QPaintEvent *event) { codeEditor->paintLineNumbers(event); } private: CodeEditor *codeEditor; }; class DebugInfo: public QWidget { public: DebugInfo(CodeEditor *e) : QWidget(e), codeEditor(e) {} QSize sizeHint() const { return QSize(codeEditor->debugInfoWidth(), 0); } protected: void paintEvent(QPaintEvent *event) { codeEditor->paintDebugInfo(event); } private: CodeEditor *codeEditor; }; class EditorHeader: public QWidget { Q_OBJECT public: EditorHeader(CodeEditor *e) : QWidget(e), codeEditor(e) { setMouseTracking(true); } QSize sizeHint() const { return QSize(0, codeEditor->debugInfoOffset()); } protected: void paintEvent(QPaintEvent *event) { codeEditor->paintHeader(event); } private: CodeEditor *codeEditor; }; #endif // CODEEDITOR_H ================================================ FILE: MiniZincIDE/configwindow.cpp ================================================ /* * Main authors: * Jason Nguyen */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "configwindow.h" #include "ui_configwindow.h" #include #include #include #include #include #include #include #include #include "process.h" #include "exception.h" #include "ideutils.h" #include #include #include ConfigWindow::ConfigWindow(QWidget *parent) : QWidget(parent), ui(new Ui::ConfigWindow) { ui->setupUi(this); ui->userDefinedBehavior_frame->setVisible(false); extraParamDialog = new ExtraParamDialog; auto* widgetAction = new QWidgetAction(nullptr); widgetAction->setDefaultWidget(extraParamDialog); auto* menu = new QMenu(this); menu->addAction(widgetAction); ui->addExtraParam_toolButton->setMenu(menu); connect(extraParamDialog, &ExtraParamDialog::addParams, this, [=](const QList& flags) { for (auto& f : flags) { addExtraParam(f); extraParamDialog->setParamEnabled(f, false); } resizeExtraFlagsTable(); invalidate(false); menu->hide(); }); connect(extraParamDialog, &ExtraParamDialog::addCustomParam, this, [=]() { addExtraParam(); resizeExtraFlagsTable(); invalidate(false); menu->hide(); }); ui->extraParams_tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); ui->extraParams_tableWidget->setItemDelegateForColumn(3, new ExtraOptionDelegate); IDEUtils::watchChildChanges(ui->options_groupBox, this, std::bind(&ConfigWindow::invalidate, this, true)); IDEUtils::watchChildChanges(ui->advanced_groupBox, this, std::bind(&ConfigWindow::invalidate, this, false)); } ConfigWindow::~ConfigWindow() { delete ui; for (auto sc : configs) { delete sc; } } void ConfigWindow::init() { loadConfigs(); initialized = true; } void ConfigWindow::loadConfigs(void) { try { QStringList files; QStringList warnings; for (auto config : configs) { if (!config->paramFile.isEmpty()) { files.append(config->paramFile); } delete config; } configs.clear(); for (auto* solver : MznDriver::get().solvers()) { if (solver->hasAllRequiredFlags()) { configs.append(new SolverConfiguration(*solver, true)); } } if (!files.empty()) { for (auto fileName : files) { configs.append(new (SolverConfiguration) (SolverConfiguration::loadJSON(fileName, warnings))); } } populateComboBox(); updateGUI(true); if (!warnings.empty()) { QMessageBox::warning(this, "MiniZinc IDE", warnings.join("\n"), QMessageBox::Ok); } } catch (Exception& e) { QMessageBox::warning(this, "Parameter configuration error", e.message(), QMessageBox::Ok); } } bool ConfigWindow::addConfig(const QString &fileName) { int index = findConfigFile(fileName); if (index != -1) { // Already have this config setCurrentIndex(index); return true; } try { QStringList warnings; updateSolverConfig(configs[currentIndex()]); configs.append(new (SolverConfiguration) (SolverConfiguration::loadJSON(fileName, warnings))); for (auto sc : configs) { if (!sc->syncedOptionsMatch(*(configs.last()))) { populating = true; // Make sure this doesn't mark the config as modified ui->sync_checkBox->setChecked(false); populating = false; break; } } populateComboBox(); if (ui->sync_checkBox->isChecked()) { // Ensure that newly loaded config overrides synced options populating = true; // Make sure this doesn't mark the config as modified ui->sync_checkBox->setChecked(false); populating = false; setCurrentIndex(configs.length() - 1); populating = true; // Make sure this doesn't mark the config as modified ui->sync_checkBox->setChecked(true); populating = false; } if (!warnings.empty()) { QMessageBox::warning(this, "MiniZinc IDE", warnings.join("\n"), QMessageBox::Ok); } return true; } catch (Exception& e) { QMessageBox::warning(this, "Parameter configuration error", e.message(), QMessageBox::Ok); return false; } } void ConfigWindow::mergeConfigs(const QList confs) { for (auto sc : confs) { int swapAt = sc->isBuiltin ? findBuiltinConfig(sc->solverDefinition.id, sc->solverDefinition.version) : findConfigFile(sc->paramFile); sc->modified = true; if (swapAt == -1) { sc->isBuiltin = false; configs.append(sc); } else { delete configs[swapAt]; configs.replace(swapAt, sc); } } updateGUI(true); populateComboBox(); } int ConfigWindow::findBuiltinConfig(const QString &id, const QString &version) { int matchId = -1; int i = 0; for (auto sc : configs) { if (sc->isBuiltin && sc->solverDefinition.id == id) { matchId = i; if (sc->solverDefinition.version == version) { return i; } } i++; } return matchId; } int ConfigWindow::findConfigFile(const QString &file) { if (file.isEmpty()) { // Unsaved configuration return -1; } int i = 0; for (auto sc : configs) { if (sc->paramFile == file) { return i; } i++; } return -1; } void ConfigWindow::stashModifiedConfigs() { auto* selected = currentSolverConfig(); for (auto* sc : configs) { if (sc->modified) { if (sc == selected) { stashSelectedModifiedConfig = stash.length(); } stash.append(StashItem(sc)); } } if (selected != nullptr) { if (selected->isBuiltin) { stashSelectedBuiltinSolverId = selected->solverDefinition.id; stashSelectedBuiltinSolverVersion = selected->solverDefinition.version; } stashSelectedParamFile = selected->paramFile; } } void ConfigWindow::unstashModifiedConfigs() { lastIndex = -1; QList merge; for (auto& item : stash) { try { merge.append(item.consume()); } catch (...) { // Ignore } } mergeConfigs(merge); int i = -1; if (stashSelectedModifiedConfig >= 0 && stashSelectedModifiedConfig < merge.size()) { i = configs.indexOf(merge[stashSelectedModifiedConfig]); } else if (!stashSelectedParamFile.isEmpty()) { i = findConfigFile(stashSelectedParamFile); } else if (!stashSelectedBuiltinSolverId.isEmpty()) { i = findBuiltinConfig(stashSelectedBuiltinSolverId, stashSelectedBuiltinSolverVersion); } if (i != -1) { setCurrentIndex(i); } stash.clear(); stashSelectedBuiltinSolverId = ""; stashSelectedBuiltinSolverVersion = ""; stashSelectedParamFile = ""; stashSelectedModifiedConfig = -1; } QString ConfigWindow::saveConfig(int index) { auto sc = configs[index]; auto oldSc = sc; updateSolverConfig(configs[currentIndex()]); if (sc->isBuiltin) { // Must clone built-in configs sc = new SolverConfiguration(*sc); } QString target = sc->paramFile; if (sc->paramFile.isEmpty()) { QFileInfo fi(sc->paramFile); target = QFileDialog::getSaveFileName(this, "Save configuration", fi.canonicalFilePath(), "Solver configuration files (*.mpc)"); } if (target.isEmpty()) { return ""; } QFile f(target); f.open(QFile::WriteOnly | QFile::Truncate); f.write(sc->toJSON()); f.close(); oldSc->modified = false; sc->modified = false; sc->paramFile = target; populateComboBox(); emit configSaved(target); return target; } QString ConfigWindow::saveConfig() { return saveConfig(currentIndex()); } SolverConfiguration* ConfigWindow::currentSolverConfig(void) { int i = currentIndex(); if (i < 0 || i >= configs.length()) { return nullptr; } auto sc = configs[i]; updateSolverConfig(sc); return sc; } const QList& ConfigWindow::solverConfigs() { currentSolverConfig(); return configs; } int ConfigWindow::currentIndex(void) { return ui->config_comboBox->currentIndex(); } void ConfigWindow::setCurrentIndex(int i) { ui->config_comboBox->setCurrentIndex(qBound(0, i, configs.length())); } void ConfigWindow::on_numSolutions_checkBox_stateChanged(int arg1) { ui->numSolutions_spinBox->setEnabled(arg1); } void ConfigWindow::on_numOptimal_checkBox_stateChanged(int arg1) { ui->numOptimal_spinBox->setEnabled(arg1); } void ConfigWindow::on_timeLimit_checkBox_stateChanged(int arg1) { ui->timeLimit_doubleSpinBox->setEnabled(arg1); } void ConfigWindow::on_randomSeed_checkBox_stateChanged(int arg1) { ui->randomSeed_lineEdit->setEnabled(arg1); } void ConfigWindow::on_defaultBehavior_radioButton_toggled(bool checked) { ui->defaultBehavior_frame->setVisible(checked); } void ConfigWindow::on_userDefinedBehavior_radioButton_toggled(bool checked) { ui->userDefinedBehavior_frame->setVisible(checked); } void ConfigWindow::on_config_comboBox_currentIndexChanged(int index) { if (!initialized) { return; } if (lastIndex >= 0) { updateSolverConfig(configs[lastIndex]); } lastIndex = index; updateGUI(); if (index >= 0 && index < configs.length()) { emit selectedIndexChanged(index); emit selectedSolverConfigurationChanged(configs[index]); } } void ConfigWindow::updateGUI(bool overrideSync) { int index = currentIndex(); if (configs.isEmpty()) { ui->solver_controls->setVisible(false); ui->options_groupBox->setVisible(false); ui->advanced_groupBox->setVisible(false); return; } auto sc = configs[index]; ui->solver_controls->setVisible(true); ui->options_groupBox->setVisible(true); ui->advanced_groupBox->setVisible(true); populating = true; ui->solver_label->setText(sc->solverDefinition.name + " " + sc->solverDefinition.version); ui->makeConfigDefault_pushButton->setEnabled(sc->isBuiltin && !sc->solverDefinition.isDefaultSolver); ui->reset_pushButton->setEnabled(sc->isBuiltin); ui->intermediateSolutions_checkBox->setEnabled(sc->supports("-a") || sc->supports("-i")); ui->numSolutions_checkBox->setEnabled(sc->supports("-a")); ui->numOptimal_checkBox->setEnabled(sc->supports("-a-o")); ui->verboseSolving_checkBox->setEnabled(sc->supports("-v")); ui->solvingStats_checkBox->setEnabled(sc->supports("-s")); if (overrideSync || !ui->sync_checkBox->isChecked()) { ui->timeLimit_checkBox->setChecked(sc->timeLimit != 0); ui->timeLimit_doubleSpinBox->setValue(sc->timeLimit / 1000.0); ui->timeLimit_doubleSpinBox->setEnabled(ui->timeLimit_checkBox->isChecked()); if (sc->printIntermediate && sc->numSolutions == 1) { ui->defaultBehavior_radioButton->setChecked(true); } else { ui->userDefinedBehavior_radioButton->setChecked(true); } ui->intermediateSolutions_checkBox->setChecked(sc->printIntermediate); ui->numSolutions_spinBox->setValue(sc->numSolutions); ui->numSolutions_checkBox->setChecked(sc->numSolutions != 0); ui->numOptimal_spinBox->setValue(sc->numOptimal); ui->numOptimal_checkBox->setChecked(sc->numOptimal != 0); ui->verboseCompilation_checkBox->setChecked(sc->verboseCompilation); if (ui->verboseSolving_checkBox->isEnabled()) { ui->verboseSolving_checkBox->setChecked(sc->verboseSolving); } ui->compilationStats_checkBox->setChecked(sc->compilationStats); if (ui->solvingStats_checkBox->isEnabled()) { ui->solvingStats_checkBox->setChecked(sc->solvingStats); } ui->timingInfo_checkBox->setChecked(sc->outputTiming); ui->outputObjective_checkBox->setChecked(sc->outputObjective); } ui->outputObjective_checkBox->setEnabled(sc->solverDefinition.inputType != Solver::I_MZN || sc->supports("--output-objective")); ui->numSolutions_spinBox->setEnabled(ui->numSolutions_checkBox->isEnabled() && ui->numSolutions_checkBox->isChecked()); ui->numOptimal_spinBox->setEnabled(ui->numOptimal_checkBox->isEnabled() && ui->numOptimal_checkBox->isChecked()); ui->optimizationLevel_comboBox->setCurrentIndex(sc->optimizationLevel); ui->additionalData_plainTextEdit->setPlainText(sc->additionalData.join("\n")); ui->numThreads_spinBox->setEnabled(sc->supports("-p")); ui->numThreads_spinBox->setValue(sc->numThreads); bool enableRandomSeed = sc->supports("-r"); ui->randomSeed_lineEdit->setText(sc->randomSeed.toString()); ui->randomSeed_checkBox->setChecked(!sc->randomSeed.isNull()); ui->randomSeed_checkBox->setEnabled(enableRandomSeed); ui->randomSeed_lineEdit->setEnabled(enableRandomSeed && !sc->randomSeed.isNull()); bool enableFreeSearch = sc->supports("-f"); ui->freeSearch_checkBox->setEnabled(enableFreeSearch); ui->freeSearch_checkBox->setChecked(sc->freeSearch); extraParamDialog->setParams(sc->solverDefinition.extraFlags); while (ui->extraParams_tableWidget->rowCount() > 0) { ui->extraParams_tableWidget->removeRow(0); } for (auto it = sc->extraOptions.begin(); it != sc->extraOptions.end(); it++) { bool matched = false; for (auto& f : sc->solverDefinition.extraFlags) { if (f.name == it.key()) { extraParamDialog->setParamEnabled(f, false); addExtraParam(f, it.value()); matched = true; break; } } if (!matched) { addExtraParam(it.key(), false, it.value()); } } for (auto it = sc->solverBackendOptions.begin(); it != sc->solverBackendOptions.end(); it++) { addExtraParam(it.key(), true, it.value()); } resizeExtraFlagsTable(); populating = false; } void ConfigWindow::updateSolverConfig(SolverConfiguration* sc) { // Make sure if anything was being edited, we defocus to update the value auto* focus = focusWidget(); if (focus && focus->hasFocus()) { focus->clearFocus(); focus->setFocus(); } if (!sc->modified) { return; } auto cfgs = ui->sync_checkBox->isChecked() ? configs : (QList() << sc); for (auto s : cfgs) { s->timeLimit = ui->timeLimit_checkBox->isChecked() ? static_cast(ui->timeLimit_doubleSpinBox->value() * 1000.0) : 0; if (ui->defaultBehavior_radioButton->isChecked()) { s->printIntermediate = true; s->numSolutions = 1; s->numOptimal = 1; } else { s->printIntermediate = ui->intermediateSolutions_checkBox->isChecked(); s->numSolutions = ui->numSolutions_checkBox->isChecked() ? ui->numSolutions_spinBox->value() : 0; s->numOptimal = ui->numOptimal_checkBox->isChecked() ? ui->numOptimal_spinBox->value() : 0; } s->verboseCompilation = ui->verboseCompilation_checkBox->isChecked(); s->verboseSolving = ui->verboseSolving_checkBox->isChecked(); s->compilationStats = ui->compilationStats_checkBox->isChecked(); s->solvingStats = ui->solvingStats_checkBox->isChecked(); s->outputTiming = ui->timingInfo_checkBox->isChecked(); s->outputObjective = ui->outputObjective_checkBox->isChecked(); } sc->optimizationLevel = ui->optimizationLevel_comboBox->currentIndex(); QString additionalData = ui->additionalData_plainTextEdit->toPlainText(); sc->additionalData = additionalData.length() > 0 ? additionalData.split("\n") : QStringList(); sc->numThreads = ui->numThreads_spinBox->value(); sc->randomSeed = ui->randomSeed_checkBox->isChecked() ? ui->randomSeed_lineEdit->text() : QVariant(); sc->freeSearch = ui->freeSearch_checkBox->isChecked(); sc->extraOptions.clear(); for (int row = 0; row < ui->extraParams_tableWidget->rowCount(); row++) { auto keyItem = ui->extraParams_tableWidget->item(row, 0); auto flagTypeWidget = static_cast(ui->extraParams_tableWidget->cellWidget(row, 1)); auto valueItem = ui->extraParams_tableWidget->item(row, 3); if (keyItem && valueItem) { auto key = keyItem->data(Qt::UserRole).isNull() ? keyItem->data(Qt::DisplayRole).toString() : qvariant_cast(keyItem->data(Qt::UserRole)).name; auto value = valueItem->data(Qt::DisplayRole); if (flagTypeWidget && flagTypeWidget->currentIndex() == 1) { sc->solverBackendOptions.insert(key, value); } else { sc->extraOptions.insert(key, value); } } } } void ConfigWindow::on_removeExtraParam_toolButton_clicked() { QVector toBeRemoved; for (auto& range : ui->extraParams_tableWidget->selectedRanges()) { for (int i = range.topRow(); i <= range.bottomRow(); i++) { toBeRemoved.append(i); } } std::sort(toBeRemoved.begin(), toBeRemoved.end(), std::greater()); for (auto i : toBeRemoved) { auto data = ui->extraParams_tableWidget->item(i, 0)->data(Qt::UserRole); if (!data.isNull()) { // Re-enable adding of this flag extraParamDialog->setParamEnabled(qvariant_cast(data), true); } ui->extraParams_tableWidget->removeRow(i); } if (!toBeRemoved.isEmpty()) { resizeExtraFlagsTable(); invalidate(false); } } void ConfigWindow::on_extraParams_tableWidget_itemSelectionChanged() { bool hasSelection = ui->extraParams_tableWidget->selectedRanges().length(); ui->removeExtraParam_toolButton->setEnabled(hasSelection); } void ConfigWindow::addExtraParam(const SolverFlag& f, const QVariant& value) { int i = ui->extraParams_tableWidget->rowCount(); ui->extraParams_tableWidget->insertRow(i); auto keyItem = new QTableWidgetItem(f.description); keyItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); keyItem->setData(Qt::UserRole, QVariant::fromValue(f)); keyItem->setToolTip(f.name); auto flagTypeItem = new QTableWidgetItem; flagTypeItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); auto typeItem = new QTableWidgetItem; typeItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); auto valueItem = new QTableWidgetItem; valueItem->setData(Qt::UserRole, QVariant::fromValue(f)); if (value.isNull()) { valueItem->setData(Qt::DisplayRole, f.def); } else { valueItem->setData(Qt::DisplayRole, value); } if (f.t == SolverFlag::T_BOOL && f.def.toBool()) { // Non switched booleans which are default true cannot be turned off // TODO: Maybe this should send the flag when different to default? valueItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); } ui->extraParams_tableWidget->setItem(i, 0, keyItem); ui->extraParams_tableWidget->setItem(i, 1, flagTypeItem); ui->extraParams_tableWidget->setItem(i, 2, typeItem); ui->extraParams_tableWidget->setItem(i, 3, valueItem); invalidate(false); } void ConfigWindow::addExtraParam(const QString& key, bool backend, const QVariant& value) { int i = ui->extraParams_tableWidget->rowCount(); ui->extraParams_tableWidget->insertRow(i); auto flag = key.startsWith("--") ? key.right(key.length() - 2) : key; auto keyItem = new QTableWidgetItem(flag); auto valItem = new QTableWidgetItem; valItem->setData(Qt::DisplayRole, value); ui->extraParams_tableWidget->setItem(i, 0, keyItem); ui->extraParams_tableWidget->setItem(i, 3, valItem); auto flagTypeComboBox = new QComboBox; flagTypeComboBox->addItems({"MiniZinc", "Solver backend"}); flagTypeComboBox->setCurrentIndex(backend ? 1 : 0); flagTypeComboBox->setToolTip("Controls whether the flag is sent to the underlying solver executable (if available), or to the minizinc command."); ui->extraParams_tableWidget->setCellWidget(i, 1, flagTypeComboBox); auto typeComboBox = new QComboBox; typeComboBox->setToolTip("Controls the data type of the flag value"); typeComboBox->addItems({"String", "Boolean", "Integer", "Float"}); switch (value.type()) { case QVariant::String: typeComboBox->setCurrentIndex(0); break; case QVariant::Bool: typeComboBox->setCurrentIndex(1); break; case QVariant::Int: typeComboBox->setCurrentIndex(2); break; case QVariant::Double: typeComboBox->setCurrentIndex(3); break; default: typeComboBox->setCurrentIndex(0); break; } connect(typeComboBox, QOverload::of(&QComboBox::currentIndexChanged), [=] (int typeIndex) mutable { QVariant::Type types[] = {QVariant::String, QVariant::Bool, QVariant::Int, QVariant::Double}; auto target = types[typeIndex]; auto data = valItem->data(Qt::DisplayRole); data.convert(target); auto newValItem = new QTableWidgetItem; newValItem->setData(Qt::DisplayRole, data); ui->extraParams_tableWidget->setItem(valItem->row(), 3, newValItem); valItem = newValItem; }); ui->extraParams_tableWidget->setCellWidget(i, 2, typeComboBox); } void ConfigWindow::invalidate(bool all) { if (populating) { // Ignore changes while populating the configuration widgets return; } bool refreshComboBox = false; int i = currentIndex(); if (i >= 0 && i < configs.length() && !configs[i]->modified) { configs[i]->modified = true; refreshComboBox = true; } if (all && ui->sync_checkBox->isChecked()) { for (auto sc : configs) { // Only non-builtin configs really need to be saved after modifying basic options if (!sc->isBuiltin && !sc->modified) { sc->modified = true; refreshComboBox = true; } } } if (refreshComboBox) { populateComboBox(); } } void ConfigWindow::populateComboBox() { bool oldInitialized = initialized; int oldIndex = currentIndex(); QStringList items; for (auto sc : configs) { items << sc->name(); } initialized = false; ui->config_comboBox->clear(); ui->config_comboBox->addItems(items); if (oldIndex >= 0 && oldIndex < configs.length()) { setCurrentIndex(oldIndex); } else { // Index no longer valid, just go back to default solver int i = 0; for (auto config : configs) { if (config->solverDefinition.isDefaultSolver) { setCurrentIndex(i); break; } i++; } } emit itemsChanged(items); initialized = oldInitialized; } void ConfigWindow::removeConfig(int i) { bool targetIndex = i < currentIndex() ? currentIndex() - 1 : currentIndex(); delete configs[i]; configs.removeAt(i); populateComboBox(); setCurrentIndex(targetIndex); } void ConfigWindow::on_clone_pushButton_clicked() { auto sc = configs[currentIndex()]; updateSolverConfig(sc); auto clone = new SolverConfiguration(*sc); clone->paramFile = ""; clone->isBuiltin = false; clone->modified = true; configs << clone; populateComboBox(); setCurrentIndex(configs.length() - 1); } void ConfigWindow::resizeExtraFlagsTable() { auto* hScroll = ui->extraParams_tableWidget->horizontalScrollBar(); // To avoid creating a vertical scrollbar if there's a horizontal one int padding = hScroll ? hScroll->height() : 0; int total_height = ui->extraParams_tableWidget->horizontalHeader()->height() + padding; if (!ui->extraParams_tableWidget->horizontalScrollBar()->isHidden()) { total_height += ui->extraParams_tableWidget->horizontalScrollBar()->height(); } for (int row = 0; row < ui->extraParams_tableWidget->rowCount(); row++) { total_height += ui->extraParams_tableWidget->rowHeight(row); } ui->extraParams_tableWidget->setMinimumHeight(total_height); } QWidget* ExtraOptionDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { if (index.data(Qt::UserRole).canConvert()) { auto f = qvariant_cast(index.data(Qt::UserRole)); switch (f.t) { case SolverFlag::T_INT: { auto field = new QLineEdit(parent); field->setAlignment(Qt::AlignRight); field->setValidator(new LongLongValidator()); return field; } case SolverFlag::T_INT_RANGE: { if (f.min_ll < std::numeric_limits::min() || f.max_ll > std::numeric_limits::max()) { auto field = new QLineEdit(parent); field->setAlignment(Qt::AlignRight); field->setValidator(new LongLongValidator(f.min_ll, f.max_ll)); return field; } else { auto field = new QSpinBox(parent); field->setRange(static_cast(f.min_ll), static_cast(f.max_ll)); return field; } } case SolverFlag::T_FLOAT: { auto field = new QLineEdit(parent); field->setAlignment(Qt::AlignRight); field->setValidator(new QDoubleValidator()); return field; } case SolverFlag::T_FLOAT_RANGE: { auto field = new QDoubleSpinBox(parent); field->setDecimals(DBL_MAX_10_EXP + DBL_DIG); field->setRange(f.min, f.max); return field; } case SolverFlag::T_OPT: { auto field = new QComboBox(parent); field->addItems(f.options); return field; } case SolverFlag::T_SOLVER: { auto field = new QComboBox(parent); for (auto* solver : MznDriver::get().solvers()) { field->addItem(solver->name); } return field; } case SolverFlag::T_BOOL: case SolverFlag::T_BOOL_ONOFF: case SolverFlag::T_STRING: return QStyledItemDelegate::createEditor(parent, option, index); } } else if (index.data().type() == QVariant::Double) { auto field = new QLineEdit(parent); field->setAlignment(Qt::AlignRight); field->setValidator(new QDoubleValidator()); return field; } else { return QStyledItemDelegate::createEditor(parent, option, index); } } void ExtraOptionDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { if (index.data(Qt::UserRole).canConvert()) { auto f = qvariant_cast(index.data(Qt::UserRole)); switch (f.t) { case SolverFlag::T_INT: static_cast(editor)->setText(QString::number(index.data().toLongLong())); break; case SolverFlag::T_INT_RANGE: { auto* e = qobject_cast(editor); if (e) { e->setValue(index.data().toLongLong()); } else { static_cast(editor)->setText(QString::number(index.data().toLongLong())); } break; } case SolverFlag::T_FLOAT: static_cast(editor)->setText(QString::number(index.data().toDouble())); break; case SolverFlag::T_FLOAT_RANGE: static_cast(editor)->setValue(index.data().toDouble()); break; case SolverFlag::T_OPT: case SolverFlag::T_SOLVER: static_cast(editor)->setCurrentText(index.data().toString()); break; case SolverFlag::T_BOOL: case SolverFlag::T_BOOL_ONOFF: case SolverFlag::T_STRING: QStyledItemDelegate::setEditorData(editor, index); break; } } else if (index.data().type() == QVariant::Double) { static_cast(editor)->setText(QString::number(index.data().toDouble())); } else { QStyledItemDelegate::setEditorData(editor, index); } } void ExtraOptionDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { if (index.data(Qt::UserRole).canConvert()) { auto f = qvariant_cast(index.data(Qt::UserRole)); switch (f.t) { case SolverFlag::T_INT: model->setData(index, static_cast(editor)->text().toInt()); break; case SolverFlag::T_INT_RANGE: { auto* e = qobject_cast(editor); if (e) { model->setData(index, e->value()); } else { model->setData(index, static_cast(editor)->text().toLongLong()); } break; } case SolverFlag::T_BOOL: case SolverFlag::T_BOOL_ONOFF: QStyledItemDelegate::setModelData(editor, model, index); break; case SolverFlag::T_FLOAT: model->setData(index, static_cast(editor)->text().toDouble()); break; case SolverFlag::T_FLOAT_RANGE: model->setData(index, static_cast(editor)->value()); break; case SolverFlag::T_STRING: model->setData(index, static_cast(editor)->text()); break; case SolverFlag::T_OPT: case SolverFlag::T_SOLVER: model->setData(index, static_cast(editor)->currentText()); break; } } else if (index.data().type() == QVariant::Double) { model->setData(index, static_cast(editor)->text().toDouble()); } else { QStyledItemDelegate::setModelData(editor, model, index); } } void ConfigWindow::on_makeConfigDefault_pushButton_clicked() { auto sc = currentSolverConfig(); if (!sc) { return; } try { MznDriver::get().setDefaultSolver(sc->solverDefinition); ui->makeConfigDefault_pushButton->setDisabled(true); } catch (Exception& e) { QMessageBox::warning(this, "MiniZinc IDE", e.message(), QMessageBox::Ok); } } void ConfigWindow::on_reset_pushButton_clicked() { auto sc = currentSolverConfig(); if (!sc || !sc->isBuiltin) { return; } auto* newSc = new SolverConfiguration(sc->solverDefinition, true); int i = currentIndex(); delete configs[i]; configs.replace(i, newSc); updateGUI(true); populateComboBox(); } ConfigWindow::StashItem::StashItem(SolverConfiguration* sc) : json(sc->toJSONObject()), isBuiltIn(sc->isBuiltin), paramFile(sc->paramFile) {} SolverConfiguration* ConfigWindow::StashItem::consume() { QStringList warnings; auto* sc = new (SolverConfiguration) (SolverConfiguration::loadJSON(QJsonDocument(json), warnings)); sc->isBuiltin = isBuiltIn; sc->paramFile = paramFile; return sc; } ================================================ FILE: MiniZincIDE/configwindow.h ================================================ /* * Main authors: * Jason Nguyen */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef CONFIGWINDOW_H #define CONFIGWINDOW_H #include #include #include #include #include #include #include #include "solver.h" #include "extraparamdialog.h" namespace Ui { class ConfigWindow; } class ConfigWindow : public QWidget { Q_OBJECT friend class TestIDE; public: explicit ConfigWindow(QWidget *parent = nullptr); ~ConfigWindow(); void init(void); /// /// \brief ConfigWindow::loadConfigs /// Loads or reloads solver configs, populating default solver configs, and reloading non-default configs /// void loadConfigs(void); /// /// \brief Loads a configuration from a JSON file /// \param fileName The path to the JSON file /// \return True if loading the config was successful /// bool addConfig(const QString& fileName); /// /// \brief Adds/merges in existing solver configurations /// \param configs The configurations to add /// void mergeConfigs(const QList configs); int findBuiltinConfig(const QString& id, const QString& version); int findConfigFile(const QString& file); void removeConfig(int i); /// /// \brief Stash modified configs so they can be re-added later /// Stash modified configs and record the currently selected one /// void stashModifiedConfigs(); /// /// \brief Unstash previously stashed modified configs /// Unstash modified configs and select the config that was active during stashing /// void unstashModifiedConfigs(); /// /// \brief Saves the solver configuration with the given index /// \param index The index of the solver configuration to save /// \return The location of the saved file or empty if cancelled /// QString saveConfig(int index); /// /// \brief Saves the currently solver configuration /// \return The location of the saved file or empty if cancelled /// QString saveConfig(void); /// /// \brief Gets the index of the current solver configuration /// \return The current solver configuration index /// int currentIndex(void); /// /// \brief Sets the current solver configuration index /// \param i The new solver configuration index /// void setCurrentIndex(int i); /// /// \brief Gets the active solver configuration /// \return The current solver config or nullptr /// SolverConfiguration* currentSolverConfig(void); /// /// \brief Gets a list of all loaded solver configurations /// \return A list of solver configurations /// const QList& solverConfigs(void); signals: void selectedIndexChanged(int index); void selectedSolverConfigurationChanged(const SolverConfiguration* sc); void itemsChanged(const QStringList& items); void configSaved(const QString& filename); private slots: void on_numSolutions_checkBox_stateChanged(int arg1); void on_timeLimit_checkBox_stateChanged(int arg1); void on_randomSeed_checkBox_stateChanged(int arg1); void on_defaultBehavior_radioButton_toggled(bool checked); void on_userDefinedBehavior_radioButton_toggled(bool checked); void on_config_comboBox_currentIndexChanged(int index); void on_removeExtraParam_toolButton_clicked(); void on_extraParams_tableWidget_itemSelectionChanged(); void on_numOptimal_checkBox_stateChanged(int arg1); void on_clone_pushButton_clicked(); void on_makeConfigDefault_pushButton_clicked(); void on_reset_pushButton_clicked(); private: class StashItem { public: StashItem() {} StashItem(SolverConfiguration* sc); SolverConfiguration* consume(); private: QJsonObject json; bool isBuiltIn; QString paramFile; }; Ui::ConfigWindow *ui; QList configs; QVector stash; QString stashSelectedBuiltinSolverId; QString stashSelectedBuiltinSolverVersion; QString stashSelectedParamFile; int stashSelectedModifiedConfig = -1; int lastIndex = -1; bool initialized = false; bool populating = false; void updateGUI(bool overrideSync = false); void updateSolverConfig(SolverConfiguration* sc); void addExtraParam(const QString& key = "", bool backend = false, const QVariant& value = ""); void addExtraParam(const SolverFlag& f, const QVariant& value = QVariant()); void invalidate(bool all); void populateComboBox(void); void resizeExtraFlagsTable(void); ExtraParamDialog* extraParamDialog; }; class ExtraOptionDelegate : public QStyledItemDelegate { Q_OBJECT public: using QStyledItemDelegate::QStyledItemDelegate; QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; void setEditorData(QWidget* editor, const QModelIndex& index) const override; void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; }; class LongLongValidator : public QValidator { Q_OBJECT public: LongLongValidator(qlonglong min, qlonglong max, QObject *parent = nullptr) : QValidator(parent), _min(min), _max(max) {} LongLongValidator(QObject *parent = nullptr) : QValidator(parent) {} qlonglong bottom() const { return _min; } qlonglong top() const { return _max; } void setBottom(qlonglong min){ if (_min != min) { _min = min; changed(); } } void setTop(qlonglong max){ if (_max != max) { _max = max; changed(); } } void setRange(qlonglong min, qlonglong max){ setBottom(min); setTop(max); } QValidator::State validate(QString &input, int&) const override{ bool ok = false; qlonglong n = input.toLongLong(&ok); if (!ok) { return QValidator::Invalid; } if (n < _min || n > _max) { return QValidator::Intermediate; } return QValidator::Acceptable; } private: qlonglong _min = std::numeric_limits::min(); qlonglong _max = std::numeric_limits::max(); }; #endif // CONFIGWINDOW_H ================================================ FILE: MiniZincIDE/configwindow.ui ================================================ ConfigWindow 0 0 688 1140 Form Configuration 0 0 0 0 0 0 <html><head/><body><p>The currently selected solver configuration</p></body></html> Clone <html><head/><body><p>Make this solver configuration the default.</p></body></html> Set as default Solver: <unknown> Qt::Horizontal 40 20 Reset to defaults Options <html><head/><body><p>Preserve these options when switching solver configurations.</p></body></html> Maintain these options across solver configurations true Solving <html><head/><body><p>Limit running time to the given amount in seconds.</p></body></html> Time limit: false 0 0 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter s 3 99999.998999999996158 Default behavior true User defined behavior 0 0 QFrame::Panel QFrame::Raised Optimization problems: false 0 0 print all intermediate solutions Satisfaction problems: false 0 0 stop after first solution 0 0 QFrame::Panel QFrame::Raised Optimization problems <html><head/><body><p>Print intermediate solutions with <span style=" font-family:'monospace';">-i</span></p></body></html> Print intermediate solutions true <html><head/><body><p>Limit number of optimal solutions with <span style=" font-family:'monospace';">-n-o</span></p></body></html> Stop after this many optimal solutions (uncheck for all) true 0 0 1 Satisfaction problems <html><head/><body><p>Limit number of solutions with <span style=" font-family:'monospace';">-n</span></p></body></html> Stop after this many solutions (uncheck for all) true 0 0 1 999999 1 Output <html><head/><body><p>Enable verbose compilation with <span style=" font-family:'monospace';">--verbose-compilation</span>.</p></body></html> Verbose compilation <html><head/><body><p>Output solving statistics with <span style=" font-family:'monospace';">--solver-statistics</span></p></body></html> Output solving statistics <html><head/><body><p>Enable verbose compilation with <span style=" font-family:'monospace';">--verbose-solving</span>.</p></body></html> Verbose Solving <html><head/><body><p>Output compilation statistics with <span style=" font-family: monospace">--compiler-statistics</span></p></body></html> Output compilation statistics <html><head/><body><p>Output timing information with <span style=" font-family:'monospace';">--output-time</span></p></body></html> Output timing information Qt::Horizontal Output objective value Advanced options Compiling <html><head/><body><p>Compiler optimization level</p></body></html> Optimization level: 1 -O0 (no optimization) -O1 (default) -O2 (two pass compilation) -O3 (two pass compilation with Gecode) -O4 (as -O3 with shaving) -O5 (as -O3 with singleton arc consistency) <html><head/><body><p>Additional string data input with <span style=" font-family:monospace;">-D</span></p></body></html> Additional data: 16777215 40 Solving <html><head/><body><p>Sets the number of solver threads with <span style=" font-family:monospace;">-p</span></p></body></html> Number of threads: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 9999 1 <html><head/><body><p>Sets the random seed for the solver with <span style=" font-family:monospace;">-r</span></p></body></html> Random seed: false <html><head/><body><p>Enable free search with <span style=" font-family:'monospace';">-f</span></p></body></html> Free search <html><head/><body><p>Extra parameters passed to MiniZinc</p></body></html> Extra configuration parameters Qt::ScrollBarAsNeeded QAbstractScrollArea::AdjustToContents QAbstractItemView::AllEditTriggers QAbstractItemView::SelectRows true false Parameter Flag Type Data Type Value Qt::Horizontal 40 20 Add parameter QToolButton::InstantPopup Qt::ToolButtonTextOnly Qt::DownArrow false Remove parameter Qt::ToolButtonTextOnly Qt::Vertical 20 40 Custom parameter Add custom parameter to configuration Add all known parameters ================================================ FILE: MiniZincIDE/dark_mode.css ================================================ *, QTableView::item { border-color: #444444; color: #FFFFFF; } QMainWindow, QDialog, QToolBar, QAbstractScrollArea, QAction, QDockWidget { background-color: #333333; alternate-background-color: #444444; } *:disabled { background-color: #333333; color: #666666; border-color: #666666; } QMenuBar, QPushButton, QToolButton, QTabWidget::pane QMenuBar, QTabWidget::pane QPushButton, QTabWidget::pane QToolButton { background-color: #444444; } QToolBar::separator { background-color: #444444; width: 1px; margin: 6px; } QMenuBar::item:selected, QMenu::item:selected { background-color: #555555; } QMenuBar::item:pressed { background-color: #666666; } QMenu { background-color: #3D3D3D; } QTabWidget::tab-bar, QTabWidget::pane, QTabBar { border: 0; } QTabWidget::tab-bar, QTabBar { background-color: #444444; } QTabBar::tab { background-color: #3A3A3A; } QTabBar::tab:selected { background-color: #262626; } QTabBar::tab:hover { background-color: #2D2D2D; } QTabWidget::pane, QTabWidget::pane * { background-color: #262626; } QPlainTextEdit, QTextEdit, QLineEdit, QComboBox, QAbstractSlider, QAbstractSpinBox, QListView, QTreeView, QTableView, QTableView::item { background-color: #222222; padding: 3px; border: 1px solid #666666; } QTabWidget::pane QPlainTextEdit, QTabWidget::pane QTextEdit, QTabWidget::pane QLineEdit, QTabWidget::pane QComboBox, QTabWidget::pane QAbstractSlider, QTabWidget::pane QAbstractSpinBox, QTabWidget::pane QListView, QTabWidget::pane QTreeView, QTabWidget::pane QTableView, QTabWidget::pane QTableView::item { background-color: #1D1D1D; padding: 3px; border: 1px solid #666666; } QGroupBox { border: 1px solid #666666; margin-top: 1.5ex; margin-bottom: 1.5ex; padding: 1ex; padding-top: 2ex; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left: 5px; padding: 0 1ex; } QToolTip { color: #000000; } QDockWidget::title { background-color: #222222; text-align: left center; } QDockWidget::close-button, QDockWidget::float-button { background-color: #000000; } QStatusBar QLabel { padding-left: 1ex; padding-right: 1ex; } QStatusBar::item { border: 0; } QScrollBar { color: #555555; } QScrollBar:vertical { margin-top: 16px; margin-bottom: 16px; } QScrollBar:horizontal { margin-left: 16px; margin-right: 16px; } QScrollBar::handle { border: 0; background: #444444; } QScrollBar::handle:vertical { min-height: 20px; } QScrollBar::handle:horizontal { min-width: 20px; } QScrollBar::add-page, QScrollBar::sub-page { background: none; } QScrollBar::add-line:vertical { height: 16px; subcontrol-position: bottom; subcontrol-origin: margin; } QScrollBar::sub-line:vertical { height: 16px; subcontrol-position: top; subcontrol-origin: margin; } QScrollBar::add-line:horizontal { width: 16px; subcontrol-position: right; subcontrol-origin: margin; border: 0; } QScrollBar::sub-line:horizontal { width: 16px; subcontrol-position: left; subcontrol-origin: margin; border: 0; } QHeaderView::section { background-color: #333333; } ================================================ FILE: MiniZincIDE/darkmodenotifier.cpp ================================================ #include "darkmodenotifier.h" #include #include DarkModeNotifier::DarkModeNotifier(QObject *parent) : QObject(parent) { init(); if (!hasSystemSetting()) { QSettings settings; settings.beginGroup("MainWindow"); _darkMode = settings.value("darkMode", false).value(); } } void DarkModeNotifier::requestChangeDarkMode(bool enable) { if (hasSystemSetting()) { // Can't change dark mode since we're using system setting return; } if (enable != _darkMode) { _darkMode = enable; emit darkModeChanged(_darkMode); } } #ifdef Q_OS_WIN #include #define NOMINMAX #include class DarkModeNotifier::Internal { public: QWinEventNotifier* notifier = nullptr; HKEY hKey = nullptr; HANDLE hEvent = nullptr; bool ok = true; }; void DarkModeNotifier::init() { auto getDarkMode = [=] () { if (!_internal->ok) { return false; } DWORD value = 1; DWORD size = sizeof(DWORD); auto result = RegGetValue(_internal->hKey, nullptr, L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &size); if (result != ERROR_SUCCESS) { _internal->ok = false; return false; } return value == 0; }; _internal = new Internal; // Get dark mode registry key auto result = RegOpenKey(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", &_internal->hKey); _internal->ok = result == ERROR_SUCCESS; if (!_internal->ok) { return; } // Get initial dark mode _darkMode = getDarkMode(); if (!_internal->ok) { return; } // Setup event to be triggered on registry change _internal->hEvent = CreateEvent(nullptr, true, false, nullptr); _internal->ok = _internal->hEvent != nullptr; if (!_internal->ok) { return; } // Listen for event _internal->notifier = new QWinEventNotifier(_internal->hEvent); connect(_internal->notifier, &QWinEventNotifier::activated, this, [=] () { bool newDarkMode = getDarkMode(); if (!_internal->ok) { return; } if (newDarkMode != _darkMode) { _darkMode = newDarkMode; emit darkModeChanged(newDarkMode); } _internal->ok = ResetEvent(_internal->hEvent) != 0; if (!_internal->ok) { return; } auto result = RegNotifyChangeKeyValue(_internal->hKey, true, REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET, _internal->hEvent, true); _internal->ok = result == ERROR_SUCCESS; }); // Start notifier result = RegNotifyChangeKeyValue(_internal->hKey, true, REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET, _internal->hEvent, true); _internal->ok = result == ERROR_SUCCESS; } DarkModeNotifier::~DarkModeNotifier() { delete _internal->notifier; CloseHandle(_internal->hEvent); RegCloseKey(_internal->hKey); delete _internal; } bool DarkModeNotifier::hasSystemSetting() const { return _internal->ok; } bool DarkModeNotifier::hasNativeDarkMode() const { #if QT_VERSION < 0x060500 // Have to style using CSS, no native dark widgets return false; #else if (hasSystemSetting()) { QApplication::setStyle("Fusion"); return true; } return false; #endif } #elif !defined(Q_OS_MAC) // No support for system dark mode void DarkModeNotifier::init() {} DarkModeNotifier::~DarkModeNotifier() {} bool DarkModeNotifier::hasSystemSetting() const { return false; } bool DarkModeNotifier::hasNativeDarkMode() const { // Have to style using CSS, no native dark widgets return false; } #endif ================================================ FILE: MiniZincIDE/darkmodenotifier.h ================================================ #ifndef DARKMODENOTIFIER_H #define DARKMODENOTIFIER_H #include class DarkModeNotifier : public QObject { Q_OBJECT public: explicit DarkModeNotifier(QObject *parent = nullptr); ~DarkModeNotifier(); bool hasSystemSetting() const; bool hasNativeDarkMode() const; bool darkMode() const { return _darkMode; } public slots: void requestChangeDarkMode(bool enable); signals: void darkModeChanged(bool darkMode); private: void init(); class Internal; Internal* _internal = nullptr; bool _darkMode = false; }; #endif // DARKMODENOTIFIER_H ================================================ FILE: MiniZincIDE/darkmodenotifier_macos.mm ================================================ #import #include "darkmodenotifier.h" #include @interface DarkModeObserver : NSObject @property (readonly, nonatomic) std::function handler; - (instancetype)initWithHandler:(std::function)handler; @end @implementation DarkModeObserver - (instancetype)initWithHandler:(std::function)handler { if ((self = [super init])) { _handler = handler; if (@available(macOS 10.14, *)) { [NSApp addObserver:self forKeyPath:@"effectiveAppearance" options:0 context:nullptr ]; } } return self; } - (void)dealloc { if (@available(macOS 10.14, *)) { [NSApp removeObserver:self forKeyPath:@"effectiveAppearance" ]; } [super dealloc]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { self.handler(); } @end class DarkModeNotifier::Internal { public: DarkModeObserver* darkModeObserver = nullptr; }; void DarkModeNotifier::init() { auto getDarkMode = [=] () { if (@available(macOS 10.14, *)) { id appObjects[] = { NSAppearanceNameAqua, NSAppearanceNameDarkAqua }; NSArray* appearances = [NSArray arrayWithObjects: appObjects count:2]; NSAppearanceName appearance = [[NSApp effectiveAppearance] bestMatchFromAppearancesWithNames: appearances]; if ([appearance isEqualToString:NSAppearanceNameDarkAqua]) { return true; } } return false; }; _internal = new Internal; _darkMode = getDarkMode(); auto handler = [=] () { bool newDarkMode = getDarkMode(); if (_darkMode != newDarkMode) { _darkMode = newDarkMode; emit darkModeChanged(newDarkMode); } }; _internal->darkModeObserver = [[DarkModeObserver alloc] initWithHandler:handler]; } DarkModeNotifier::~DarkModeNotifier() { [_internal->darkModeObserver release]; delete _internal; } bool DarkModeNotifier::hasSystemSetting() const { if (@available(macOS 10.14, *)) { return true; } return false; } bool DarkModeNotifier::hasNativeDarkMode() const { return hasSystemSetting(); } ================================================ FILE: MiniZincIDE/elapsedtimer.cpp ================================================ #include "elapsedtimer.h" ElapsedTimer::ElapsedTimer(QObject* parent) : QObject(parent) { connect(&_interval, &QTimer::timeout, this, [=] () { emit timeElapsed(_elapsed.elapsed()); }); } qint64 ElapsedTimer::elapsed() { if (_elapsed.isValid()) { return _elapsed.elapsed(); } return finalTime; } bool ElapsedTimer::isRunning() { return _elapsed.isValid(); } void ElapsedTimer::start(int updateRate) { _interval.start(updateRate); _elapsed.start(); } void ElapsedTimer::stop() { _interval.stop(); if (_elapsed.isValid()) { finalTime = _elapsed.elapsed(); _elapsed.invalidate(); } } ================================================ FILE: MiniZincIDE/elapsedtimer.h ================================================ #ifndef ELAPSEDTIMER_H #define ELAPSEDTIMER_H #include #include #include class ElapsedTimer: public QObject { Q_OBJECT public: ElapsedTimer(QObject* parent = nullptr); qint64 elapsed(); bool isRunning(); signals: void timeElapsed(qint64 time); public slots: void start(int updateRate); void stop(); private: QTimer _interval; QElapsedTimer _elapsed; qint64 finalTime; }; #endif // ELAPSEDTIMER_H ================================================ FILE: MiniZincIDE/esclineedit.cpp ================================================ #include "esclineedit.h" #include #include EscLineEdit::EscLineEdit(QWidget* parent) : QLineEdit(parent) { } void EscLineEdit::keyReleaseEvent(QKeyEvent* event) { if(event->key() == Qt::Key_Escape) { emit(escPressed()); } } ================================================ FILE: MiniZincIDE/esclineedit.h ================================================ #ifndef ESCLINEEDIT_H #define ESCLINEEDIT_H #include class EscLineEdit : public QLineEdit { Q_OBJECT public: EscLineEdit(QWidget* parent = 0); signals: void escPressed(void); protected: void keyReleaseEvent(QKeyEvent *); }; #endif // ESCLINEEDIT_H ================================================ FILE: MiniZincIDE/exception.h ================================================ #ifndef EXCEPTION_H #define EXCEPTION_H #include class Exception : public QException { public: Exception(const QString& _msg) : msg(_msg) {} const QString& message(void) const { return msg; } void raise() const override { throw *this; } Exception *clone() const override { return new Exception(*this); } protected: QString msg; }; class InternalError : public Exception { public: InternalError(const QString& _msg) : Exception(_msg) {} void raise() const override { throw *this; } Exception *clone() const override { return new InternalError(*this); } }; class ProcessError : public Exception { public: ProcessError(const QString& _msg) : Exception(_msg) {} void raise() const override { throw *this; } Exception *clone() const override { return new ProcessError(*this); } }; class ProjectError : public Exception { public: ProjectError(const QString& _msg) : Exception(_msg) {} void raise() const override { throw *this; } Exception *clone() const override { return new ProjectError(*this); } }; class MoocError : public Exception { public: MoocError(const QString& _msg) : Exception(_msg) {} void raise() const override { throw *this; } Exception *clone() const override { return new MoocError(*this); } }; class ConfigError : public Exception { public: ConfigError(const QString& _msg) : Exception(_msg) {} void raise() const override { throw *this; } Exception *clone() const override { return new ConfigError(*this); } }; class DriverError : public Exception { public: DriverError(const QString& _msg) : Exception(_msg) {} void raise() const override { throw *this; } Exception *clone() const override { return new DriverError(*this); } }; class FileError : public Exception { public: FileError(const QString& _msg) : Exception(_msg) {} void raise() const override { throw *this; } Exception *clone() const override { return new FileError(*this); } }; class ServerError : public Exception { public: ServerError(const QString& _msg) : Exception(_msg) {} void raise() const override { throw *this; } Exception *clone() const override { return new ServerError(*this); } }; #endif // EXCEPTION_H ================================================ FILE: MiniZincIDE/extraparamdialog.cpp ================================================ #include "extraparamdialog.h" #include "ui_extraparamdialog.h" #include ExtraParamDialog::ExtraParamDialog(QWidget* parent) : QWidget(parent), ui(new Ui::ExtraParamDialog) { ui->setupUi(this); _sourceModel = new QStandardItemModel(this); _filterModel = new QSortFilterProxyModel(this); _filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); _filterModel->setSourceModel(_sourceModel); _filterModel->setFilterKeyColumn(1); ui->params_listView->setModel(_filterModel); connect(ui->params_listView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ExtraParamDialog::on_selectionChanged); } ExtraParamDialog::~ExtraParamDialog() { delete ui; } void ExtraParamDialog::setParams(const QList& params) { _sourceModel->clear(); _sourceModel->setRowCount(params.length()); _sourceModel->setColumnCount(2); for (int i = 0; i < params.length(); i++) { auto* item = new QStandardItem(params[i].description); item->setFlags(Qt::ItemFlag::ItemIsSelectable | Qt::ItemFlag::ItemIsEnabled); item->setData(QVariant::fromValue(params[i])); item->setToolTip(params[i].name); _sourceModel->setItem(i, 0, item); // Allow filtering to search by flag name and description auto* searchItem = new QStandardItem(params[i].name + " " + params[i].description); _sourceModel->setItem(i, 1, searchItem); } } void ExtraParamDialog::setParamEnabled(const SolverFlag& param, bool enabled) { for (int i = 0; i < _sourceModel->rowCount(); i++) { auto* item = _sourceModel->item(i); auto flag = qvariant_cast(item->data()); if (flag.name == param.name) { item->setFlags(enabled ? Qt::ItemFlag::ItemIsSelectable | Qt::ItemFlag::ItemIsEnabled : Qt::ItemFlag::NoItemFlags); return; } } } void ExtraParamDialog::on_selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { // The selected argument is wrong for some reason, so just check the selected indexes manually bool hasSelection = ui->params_listView->selectionModel()->selectedIndexes().empty(); ui->add_pushButton->setDisabled(hasSelection); } void ExtraParamDialog::showEvent(QShowEvent* event) { QWidget::showEvent(event); ui->filter_lineEdit->setFocus(); } void ExtraParamDialog::on_filter_lineEdit_textEdited(const QString& pattern) { _filterModel->setFilterFixedString(pattern); } void ExtraParamDialog::on_addAll_pushButton_clicked() { QList flags; for (int i = 0; i < _sourceModel->rowCount(); i++) { auto* item = _sourceModel->item(i); if (item->isEnabled()) { auto sf = qvariant_cast(item->data()); flags << sf; } } emit addParams(flags); resetControls(); } void ExtraParamDialog::on_customParameter_pushButton_clicked() { emit addCustomParam(); resetControls(); } void ExtraParamDialog::on_add_pushButton_clicked() { QList flags; for (auto item : ui->params_listView->selectionModel()->selectedIndexes()) { if (item.column() == 0) { auto sf = qvariant_cast(item.data(Qt::UserRole + 1)); flags << sf; } } emit addParams(flags); resetControls(); } void ExtraParamDialog::resetControls() { ui->filter_lineEdit->clear(); ui->params_listView->selectionModel()->clear(); _filterModel->setFilterFixedString(""); } void ExtraParamDialog::mousePressEvent(QMouseEvent* event) { // Hack which stops the menu from closing when clicking on the widget empty space event->accept(); } void ExtraParamDialog::mouseReleaseEvent(QMouseEvent* event) { // Hack which stops the menu from closing when clicking on the widget empty space event->accept(); } void ExtraParamDialog::on_params_listView_activated(const QModelIndex &index) { on_add_pushButton_clicked(); } ================================================ FILE: MiniZincIDE/extraparamdialog.h ================================================ #ifndef EXTRAPARAMDIALOG_H #define EXTRAPARAMDIALOG_H #include #include #include #include "solver.h" namespace Ui { class ExtraParamDialog; } class ExtraParamDialog : public QWidget { Q_OBJECT public: explicit ExtraParamDialog(QWidget* parent = nullptr); ~ExtraParamDialog() override; void showEvent(QShowEvent* event) override; signals: void addParams(const QList& params); void addCustomParam(); public slots: void setParams(const QList& params); void setParamEnabled(const SolverFlag& param, bool enabled); protected: void mousePressEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; private slots: void on_filter_lineEdit_textEdited(const QString& pattern); void on_selectionChanged(const QItemSelection& selected, const QItemSelection& deselected); void on_addAll_pushButton_clicked(); void on_customParameter_pushButton_clicked(); void on_add_pushButton_clicked(); void resetControls(); void on_params_listView_activated(const QModelIndex &index); private: Ui::ExtraParamDialog* ui; QStandardItemModel* _sourceModel; QSortFilterProxyModel* _filterModel; }; #endif // EXTRAPARAMDIALOG_H ================================================ FILE: MiniZincIDE/extraparamdialog.ui ================================================ ExtraParamDialog 0 0 371 352 Form Filter QAbstractItemView::NoEditTriggers true QAbstractItemView::ExtendedSelection QAbstractItemView::SelectRows true false Add the selected parameters. Add parameter Add all available parameters. Add all known parameters Add a custom command line parameter to the minizinc command or to the solver executable. Add custom parameter ================================================ FILE: MiniZincIDE/fzndoc.cpp ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "fzndoc.h" FznDoc::FznDoc(QObject *parent) : QObject(parent) { } ================================================ FILE: MiniZincIDE/fzndoc.h ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef FZNDOC_H #define FZNDOC_H #include class FznDoc : public QObject { Q_OBJECT Q_PROPERTY(QString str READ str WRITE setstr) public: explicit FznDoc(QObject *parent = 0); void setstr(const QString& s) { m_str =s ; } QString str(void) { return m_str; } private: QString m_str; }; #endif // FZNDOC_H ================================================ FILE: MiniZincIDE/gotolinedialog.cpp ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "gotolinedialog.h" #include "ui_gotolinedialog.h" GoToLineDialog::GoToLineDialog(QWidget *parent) : QDialog(parent), ui(new Ui::GoToLineDialog) { ui->setupUi(this); ui->line->setFocus(); } GoToLineDialog::~GoToLineDialog() { delete ui; } int GoToLineDialog::getLine(bool *ok) { return ui->line->text().toInt(ok); } ================================================ FILE: MiniZincIDE/gotolinedialog.h ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef GOTOLINEDIALOG_H #define GOTOLINEDIALOG_H #include namespace Ui { class GoToLineDialog; } class GoToLineDialog : public QDialog { Q_OBJECT public: explicit GoToLineDialog(QWidget *parent = 0); ~GoToLineDialog(); int getLine(bool* ok); private: Ui::GoToLineDialog *ui; }; #endif // GOTOLINEDIALOG_H ================================================ FILE: MiniZincIDE/gotolinedialog.ui ================================================ GoToLineDialog 0 0 179 78 Go to line true 10 40 161 32 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok 10 10 161 21 line buttonBox buttonBox accepted() GoToLineDialog accept() 248 254 157 274 buttonBox rejected() GoToLineDialog reject() 316 260 286 274 ================================================ FILE: MiniZincIDE/highlighter.cpp ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include "highlighter.h" #include "ideutils.h" Highlighter::Highlighter(QFont& font, const Theme& theme, bool dm, QTextDocument *parent) : QSyntaxHighlighter(parent), keywordColor(Qt::darkGreen), functionColor(Qt::blue), stringColor(Qt::darkRed), commentColor(Qt::red) { Rule rule; commentFormat.setForeground(commentColor); QStringList patterns; patterns << "\\bann\\b" << "\\bannotation\\b" << "\\bany\\b" << "\\barray\\b" << "\\bbool\\b" << "\\bcase\\b" << "\\bconstraint\\b" << "\\bdefault\\b" << "\\bdiv\\b" << "\\bdiff\\b" << "\\belse\\b" << "\\belseif\\b" << "\\bendif\\b" << "\\benum\\b" << "\\bfloat\\b" << "\\bfunction\\b" << "\\bif\\b" << "\\binclude\\b" << "\\bintersect\\b" << "\\bin\\b" << "\\bint\\b" << "\\blet\\b" << "\\blist\\b" << "\\bmaximize\\b" << "\\bminimize\\b" << "\\bmod\\b" << "\\bnot\\b" << "\\bof\\b" << "\\boutput\\b" << "\\bopt\\b" << "\\bpar\\b" << "\\bpredicate\\b" << "\\brecord\\b" << "\\bsatisfy\\b" << "\\bset\\b" << "\\bsolve\\b" << "\\bstring\\b" << "\\bsubset\\b" << "\\bsuperset\\b" << "\\bsymdiff\\b" << "\\btest\\b" << "\\bthen\\b" << "\\btuple\\b" << "\\btype\\b" << "\\bunion\\b" << "\\bvar\\b" << "\\bvariant_record\\b" << "\\bwhere\\b" << "\\bxor\\b"; QTextCharFormat format; format.setForeground(functionColor); rule.pattern = QRegularExpression("\\b[A-Za-z0-9_]+(?=\\s*\\()"); rule.format = format; rules.append(rule); format = QTextCharFormat(); format.setFontWeight(QFont::Bold); format.setForeground(keywordColor); for (int i=0; i b.el || (a.el == b.el && a.ec >= b.ec)); } void Highlighter::addFixedBg( unsigned int sl, unsigned int sc, unsigned int el, unsigned ec, QColor colour, QString tip) { FixedBg ifb {sl, sc, el, ec}; for(BgMap::iterator it = fixedBg.begin(); it != fixedBg.end();) { const FixedBg& fb = it.key(); if (fg_contains(fb, ifb)) { it = fixedBg.erase(it); } else { ++it; } } fixedBg.insert(ifb, QPair(colour, tip)); } void Highlighter::clearFixedBg() { fixedBg.clear(); } void Highlighter::highlightBlock(const QString &text) { QTextBlock block = currentBlock(); BracketData* bd = new BracketData; if (currentBlockUserData()) { bd->d = static_cast(currentBlockUserData())->d; } if (block.previous().userData()) { bd->highlightingState = static_cast(block.previous().userData())->highlightingState; } QRegularExpression noneRegExp("\"|%|/\\*"); QRegularExpression stringRegExp("\"|\\\\\\("); QRegularExpression commentRegexp("\\*/"); QRegularExpression interpolateRegexp("[)\"%(]|/\\*"); QTextCharFormat stringFormat; stringFormat.setForeground(stringColor); stringFormat.setFontItalic(true); QTextCharFormat interpolateFormat; interpolateFormat.setFontItalic(true); // Stage 1: find strings (including interpolations) and comments QVector& highlightingState = bd->highlightingState; int curPosition = 0; HighlightingState currentState; while (curPosition >= 0) { currentState = highlightingState.empty() ? HS_NONE : highlightingState.back(); switch (currentState) { case HS_NONE: { int nxt = noneRegExp.match(text, curPosition).capturedStart(); if (nxt==-1) { curPosition = -1; } else { if (text[nxt]=='"') { highlightingState.push_back(HS_STRING); curPosition = nxt+1; } else if (text[nxt]=='%') { setFormat(nxt, text.size()-nxt, commentFormat); curPosition = -1; } else { // /* highlightingState.push_back(HS_COMMENT); curPosition = nxt+2; } } } break; case HS_STRING: { int nxt = stringRegExp.match(text, curPosition).capturedStart(); int stringStartIdx = curPosition==0 ? 0 : curPosition-1; if (nxt==-1) { setFormat(stringStartIdx, text.size()-stringStartIdx, stringFormat); highlightingState.clear(); // this is an error, reset to NONE state curPosition = -1; } else { if (text[nxt]=='"') { setFormat(stringStartIdx, nxt-stringStartIdx+1, stringFormat); curPosition = nxt+1; highlightingState.pop_back(); } else { setFormat(stringStartIdx, nxt-stringStartIdx+2, stringFormat); curPosition = nxt+2; highlightingState.push_back(HS_INTERPOLATE); } } } break; case HS_COMMENT: { int nxt = commentRegexp.match(text, curPosition).capturedStart(); int commentStartIdx = curPosition <= 1 ? 0 : curPosition - 2; if (nxt==-1) { // EOL -> stay in COMMENT state setFormat(commentStartIdx, text.size()-commentStartIdx, commentFormat); curPosition = -1; } else { // finish comment setFormat(commentStartIdx, nxt-commentStartIdx+2, commentFormat); curPosition = nxt+2; highlightingState.pop_back(); } } break; case HS_INTERPOLATE: { int nxt = interpolateRegexp.match(text, curPosition).capturedStart(); if (nxt==-1) { // EOL -> stay in INTERPOLATE state setFormat(curPosition, text.size()-curPosition, interpolateFormat); curPosition = -1; } else { setFormat(curPosition, nxt-curPosition+1, interpolateFormat); if (text[nxt]==')') { curPosition = nxt+1; highlightingState.pop_back(); } else if (text[nxt]=='(') { curPosition = nxt+1; highlightingState.push_back(HS_INTERPOLATE); } else if (text[nxt]=='%') { setFormat(nxt, text.size()-nxt, commentFormat); curPosition = -1; } else if (text[nxt]=='"') { curPosition = nxt+1; highlightingState.push_back(HS_STRING); } else { // /* highlightingState.push_back(HS_COMMENT); curPosition = nxt+1; } } } break; } } currentState = highlightingState.empty() ? HS_NONE : highlightingState.back(); setCurrentBlockState(currentState); // Stage 2: find keywords and functions for (int i=0; i= 0) { int length = match.capturedLength(); if (format(index)!=commentFormat && format(index)!=stringFormat) { if (format(index)==interpolateFormat) { QTextCharFormat interpolateRule = rule.format; interpolateRule.setFontItalic(true); setFormat(index, length, interpolateRule); } else { setFormat(index, length, rule.format); } } match = expression.match(text, index + length); index = match.capturedStart(); } } // Stage 3: update bracket data QRegularExpression re("\\(|\\)|\\{|\\}|\\[|\\]"); auto re_match = re.match(text); int pos = re_match.capturedStart(); while (pos != -1) { if (format(pos)!=commentFormat) { Bracket b; b.b = text.at(pos); b.pos = pos; bd->brackets.append(b); } re_match = re.match(text, pos+1); pos = re_match.capturedStart(); } setCurrentBlockUserData(bd); // Stage 4: apply highlighting for(QHash >::iterator it = fixedBg.begin(); it != fixedBg.end(); ++it) { const FixedBg& fb = it.key(); QPair val = it.value(); QColor colour = val.first; QString tip = val.second; unsigned int blockNumber = block.blockNumber() + 1; if(fb.sl <= blockNumber && fb.el >= blockNumber) { int index = 0; int length = block.length(); if(fb.sl == blockNumber) { index = fb.sc - 1; if(fb.sl == fb.el) { length = fb.ec - index; } } else if(fb.el == blockNumber) { length = fb.ec; } int endpos = index + length; foreach(const QTextLayout::FormatRange& fr, block.textFormats()) { //if(index >= fr.start && index <= fr.start + length) { int local_index = fr.start < index ? index : fr.start; int fr_endpos = fr.start + fr.length; int local_len = (fr_endpos < endpos ? fr_endpos : endpos) - local_index; QTextCharFormat fmt = fr.format; fmt.setBackground(colour); fmt.setToolTip(tip); setFormat(local_index, local_len, fmt); //} } } } } #include #include #include #include #include #include #include void Highlighter::copyHighlightedToClipboard(QTextCursor cursor) { QTextDocument* tempDocument(new QTextDocument); Q_ASSERT(tempDocument); QTextCursor tempCursor(tempDocument); tempCursor.insertFragment(cursor.selection()); tempCursor.select(QTextCursor::Document); QTextCharFormat textfmt = cursor.charFormat(); textfmt.setFont(commentFormat.font()); tempCursor.setCharFormat(textfmt); QTextBlock start = document()->findBlock(cursor.selectionStart()); QTextBlock end = document()->findBlock(cursor.selectionEnd()); end = end.next(); const int selectionStart = cursor.selectionStart(); const int endOfDocument = tempDocument->characterCount() - 1; for(QTextBlock current = start; current.isValid() && current != end; current = current.next()) { const QTextLayout* layout(current.layout()); foreach(const QTextLayout::FormatRange &range, layout->formats()) { const int start = current.position() + range.start - selectionStart; const int end = start + range.length; if(end <= 0 || start >= endOfDocument) continue; tempCursor.setPosition(qMax(start, 0)); tempCursor.setPosition(qMin(end, endOfDocument), QTextCursor::KeepAnchor); tempCursor.setCharFormat(range.format); } } for(QTextBlock block = tempDocument->begin(); block.isValid(); block = block.next()) block.setUserState(-1); tempCursor.select(QTextCursor::Document); QTextBlockFormat blockFormat = cursor.blockFormat(); blockFormat.setNonBreakableLines(true); tempCursor.setBlockFormat(blockFormat); IDEUtils::MimeDataExporter te; te.setDocument(tempDocument); te.selectAll(); QMimeData* mimeData = te.md(); QApplication::clipboard()->setMimeData(mimeData); delete tempDocument; } void Highlighter::setTheme(const Theme& theme, bool darkMode) { keywordColor = theme.keywordColor.get(darkMode); functionColor = theme.functionColor.get(darkMode); stringColor = theme.stringColor.get(darkMode); commentColor = theme.commentColor.get(darkMode); commentFormat.setForeground(commentColor); rules[0].format.setForeground(functionColor); for (int i=1; i * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef HIGHLIGHTER_H #define HIGHLIGHTER_H #include "theme.h" #include #include #include #include struct Bracket { QChar b; int pos; }; class DebugInfoData { public: int con; // DebugInfo: number of constraints int var; // DebugInfo: number of variables int ms; // DebugInfo: milliseconds int totalCon; int totalVar; int totalMs; DebugInfoData(void) : con(0), var(0), ms(0), totalCon(0), totalVar(0), totalMs(1) {} bool hasData(void) { return con!=0 || var!=0 || ms!=0; } void reset(void) { con=0; var=0; ms=0; totalCon=0; totalVar=0; totalMs=1; } QString toString(void) { return QString().number(ms)+"ms,"+QString().number(con)+","+QString().number(var); } }; enum HighlightingState { HS_NONE=-1, HS_STRING, HS_INTERPOLATE, HS_COMMENT }; class BracketData : public QTextBlockUserData { public: QVector brackets; DebugInfoData d; QVector highlightingState; }; struct FixedBg { unsigned int sl; unsigned int sc; unsigned int el; unsigned int ec; }; typedef QHash > BgMap; uint qHash(const FixedBg& key); inline bool operator==(const FixedBg &A, const FixedBg &B) { return A.sl == B.sl && A.sc == B.sc && A.el == B.el && A.ec == B.ec; } class Highlighter : public QSyntaxHighlighter { Q_OBJECT public: Highlighter(QFont& font, const Theme& theme, bool darkMode, QTextDocument *parent = 0); void setEditorFont(QFont& font); void copyHighlightedToClipboard(QTextCursor selectionCursor); void setTheme(const Theme& theme, bool darkMode); void addFixedBg(unsigned int sl, unsigned int sc, unsigned int el, unsigned ec, QColor colour, QString tip); void clearFixedBg(); protected: void highlightBlock(const QString &text); private: struct Rule { QRegularExpression pattern; QTextCharFormat format; }; QVector rules; BgMap fixedBg; QColor keywordColor; QColor functionColor; QColor stringColor; QColor commentColor; QTextCharFormat commentFormat; QRegularExpression commentStartExp; QRegularExpression commentEndExp; }; #endif // HIGHLIGHTER_H ================================================ FILE: MiniZincIDE/history.cpp ================================================ #include "history.h" #include #include #include #include #include FileDiff::FileDiff(const QString& origText, const QString& newText): _orig(origText.split("\n")), _new(newText.split("\n")) { _offset = 0; _origLength = _orig.size(); _newLength = _new.size(); // Trim start lines if same while (_offset < _orig.size() - 1 && _offset < _new.size() - 1 && _orig[_offset] == _new[_offset]) { _offset++; _origLength--; _newLength--; } // Trim end lines if same while (_origLength > 0 && _newLength > 0 && _orig[_offset + _origLength - 1] == _new[_offset + _newLength - 1]) { _origLength--; _newLength--; } } QVector FileDiff::diff() { if (_origLength == 0 && _newLength == 0) { return {}; } auto m = _origLength + 1; auto n = _newLength + 1; QVector c; c.resize(m * n); for (auto i = 0; i < _origLength; i++) { for (auto j = 0; j < _newLength; j++) { if (_orig[i + _offset] == _new[j + _offset]) { c[(j + 1) * m + i + 1] = c[j * m + i] + 1; } else { auto a = c[j * m + i + 1]; auto b = c[(j + 1) * m + i]; c[(j + 1) * m + i + 1] = qMax(a, b); } } } QVector result; auto i = _origLength; auto j = _newLength; while (true) { if (i > 0 && j > 0 && c[j * m + i] == c[(j - 1) * m + i - 1] + 1 && c[(j - 1) * m + i] == c[j * m + i - 1]) { i--; j--; } else if (j > 0 && (i == 0 || c[(j - 1) * m + i] >= c[j * m + i - 1])) { result.prepend(FileDiff::Entry( FileDiff::EntryType::Insertion, i + 1 + _offset, _new[j - 1 + _offset] )); j--; } else if (i > 0 && (j == 0 || c[(j - 1) * m + i] < c[j * m + i - 1])) { result.prepend(FileDiff::Entry( FileDiff::EntryType::Deletion, i + _offset, _orig[i - 1 + _offset] )); i--; } else { break; } } return result; } QString FileDiff::apply(const QString& source, const QVector& diff) { auto split = source.split("\n"); QStringList text; auto it = diff.begin(); for (auto i = 0; i < split.size(); i++) { bool deleteOrig = false; while (it != diff.end() && it->line == i + 1) { if (it->kind == FileDiff::EntryType::Insertion) { text.append(it->text); } else { assert(!deleteOrig); deleteOrig = true; } it++; } if (!deleteOrig) { text.append(split[i]); } } while (it != diff.end()) { assert(it->kind == FileDiff::EntryType::Insertion); assert(it->line == split.size() + 1); text.append(it->text); it++; } return text.join('\n'); } History::History(const QJsonObject& obj, const QDir& relativeTo, QObject* parent): QObject(parent), _rootDir(relativeTo) { _uuid = obj["uuid"].toString(); _parent = obj["parent"].toString(); auto files = obj["files"].toObject(); for (auto it = files.begin(); it != files.end(); it++) { auto file = _rootDir.absoluteFilePath(it.key()); auto entry = it.value().toObject(); FileHistory h; h.snapshot = entry["snapshot"].toString(); for (auto change : entry["changes"].toArray()) { auto changeObj = change.toObject(); History::Change c; c.timestamp = changeObj["timestamp"].toDouble(); for (auto edit : changeObj["edits"].toArray()) { auto editObj = edit.toObject(); auto type = editObj["type"].toString() == "insertion" ? FileDiff::EntryType::Insertion : FileDiff::EntryType::Deletion; auto line = editObj["line"].toInt(); auto text = editObj["text"].toString(); c.edits.append(FileDiff::Entry(type, line, text)); } h.changes.append(c); } _history.insert(file, h); } } History::History(const QString& uuid, QObject* parent): QObject(parent), _uuid(uuid) { } void History::addFile(const QString& file, const QString& contents) { FileHistory h; h.snapshot = contents; _history.insert(file, h); } QJsonObject History::toJSON() const { QJsonObject root; root["uuid"] = _uuid; root["parent"] = _parent.isEmpty() ? QJsonValue(QJsonValue::Type::Null) : QJsonValue(_parent); QJsonObject files; for (auto it = _history.begin(); it != _history.end(); it++) { auto& val = it.value(); auto file = _rootDir.relativeFilePath(it.key()); QJsonObject entry; entry["snapshot"] = val.snapshot; QJsonArray changes; for (auto& change : val.changes) { QJsonObject c; c["timestamp"] = change.timestamp; QJsonArray edits; for (auto& edit : change.edits) { QJsonObject e; e["type"] = edit.kind == FileDiff::EntryType::Insertion ? "insertion" : "deletion"; e["line"] = edit.line; e["text"] = edit.text; edits.append(e); } c["edits"] = edits; changes.append(c); } entry["changes"] = changes; files.insert(file, entry); } root["files"] = files; return root; } void History::updateFileContents(const QString& file, const QString& contents) { auto it = _history.find(file); if (it == _history.end()) { return; } FileDiff d(it.value().snapshot, contents); auto diff = d.diff(); if (diff.empty()) { return; } History::Change c; c.edits = diff; c.timestamp = static_cast(QDateTime::currentMSecsSinceEpoch()) / 1000.0; it.value().changes.append(std::move(c)); it.value().snapshot = contents; emit historyChanged(); } void History::commit() { for (auto it = _history.begin(); it != _history.end(); it++) { it.value().changes.clear(); } _parent = _uuid; _uuid = QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces); emit historyChanged(); } ================================================ FILE: MiniZincIDE/history.h ================================================ #ifndef HISTORY_H #define HISTORY_H #include #include #include #include #include #include #include class FileDiff { public: enum class EntryType { Insertion, Deletion }; struct Entry { EntryType kind; qsizetype line; QString text; Entry() {} Entry(EntryType _kind, qsizetype _line, const QString& _text): kind(_kind), line(_line), text(_text) {} }; FileDiff(const QString& origText, const QString& newText); QVector diff(); static QString apply(const QString& source, const QVector& diff); private: QStringList _orig; QStringList _new; qsizetype _offset; qsizetype _origLength; qsizetype _newLength; }; class History : public QObject { Q_OBJECT public: /// /// \brief Create a new history tracker /// \param doc The JSON history data /// \param parent The parent object /// explicit History(const QJsonObject& obj, const QDir& relativeTo, QObject* parent = nullptr); explicit History(const QString& uuid, QObject* parent = nullptr); /// /// \brief Output history as JSON /// \return The JSON document /// QJsonObject toJSON() const; void addFile(const QString& file, const QString& contents); signals: /// /// \brief Emitted when the history has changed /// void historyChanged(); public slots: /// /// \brief Update the history of the given file if it is currently being tracked /// \param file The file path /// \param contents The new file contents /// void updateFileContents(const QString& file, const QString& contents); /// /// \brief Remove stored changes and create new history node /// void commit(); private: struct Change { double timestamp; QVector edits; }; struct FileHistory { QString snapshot; QVector changes; }; QString _uuid; QString _parent; QMap _history; QDir _rootDir; }; #endif // HISTORY_H ================================================ FILE: MiniZincIDE/ide.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include "ide.h" #include "ideutils.h" #include "mainwindow.h" #include "ui_mainwindow.h" #include "checkupdatedialog.h" #ifdef MINIZINC_IDE_TESTING #include "tests/testide.h" #endif IDEStatistics::IDEStatistics(void) : errorsShown(0), errorsClicked(0), modelsRun(0) {} void IDEStatistics::init(QVariant v) { if (v.isValid()) { QMap m = v.toMap(); errorsShown = m["errorsShown"].toInt(); errorsClicked = m["errorsClicked"].toInt(); modelsRun = m["modelsRun"].toInt(); solvers = m["solvers"].toStringList(); } } QVariantMap IDEStatistics::toVariantMap(void) { QMap m; m["errorsShown"] = errorsShown; m["errorsClicked"] = errorsClicked; m["modelsRun"] = modelsRun; m["solvers"] = solvers; return m; } QByteArray IDEStatistics::toJson(void) { QJsonDocument jd(QJsonObject::fromVariantMap(toVariantMap())); return jd.toJson(); } void IDEStatistics::resetCounts(void) { errorsShown = 0; errorsClicked = 0; modelsRun = 0; } struct IDE::Doc { QTextDocument td; QSet editors; bool large; Doc() { td.setDocumentLayout(new QPlainTextDocumentLayout(&td)); } }; bool IDE::event(QEvent *e) { switch (e->type()) { case QEvent::FileOpen: { QString file = static_cast(e)->file(); if (file.endsWith(".mzp")) { PMap::iterator it = projects.find(file); if (it==projects.end()) { MainWindow* mw = qobject_cast(activeWindow()); if (mw==nullptr) { mw = new MainWindow(file); mw->show(); } else { mw->openProject(file); } } else { it.value()->raise(); it.value()->activateWindow(); } } else { MainWindow* curw = qobject_cast(activeWindow()); if (curw != nullptr && (curw->isEmptyProject() || curw==lastDefaultProject)) { curw->openFile(file); lastDefaultProject = curw; } else { QStringList files; files << file; MainWindow* mw = new MainWindow(files); if (curw!=nullptr) { QPoint p = curw->pos(); mw->move(p.x()+20, p.y()+20); } mw->show(); lastDefaultProject = mw; } } return true; } default: return QApplication::event(e); } } void IDE::checkUpdate(void) { QSettings settings; settings.sync(); settings.beginGroup("ide"); if (settings.value("checkforupdates21",false).toBool()) { QDate lastCheck = QDate::fromString(settings.value("lastCheck21", QDate::currentDate().addDays(-2).toString()).toString()); if (lastCheck < QDate::currentDate()) { // Prepare Google Analytics event QUrlQuery gaQuery; gaQuery.addQueryItem("v","1"); // version 1 of the protocol gaQuery.addQueryItem("tid","UA-63390311-1"); // the MiniZinc ID gaQuery.addQueryItem("cid",settings.value("uuid","unknown").toString()); // identifier for this installation gaQuery.addQueryItem("aip","1"); // anonymize IP address gaQuery.addQueryItem("t","event"); // feedback type gaQuery.addQueryItem("ec","check"); // event type gaQuery.addQueryItem("ea","checkUpdate"); // event action gaQuery.addQueryItem("el",applicationVersion()); // event label (IDE version) QNetworkRequest gaRequest(QUrl("http://www.google-analytics.com/collect")); gaRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); networkManager->post(gaRequest, gaQuery.toString().toLocal8Bit()); // Check if an update is available QNetworkRequest request(QUrl("http://www.minizinc.org/version-info.php")); versionCheckReply = networkManager->get(request); connect(versionCheckReply, &QNetworkReply::finished, this, &IDE::versionCheckFinished); } QTimer::singleShot(24*60*60*1000, this, SLOT(checkUpdate())); } settings.endGroup(); } IDE::IDE(int& argc, char* argv[]) : QApplication(argc,argv) { setApplicationVersion(MINIZINC_IDE_VERSION); setOrganizationName("MiniZinc"); setOrganizationDomain("minizinc.org"); #ifdef MINIZINC_IDE_TESTING setApplicationName("MiniZinc IDE Test Suite"); TestMocker::resetSettings(); #elif MINIZINC_IDE_BUNDLED setApplicationName("MiniZinc IDE (bundled)"); #else setApplicationName("MiniZinc IDE"); #endif networkManager = new QNetworkAccessManager(this); setAttribute(Qt::AA_UseHighDpiPixmaps); QSettings settings; settings.sync(); settings.beginGroup("ide"); if (settings.value("lastCheck21",QString()).toString().isEmpty()) { settings.setValue("uuid", QUuid::createUuid().toString()); CheckUpdateDialog cud; int result = cud.exec(); settings.setValue("lastCheck21",QDate::currentDate().addDays(-2).toString()); settings.setValue("checkforupdates21",result==QDialog::Accepted); settings.sync(); } bool wordWrap = settings.value("wordWrap", true).toBool(); settings.endGroup(); settings.beginGroup("Recent"); recentFiles = settings.value("files",QStringList()).toStringList(); recentProjects = settings.value("projects",QStringList()).toStringList(); settings.endGroup(); stats.init(settings.value("statistics")); lastDefaultProject = nullptr; darkModeNotifier = new DarkModeNotifier(this); themeManager = new ThemeManager(this); { // Load cheat sheet QString fileContents; QFile file(":/cheat_sheet.mzn"); if (file.open(QFile::ReadOnly)) { fileContents = file.readAll(); } else { qDebug() << "internal error: cannot open cheat sheet."; } QSettings settings; settings.beginGroup("MainWindow"); QFont defaultFont; defaultFont.setFamily("Menlo"); if (!defaultFont.exactMatch()) { defaultFont.setFamily("Consolas"); } if (!defaultFont.exactMatch()) { defaultFont.setFamily("Courier New"); } defaultFont.setStyleHint(QFont::TypeWriter); defaultFont.setPointSize(13); auto editorFont = IDEUtils::fontFromString(settings.value("editorFont").toString()); int zoom = settings.value("zoom", 100).toInt(); editorFont.setPointSize(editorFont.pointSize() * zoom / 100); bool darkMode = darkModeNotifier->darkMode(); auto themeIdx = settings.value("theme", 0).toInt(); themeManager->current(themeIdx); auto& theme = themeManager->current(); settings.endGroup(); cheatSheet = new QMainWindow; cheatSheet->setWindowTitle("MiniZinc Cheat Sheet"); CodeEditor* ce = new CodeEditor(nullptr,":/cheat_sheet.mzn",false,false,editorFont,2,false,theme,darkMode,nullptr,nullptr); ce->setWordWrapMode(wordWrap ? QTextOption::WrapAtWordBoundaryOrAnywhere : QTextOption::NoWrap); ce->document()->setPlainText(fileContents); QTextCursor cursor = ce->textCursor(); cursor.movePosition(QTextCursor::Start); ce->setTextCursor(cursor); ce->setReadOnly(true); cheatSheet->setCentralWidget(ce); cheatSheet->resize(800, 600); } connect(darkModeNotifier, &DarkModeNotifier::darkModeChanged, this, &IDE::onDarkModeChanged); onDarkModeChanged(darkModeNotifier->darkMode()); connect(&fsWatch, &QFileSystemWatcher::fileChanged, this, &IDE::fileModified); connect(this, &IDE::focusChanged, this, &IDE::handleFocusChange); connect(&modifiedTimer, &QTimer::timeout, this, &IDE::fileModifiedTimeout); #ifdef Q_OS_MAC MainWindow* mw = new MainWindow(QString()); const QMenuBar* mwb = mw->ui->menubar; defaultMenuBar = new QMenuBar(0); recentFilesMenu = new QMenu("Recent files"); recentProjectsMenu = new QMenu("Recent projects"); connect(recentFilesMenu, &QMenu::triggered, this, &IDE::recentFileMenuAction); connect(recentProjectsMenu, &QMenu::triggered, this, &IDE::recentProjectMenuAction); addRecentFile(""); addRecentProject(""); QList lst = mwb->children(); foreach (QObject* mo, lst) { if (QMenu* m = qobject_cast(mo)) { if (m->title()=="&File" || m->title()=="Help") { QMenu* nm = defaultMenuBar->addMenu(m->title()); foreach (QAction* a, m->actions()) { if (a->isSeparator()) { nm->addSeparator(); } else { if (a->text()=="Recent Files") { nm->addMenu(recentFilesMenu); } else if (a->text()=="Recent Projects") { nm->addMenu(recentProjectsMenu); } else { QAction* na = nm->addAction(a->text()); na->setShortcut(a->shortcut()); if (a==mw->ui->actionQuit) { connect(na, &QAction::triggered, this, &IDE::quit); } else if (a==mw->ui->actionNewModel_file || a==mw->ui->actionNew_project) { connect(na, &QAction::triggered, this, &IDE::newProject); } else if (a==mw->ui->actionOpen) { connect(na, &QAction::triggered, this, [=] () { openFile(); }); } else if (a==mw->ui->actionHelp) { connect(na, &QAction::triggered, this, &IDE::help); } else { na->setEnabled(false); } } } } } } } mainWindows.remove(mw); delete mw; #endif checkUpdate(); } #ifdef Q_OS_MAC void IDE::recentFileMenuAction(QAction* a) { if (a->text()=="Clear Menu") { recentFiles.clear(); recentFilesMenu->clear(); recentFilesMenu->addSeparator(); recentFilesMenu->addAction("Clear Menu"); } else { openFile(a->data().toString()); } } void IDE::recentProjectMenuAction(QAction* a) { if (a->text()=="Clear Menu") { IDE::instance()->recentProjects.clear(); recentProjectsMenu->clear(); recentProjectsMenu->addSeparator(); recentProjectsMenu->addAction("Clear Menu"); } else { openFile(a->data().toString()); } } #endif void IDE::handleFocusChange(QWidget *old, QWidget *newW) { if (old==nullptr && newW!=nullptr && !modifiedFiles.empty()) { fileModifiedTimeout(); } } void IDE::fileModifiedTimeout(void) { QSet modCopy = modifiedFiles; modifiedFiles.clear(); for (QSet::iterator s_it = modCopy.begin(); s_it != modCopy.end(); ++s_it) { DMap::iterator it = documents.find(*s_it); if (it != documents.end()) { QFileInfo fi(*s_it); QMessageBox msg; if (!fi.exists()) { msg.setText("The file "+fi.fileName()+" has been removed or renamed outside MiniZinc IDE."); msg.setStandardButtons(QMessageBox::Ok); } else { msg.setText("The file "+fi.fileName()+" has been modified outside MiniZinc IDE."); bool isModified = it.value()->td.isModified(); if (!isModified) { QFile newFile(fi.absoluteFilePath()); if (newFile.open(QFile::ReadOnly | QFile::Text)) { QString newFileContents = newFile.readAll(); isModified = (newFileContents != it.value()->td.toPlainText()); } else { isModified = true; } } if (isModified) { if (it.value()->td.isModified()) { msg.setInformativeText("Do you want to reload the file and discard your changes?"); } else { msg.setInformativeText("Do you want to reload the file?"); } QPushButton* cancelButton = msg.addButton(QMessageBox::Cancel); msg.addButton("Reload", QMessageBox::AcceptRole); msg.exec(); if (msg.clickedButton()==cancelButton) { it.value()->td.setModified(true); } else { QFile file(*s_it); if (file.open(QFile::ReadOnly | QFile::Text)) { QString contents = file.readAll(); it.value()->td.setPlainText(contents); it.value()->td.setModified(false); emit reloadedFile(*s_it, contents); } else { QMessageBox::warning(nullptr, "MiniZinc IDE", "Could not reload file "+*s_it, QMessageBox::Ok); it.value()->td.setModified(true); } } } } } } } void IDE::fileModified(const QString &f) { modifiedFiles.insert(f); if (activeWindow()!=nullptr) { modifiedTimer.setSingleShot(true); modifiedTimer.start(3000); } } void IDE::addRecentProject(const QString& p) { if (p != "") { recentProjects.removeAll(p); recentProjects.insert(0,p); while (recentProjects.size() > 12) recentProjects.pop_back(); } #ifdef Q_OS_MAC recentProjectsMenu->clear(); for (int i=0; iaddAction(recentProjects[i]); na->setData(recentProjects[i]); } recentProjectsMenu->addSeparator(); recentProjectsMenu->addAction("Clear Menu"); #endif } void IDE::addRecentFile(const QString& f) { if (f != "") { recentFiles.removeAll(f); recentFiles.insert(0,f); while (recentFiles.size() > 12) recentFiles.pop_back(); } #ifdef Q_OS_MAC recentFilesMenu->clear(); for (int i=0; iaddAction(recentFiles[i]); na->setData(recentFiles[i]); } recentFilesMenu->addSeparator(); recentFilesMenu->addAction("Clear Menu"); #endif } void IDE::newProject() { MainWindow* mw = new MainWindow(QString()); mw->show(); } QString IDE::getLastPath(void) { QSettings settings; settings.beginGroup("Path"); return settings.value("lastPath","").toString(); } void IDE::setLastPath(const QString& path) { QSettings settings; settings.beginGroup("Path"); settings.setValue("lastPath", path); settings.endGroup(); } void IDE::setEditorFont(QFont font) { for (auto* mw : qAsConst(mainWindows)) { mw->setEditorFont(font); } static_cast(cheatSheet->centralWidget())->setEditorFont(font); } void IDE::setEditorIndent(int indentSize, bool useTabs) { for (auto* mw : qAsConst(mainWindows)) { mw->setEditorIndent(indentSize, useTabs); } } void IDE::setEditorWordWrap(QTextOption::WrapMode mode) { for (auto* mw : qAsConst(mainWindows)) { mw->setEditorWordWrap(mode); } static_cast(cheatSheet->centralWidget())->setWordWrapMode(mode); } void IDE::openFile(const QString& fileName0) { QStringList fileNames; if (fileName0.isEmpty()) { fileNames = QFileDialog::getOpenFileNames(nullptr, tr("Open File"), getLastPath(), "MiniZinc Files (*.mzn *.dzn *.fzn *.json *.mzp *.mzc *.mpc);;Other (*)"); if (!fileNames.isEmpty()) { setLastPath(QFileInfo(fileNames.last()).absolutePath() + fileDialogSuffix); } } else { fileNames << fileName0; } if (!fileNames.isEmpty()) { MainWindow* mw = new MainWindow(fileNames); mw->show(); } } void IDE::help() { QDesktopServices::openUrl(QUrl(QString("http://docs.minizinc.dev/en/")+MINIZINC_IDE_VERSION+"/minizinc_ide.html")); } IDE::~IDE(void) { QSettings settings; settings.setValue("statistics",stats.toVariantMap()); settings.beginGroup("Recent"); settings.setValue("files",recentFiles); settings.setValue("projects",recentProjects); settings.endGroup(); settings.beginGroup("ide"); settings.endGroup(); } bool IDE::hasFile(const QString& path) { return documents.find(path) != documents.end(); } QTextDocument* IDE::addDocument(const QString& path, QTextDocument *doc, CodeEditor *ce) { Doc* d = new Doc; d->td.setDefaultFont(ce->font()); d->td.setPlainText(doc->toPlainText()); d->editors.insert(ce); d->large = false; documents.insert(path,d); fsWatch.addPath(path); return &d->td; } QPair IDE::loadFile(const QString& path, QWidget* parent) { DMap::iterator it = documents.find(path); if (it==documents.end()) { QFile file(path); if (file.open(QFile::ReadOnly | QFile::Text)) { Doc* d = new Doc; if ( (path.endsWith(".dzn") || path.endsWith(".fzn") || path.endsWith(".json")) && file.size() > 5*1024*1024) { d->large = true; } else { d->td.setPlainText(file.readAll()); d->large = false; } d->td.setModified(false); documents.insert(path,d); if (!d->large) fsWatch.addPath(path); return qMakePair(&d->td,d->large); } else { QMessageBox::warning(parent, "MiniZinc IDE", "Could not open file "+path, QMessageBox::Ok); QTextDocument* nd = nullptr; return qMakePair(nd,false); } } else { return qMakePair(&it.value()->td,it.value()->large); } } void IDE::loadLargeFile(const QString &path, QWidget* parent) { DMap::iterator it = documents.find(path); if (it.value()->large) { QFile file(path); if (file.open(QFile::ReadOnly | QFile::Text)) { QTextStream file_stream(&file); #if QT_VERSION < 0x060000 file_stream.setCodec("UTF-8"); #endif it.value()->td.setPlainText(file_stream.readAll()); it.value()->large = false; it.value()->td.setModified(false); QSet::iterator ed = it.value()->editors.begin(); for (; ed != it.value()->editors.end(); ++ed) { (*ed)->loadedLargeFile(); } fsWatch.addPath(path); } else { QMessageBox::warning(parent, "MiniZinc IDE", "Could not open file "+path, QMessageBox::Ok); } } } void IDE::registerEditor(const QString& path, CodeEditor* ce) { DMap::iterator it = documents.find(path); QSet& editors = it.value()->editors; editors.insert(ce); } void IDE::removeEditor(const QString& path, CodeEditor* ce) { DMap::iterator it = documents.find(path); if (it == documents.end()) { qDebug() << "internal error: document " << path << " not found"; } else { QSet& editors = it.value()->editors; editors.remove(ce); if (editors.empty()) { delete it.value(); documents.remove(path); fsWatch.removePath(path); } } } void IDE::renameFile(const QString& oldPath, const QString& newPath) { DMap::iterator it = documents.find(oldPath); if (it == documents.end()) { qDebug() << "internal error: document " << oldPath << " not found"; } else { Doc* doc = it.value(); documents.remove(oldPath); fsWatch.removePath(oldPath); documents.insert(newPath, doc); fsWatch.addPath(newPath); } } void IDE::versionCheckFinished(void) { if (versionCheckReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()==200) { QString currentVersion = versionCheckReply->readAll(); QRegularExpression versionRegExp("([1-9][0-9]*)\\.([0-9]+)\\.([0-9]+)"); int curVersionMajor = 0; int curVersionMinor = 0; int curVersionPatch = 0; bool ok = true; QRegularExpressionMatch curVersionMatch = versionRegExp.match(currentVersion); if (curVersionMatch.hasMatch()) { curVersionMajor = curVersionMatch.captured(1).toInt(&ok); if (ok) curVersionMinor = curVersionMatch.captured(2).toInt(&ok); if (ok) curVersionPatch = curVersionMatch.captured(3).toInt(&ok); } int appVersionMajor = 0; int appVersionMinor = 0; int appVersionPatch = 0; QRegularExpressionMatch appVersionMatch = versionRegExp.match(applicationVersion()); if (ok && appVersionMatch.hasMatch()) { appVersionMajor = appVersionMatch.captured(1).toInt(&ok); if (ok) appVersionMinor = appVersionMatch.captured(2).toInt(&ok); if (ok) appVersionPatch = appVersionMatch.captured(3).toInt(&ok); } bool needUpdate = ok && (curVersionMajor > appVersionMajor || (curVersionMajor==appVersionMajor && (curVersionMinor > appVersionMinor || (curVersionMinor==appVersionMinor && curVersionPatch > appVersionPatch)))); if (needUpdate) { int button = QMessageBox::information(nullptr,"Update available", "Version "+currentVersion+" of MiniZinc is now available. " "You are currently using version "+applicationVersion()+ ".\nDo you want to open the MiniZinc web site?", QMessageBox::Cancel|QMessageBox::Ok,QMessageBox::Ok); if (button==QMessageBox::Ok) { QDesktopServices::openUrl(QUrl("http://www.minizinc.org/")); } } QSettings settings; settings.beginGroup("ide"); settings.setValue("lastCheck21",QDate::currentDate().toString()); settings.endGroup(); stats.resetCounts(); } } QString IDE::appDir(void) const { #ifdef Q_OS_MAC return applicationDirPath()+"/../Resources/"; #else return applicationDirPath(); #endif } IDE* IDE::instance(void) { return static_cast(qApp); } void IDE::onDarkModeChanged(bool) { refreshTheme(); } void IDE::refreshTheme() { auto& theme = themeManager->current(); bool darkMode = darkModeNotifier->darkMode(); static_cast(cheatSheet->centralWidget())->setTheme(theme, darkMode); if (!darkModeNotifier->hasNativeDarkMode()) { // No native dark widgets, so use stylesheet instead if (darkMode) { QFile sheet(":/dark_mode.css"); sheet.open(QFile::ReadOnly); qApp->setStyleSheet(sheet.readAll()); } else { qApp->setStyleSheet(""); } } for (auto* mw : qAsConst(mainWindows)) { mw->setTheme(theme, darkMode); } } ================================================ FILE: MiniZincIDE/ide.h ================================================ #ifndef IDE_H #define IDE_H #include #include #include #include #include #include "codeeditor.h" #include "darkmodenotifier.h" #include "theme.h" #ifdef Q_OS_WIN #define pathSep ";" #define fileDialogSuffix "/" #define MZNOS "win" #else #define pathSep ":" #ifdef Q_OS_MAC #define fileDialogSuffix "/*" #define MZNOS "mac" #else #define fileDialogSuffix "/" #define MZNOS "linux" #endif #endif class MainWindow; class IDEStatistics { public: int errorsShown; int errorsClicked; int modelsRun; QStringList solvers; QVariantMap toVariantMap(void); IDEStatistics(void); void init(QVariant v); QByteArray toJson(void); void resetCounts(void); }; class IDE : public QApplication { Q_OBJECT public: IDE(int& argc, char* argv[]); ~IDE(void); struct Doc; typedef QMap DMap; DMap documents; typedef QMap PMap; PMap projects; QSet mainWindows; QStringList recentFiles; QStringList recentProjects; QSet modifiedFiles; QTimer modifiedTimer; IDEStatistics stats; MainWindow* lastDefaultProject; QMainWindow* cheatSheet; QNetworkAccessManager* networkManager; QNetworkReply* versionCheckReply; DarkModeNotifier* darkModeNotifier; ThemeManager* themeManager; #ifdef Q_OS_MAC QMenuBar* defaultMenuBar; QMenu* recentFilesMenu; QMenu* recentProjectsMenu; public slots: void recentFileMenuAction(QAction*); void recentProjectMenuAction(QAction*); public: #endif QFileSystemWatcher fsWatch; bool hasFile(const QString& path); QPair loadFile(const QString& path, QWidget* parent); void loadLargeFile(const QString& path, QWidget* parent); QTextDocument* addDocument(const QString& path, QTextDocument* doc, CodeEditor* ce); void registerEditor(const QString& path, CodeEditor* ce); void removeEditor(const QString& path, CodeEditor* ce); void renameFile(const QString& oldPath, const QString& newPath); QString appDir(void) const; static IDE* instance(void); QString getLastPath(void); void setLastPath(const QString& path); void setEditorFont(QFont font); void setEditorIndent(int indentSize, bool useTabs); void setEditorWordWrap(QTextOption::WrapMode mode); void addRecentFile(const QString& file); void addRecentProject(const QString& file); void refreshTheme(); protected: bool event(QEvent *); protected slots: void versionCheckFinished(void); void newProject(void); void openFile(const QString& filename = QString("")); void fileModified(const QString&); void fileModifiedTimeout(void); void handleFocusChange(QWidget*,QWidget*); void onDarkModeChanged(bool darkMode); public slots: void checkUpdate(void); void help(void); signals: void reloadedFile(const QString& file, const QString& contents); }; #endif // IDE_H ================================================ FILE: MiniZincIDE/ideutils.cpp ================================================ #include "ideutils.h" #include #include #include #include #include #include #include #include #include #include namespace IDEUtils { QString formatTime(qint64 time) { int hours = time / 3600000; int minutes = (time % 3600000) / 60000; int seconds = (time % 60000) / 1000; int msec = (time % 1000); QString elapsed; if (hours > 0) { elapsed += QString("%1h ").arg(hours); } if (hours > 0 || minutes > 0) { elapsed += QString("%1m ").arg(minutes); } if (hours > 0 || minutes > 0 || seconds > 0) { elapsed += QString("%1s").arg(seconds); } if (hours==0 && minutes==0) { if (seconds > 0) { elapsed += " "; } elapsed += QString("%1msec").arg(msec); } return elapsed; } bool isChildPath(const QString& parent, const QString& child) { return !QDir(parent).relativeFilePath(child).startsWith("."); } void watchChildChanges(QWidget* target, QObject* receiver, std::function action) { for (auto widget : target->findChildren()) { QCheckBox* checkBox; QLineEdit* lineEdit; QSpinBox* spinBox; QDoubleSpinBox* doubleSpinBox; QComboBox* comboBox; QGroupBox* groupBox; QTableWidget* tableWidget; QPlainTextEdit* plainTextEdit; if ((checkBox = qobject_cast(widget))) { QObject::connect(checkBox, &QCheckBox::stateChanged, receiver, [=] (int) { action(); }); } else if ((lineEdit = qobject_cast(widget))) { QObject::connect(lineEdit, &QLineEdit::textChanged, receiver, [=] (const QString&) { action(); }); } else if ((spinBox = qobject_cast(widget))) { QObject::connect(spinBox, QOverload::of(&QSpinBox::valueChanged), receiver, [=] (int) { action(); }); } else if ((doubleSpinBox = qobject_cast(widget))) { QObject::connect(doubleSpinBox, QOverload::of(&QDoubleSpinBox::valueChanged), receiver, [=] (double) { action(); }); } else if ((comboBox = qobject_cast(widget))) { QObject::connect(comboBox, QOverload::of(&QComboBox::currentIndexChanged), receiver, [=] (int) { action(); }); } else if ((groupBox = qobject_cast(widget))) { QObject::connect(groupBox, &QGroupBox::toggled, receiver, [=] (bool) { action(); }); } else if ((tableWidget = qobject_cast(widget))) { QObject::connect(tableWidget, &QTableWidget::cellChanged, receiver, [=] (int, int) { action(); }); } else if ((plainTextEdit = qobject_cast(widget))) { QObject::connect(plainTextEdit, &QPlainTextEdit::textChanged,receiver, [=] () { action(); }); } } } QFont fontFromString(const QString& s) { QFont font; if (!s.isEmpty() && font.fromString(s)) { return font; } QStringList families({"Menlo", "Consolas", "Courier New"}); font.setPointSize(13); font.setStyleHint(QFont::TypeWriter); for (auto& family : families) { font.setFamily(family); if (font.exactMatch()) { break; } } return font; } } ================================================ FILE: MiniZincIDE/ideutils.h ================================================ #ifndef IDEUTILS_H #define IDEUTILS_H #include #include #include #include #include namespace IDEUtils { QString formatTime(qint64 time); bool isChildPath(const QString& parent, const QString& child); void watchChildChanges(QWidget* target, QObject* receiver, std::function action); template void watchChildChanges(QWidget* target, T* receiver, void (T::* action)()) { watchChildChanges(target, receiver, std::bind(action, receiver)); } QFont fontFromString(const QString& s); class MimeDataExporter : public QTextEdit { public: QMimeData* md(void) const { QMimeData* mymd = createMimeDataFromSelection(); mymd->removeFormat("text/plain"); return mymd; } }; } #endif // IDEUTILS_H ================================================ FILE: MiniZincIDE/images/about.html ================================================

The MiniZinc IDE

Version $VERSION
Author: Guido Tack
Copyright Monash University and NICTA 2013, 2014, 2015
This program is provided under the terms of the Mozilla Public License Version 2.0. It uses the Qt toolkit, available from qt-project.org.
lala
================================================ FILE: MiniZincIDE/main.cpp ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "ide.h" #include "mainwindow.h" int main(int argc, char *argv[]) { IDE a(argc, argv); QStringList args = QApplication::arguments(); QStringList files; bool hadProject = false; for (int i=1; ishow(); hadProject = true; } else { files << args[i]; } } if (!hadProject) { MainWindow* w = new MainWindow(files); w->show(); } #ifdef Q_OS_MAC a.setQuitOnLastWindowClosed(false); #endif return a.exec(); } ================================================ FILE: MiniZincIDE/mainwindow.cpp ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include #include #include #include #include #include "mainwindow.h" #include "ui_mainwindow.h" #include "ide.h" #include "codeeditor.h" #include "fzndoc.h" #include "gotolinedialog.h" #include "paramdialog.h" #include "checkupdatedialog.h" #include "moocsubmission.h" #include "highlighter.h" #include "exception.h" #include "ideutils.h" #include "preferencesdialog.h" #include "cp-profiler/src/cpprofiler/execution.hh" #include "cp-profiler/src/cpprofiler/options.hh" #include #define BUILD_YEAR (__DATE__ + 7) MainWindow::MainWindow(const QString& project) : ui(new Ui::MainWindow), curEditor(nullptr), code_checker(nullptr), tmpDir(nullptr), saveBeforeRunning(false), processRunning(false) { init(project); } MainWindow::MainWindow(const QStringList& files) : ui(new Ui::MainWindow), curEditor(nullptr), code_checker(nullptr), tmpDir(nullptr), saveBeforeRunning(false), processRunning(false) { init(QString()); for (int i=0; imenuWindow->clear(); ui->menuWindow->addAction(minimizeAction); ui->menuWindow->addSeparator(); for (QSet::iterator it = IDE::instance()->mainWindows.begin(); it != IDE::instance()->mainWindows.end(); ++it) { QAction* windowAction = ui->menuWindow->addAction((*it)->windowTitle()); QVariant v = QVariant::fromValue(static_cast(*it)); windowAction->setData(v); windowAction->setCheckable(true); if (*it == this) { windowAction->setChecked(true); } } } void MainWindow::windowMenuSelected(QAction* a) { if (a==minimizeAction) { showMinimized(); } else { QMainWindow* mw = static_cast(a->data().value()); mw->showNormal(); mw->raise(); mw->activateWindow(); } } void MainWindow::init(const QString& projectFile) { code_checker = new CodeChecker(this); IDE::instance()->mainWindows.insert(this); ui->setupUi(this); ui->tabWidget->removeTab(0); #ifndef Q_OS_MAC ui->menuFile->addAction(ui->actionQuit); #endif setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); // initialise find widget ui->findFrame->hide(); ui->findWidget->installEventFilter(this); QWidget* solverConfFrame = new QWidget; QVBoxLayout* solverConfFrameLayout = new QVBoxLayout; solverConfCombo = new QComboBox; solverConfCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents); solverConfCombo->setMinimumWidth(100); QLabel* solverConfLabel = new QLabel("Solver configuration:"); QFont solverConfLabelFont = solverConfLabel->font(); solverConfLabelFont.setPointSizeF(solverConfLabelFont.pointSizeF()*0.9); solverConfLabel->setFont(solverConfLabelFont); solverConfFrameLayout->addWidget(solverConfLabel); solverConfFrameLayout->addWidget(solverConfCombo); solverConfFrame->setLayout(solverConfFrameLayout); QAction* solverConfComboAction = ui->toolBar->insertWidget(ui->actionSubmit_to_MOOC, solverConfFrame); auto runMenu = new QMenu(this); runMenu->addAction(ui->actionRun); runMenu->addAction(ui->actionCompile); runMenu->addAction(ui->actionProfile_compilation); runMenu->addAction(ui->actionProfile_search); runButton = new QToolButton; runButton->setDefaultAction(ui->actionRun); runButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); runButton->setPopupMode(QToolButton::MenuButtonPopup); runButton->setMenu(runMenu); ui->toolBar->insertWidget(solverConfComboAction, runButton); setAcceptDrops(true); setAttribute(Qt::WA_DeleteOnClose, true); minimizeAction = new QAction("&Minimize",this); minimizeAction->setShortcut(Qt::CTRL | Qt::Key_M); #ifdef Q_OS_MAC connect(ui->menuWindow, &QMenu::aboutToShow, this, &MainWindow::showWindowMenu); connect(ui->menuWindow, &QMenu::triggered, this, &MainWindow::windowMenuSelected); ui->menuWindow->addAction(minimizeAction); ui->menuWindow->addSeparator(); #else ui->menuWindow->hide(); ui->menubar->removeAction(ui->menuWindow->menuAction()); #endif QWidget* toolBarSpacer = new QWidget(); toolBarSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); ui->toolBar->insertWidget(ui->actionEditSolverConfig, toolBarSpacer); newFileCounter = 1; profileInfoVisible = false; paramDialog = new ParamDialog(this); fakeRunAction = new QAction(this); fakeRunAction->setShortcut(Qt::CTRL | Qt::Key_R); fakeRunAction->setEnabled(true); this->addAction(fakeRunAction); fakeCompileAction = new QAction(this); fakeCompileAction->setShortcut(Qt::CTRL | Qt::Key_B); fakeCompileAction->setEnabled(true); this->addAction(fakeCompileAction); fakeStopAction = new QAction(this); fakeStopAction->setShortcut(Qt::CTRL | Qt::Key_E); fakeStopAction->setEnabled(true); this->addAction(fakeStopAction); updateRecentProjects(""); updateRecentFiles(""); connect(ui->menuRecent_Files, &QMenu::triggered, this, &MainWindow::recentFileMenuAction); connect(ui->menuRecent_Projects, &QMenu::triggered, this, &MainWindow::recentProjectMenuAction); connect(ui->tabWidget, &QTabWidget::tabCloseRequested, this, &MainWindow::tabCloseRequest); connect(ui->tabWidget, &QTabWidget::currentChanged, this, &MainWindow::tabChange); progressBar = new QProgressBar; progressBar->setRange(0, 100); progressBar->setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Minimum); progressBar->setHidden(true); statusLabel = new QLabel(""); statusLineColLabel = new QLabel(""); ui->statusbar->addPermanentWidget(statusLabel); ui->statusbar->addPermanentWidget(progressBar); ui->statusbar->addWidget(statusLineColLabel); ui->actionStop->setEnabled(false); QTabBar* tb = ui->tabWidget->findChild(); tb->setTabButton(0, QTabBar::RightSide, 0); tb->setTabButton(0, QTabBar::LeftSide, 0); ui->actionSubmit_to_MOOC->setVisible(false); connect(ui->outputWidget, &OutputWidget::anchorClicked, this, &MainWindow::anchorClicked); QSettings settings; settings.beginGroup("MainWindow"); editorFont = IDEUtils::fontFromString(settings.value("editorFont").toString()); auto zoom = settings.value("zoom", 100).toInt(); editorFont.setPointSize(editorFont.pointSize() * zoom / 100); initTheme(); ui->outputWidget->setBrowserFont(editorFont); resize(settings.value("size", QSize(800, 600)).toSize()); move(settings.value("pos", QPoint(100, 100)).toPoint()); auto frameGeom = frameGeometry(); bool positionValid = false; for (auto* screen: QGuiApplication::screens()) { auto geom = screen->availableGeometry(); if (geom.intersects(frameGeom)) { positionValid = true; break; } } if (!positionValid) { // Window is outside of view resize(800, 600); move(100, 100); } if (settings.value("toolbarHidden", false).toBool()) { on_actionHide_tool_bar_triggered(); } if (settings.value("outputWindowHidden", true).toBool()) { on_actionOnly_editor_triggered(); } ui->check_wrap->setChecked(settings.value("findWrapAround", false).toBool()); ui->check_re->setChecked(settings.value("findRegularExpression", false).toBool()); ui->check_case->setChecked(settings.value("findCaseSensitive", false).toBool()); settings.endGroup(); settings.beginGroup("ide"); indentSize = settings.value("indentSize", 2).toInt(); useTabs = settings.value("indentTabs", false).toBool(); settings.endGroup(); IDE::instance()->setEditorFont(editorFont); settings.beginGroup("minizinc"); QString mznDistribPath = settings.value("mznpath","").toString(); settings.endGroup(); auto& driver = MznDriver::get(); if (!driver.isValid()) { try { driver.setLocation(mznDistribPath); } catch (Exception& e) { int ret = QMessageBox::warning(this, "MiniZinc IDE", e.message() + "\nDo you want to open the settings dialog?", QMessageBox::Ok | QMessageBox::Cancel); if (ret == QMessageBox::Ok) on_actionManage_solvers_triggered(); } } ui->config_window->init(); connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &MainWindow::onClipboardChanged); ui->projectExplorerDockWidget->hide(); ui->configWindow_dockWidget->hide(); project = new Project(ui->config_window->solverConfigs(), this); connect(ui->tabWidget, &QTabWidget::currentChanged, [=] (int i) { project->openTabsChanged(getOpenFiles(), i); }); connect(ui->config_window, &ConfigWindow::selectedSolverConfigurationChanged, project, &Project::activeSolverConfigChanged); connect(ui->config_window, &ConfigWindow::configSaved, [=] (const QString& f) { openFile(f); }); ui->projectBrowser->project(project); connect(project, &Project::moocChanged, this, &MainWindow::on_moocChanged); ui->actionNext_tab->setShortcuts(QKeySequence::NextChild); ui->actionPrevious_tab->setShortcuts(QKeySequence::PreviousChild); if (!projectFile.isEmpty()) { loadProject(projectFile); setLastPath(QFileInfo(projectFile).absolutePath()+fileDialogSuffix); } else { createEditor("Playground",false,true); if (getLastPath().isEmpty()) { setLastPath(QDir::currentPath()+fileDialogSuffix); } } getProject().setModified(false); ui->cpprofiler_dockWidget->hide(); } void MainWindow::updateUiProcessRunning(bool pr) { processRunning = pr; if (processRunning) { fakeRunAction->setEnabled(true); ui->actionRun->setEnabled(false); ui->actionProfile_compilation->setEnabled(false); ui->actionProfile_search->setEnabled(false); fakeCompileAction->setEnabled(true); ui->actionCompile->setEnabled(false); fakeStopAction->setEnabled(false); ui->actionStop->setEnabled(true); runButton->removeAction(ui->actionRun); runButton->setDefaultAction(ui->actionStop); ui->actionSubmit_to_MOOC->setEnabled(false); } else { bool isPlayground = false; bool isMzn = false; bool isFzn = false; bool isData = false; if (curEditor) { isPlayground = curEditor->filepath==""; isMzn = (isPlayground && !curEditor->filename.endsWith(".fzn")) || QFileInfo(curEditor->filepath).suffix()=="mzn"; isFzn = QFileInfo(curEditor->filepath).suffix()=="fzn" || curEditor->filename.endsWith(".fzn"); isData = QFileInfo(curEditor->filepath).suffix()=="dzn" || QFileInfo(curEditor->filepath).suffix()=="json"; } fakeRunAction->setEnabled(! (isMzn || isFzn || isData)); ui->actionRun->setEnabled(isMzn || isFzn || isData); ui->actionProfile_compilation->setEnabled(isMzn || isData); ui->actionProfile_search->setEnabled(isMzn || isFzn || isData); fakeCompileAction->setEnabled(!(isMzn||isData)); ui->actionCompile->setEnabled(isMzn||isData); fakeStopAction->setEnabled(true); ui->actionStop->setEnabled(false); runButton->removeAction(ui->actionStop); runButton->setDefaultAction(ui->actionRun); ui->actionSubmit_to_MOOC->setEnabled(true); } } MainWindow::~MainWindow() { delete code_checker; delete server; for (int i=0; iterminate(); delete cleanupProcesses[i]; } delete project; delete ui; } void MainWindow::on_actionNewModel_file_triggered() { createEditor(".mzn",false,true); } void MainWindow::on_actionNewData_file_triggered() { createEditor(".dzn",false,true); } void MainWindow::createEditor(const QString& path, bool openAsModified, bool isNewFile, bool readOnly, bool focus) { QTextDocument* doc = nullptr; bool large = false; QString fileContents; QString absPath = QFileInfo(path).canonicalFilePath(); if (isNewFile && path == "Playground") { absPath = path; fileContents = "% Use this editor as a MiniZinc scratch book\n"; openAsModified = false; } else if (isNewFile && path.startsWith(".")) { absPath = QString("Untitled")+QString().setNum(newFileCounter++)+path; } else if (path.isEmpty()) { absPath = path; // Do nothing } else if (openAsModified) { QFile file(path); if (file.open(QFile::ReadOnly)) { fileContents = file.readAll(); } else { QMessageBox::warning(this,"MiniZinc IDE", "Could not open file "+path, QMessageBox::Ok); return; } if (isNewFile) { absPath = QFileInfo(path).fileName(); } } else { if (absPath.isEmpty()) { QMessageBox::warning(this,"MiniZinc IDE", "Could not open file "+path, QMessageBox::Ok); return; } QPair d = IDE::instance()->loadFile(absPath,this); updateRecentFiles(absPath); doc = d.first; large = d.second; } if (doc || !fileContents.isEmpty() || isNewFile) { int closeTab = -1; if (!isNewFile && ui->tabWidget->count()==1) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(0)); if (ce && ce->filepath == "" && !ce->document()->isModified()) { closeTab = 0; } } auto& theme = IDE::instance()->themeManager->current(); CodeEditor* ce = new CodeEditor(doc,absPath,isNewFile,large,editorFont,indentSize,useTabs,theme,darkMode,ui->tabWidget,this); if (readOnly || ce->filename == "_coursera" || ce->filename.endsWith(".mzc")) ce->setReadOnly(true); int tab = ui->tabWidget->addTab(ce, ce->filename); if(profileInfoVisible) ce->showDebugInfo(profileInfoVisible); if (focus) { ui->tabWidget->setCurrentIndex(tab); curEditor->setFocus(); } if (!fileContents.isEmpty()) { curEditor->filepath = ""; curEditor->document()->setPlainText(fileContents); curEditor->document()->setModified(openAsModified); tabChange(ui->tabWidget->currentIndex()); auto* history = getProject().history(); if (history != nullptr) { history->updateFileContents(absPath, fileContents); } } else if (doc) { if (!absPath.endsWith(".fzn")) { getProject().add(absPath); auto* history = getProject().history(); if (history != nullptr) { history->updateFileContents(absPath, doc->toPlainText()); } } IDE::instance()->registerEditor(absPath,curEditor); } if (closeTab >= 0) tabCloseRequest(closeTab); } } void MainWindow::setLastPath(const QString &s) { IDE::instance()->setLastPath(s); } QString MainWindow::getLastPath(void) { return IDE::instance()->getLastPath(); } void MainWindow::openFile(const QString &path, bool openAsModified, bool focus) { QStringList fileNames; if (path.isEmpty()) { fileNames = QFileDialog::getOpenFileNames(this, tr("Open File"), getLastPath(), "MiniZinc Files (*.mzn *.dzn *.fzn *.json *.mzp *.mzc *.mpc);;Other (*)"); if (!fileNames.isEmpty()) { setLastPath(QFileInfo(fileNames.last()).absolutePath() + fileDialogSuffix); } } else { fileNames << path; } for (auto& fileName : fileNames) { if (fileName.endsWith(".mzp")) { openProject(fileName); } else if (fileName.endsWith(".mpc")) { QString absPath = QFileInfo(fileName).canonicalFilePath(); if (ui->config_window->addConfig(absPath)) { getProject().add(absPath); ui->configWindow_dockWidget->setVisible(true); } } else { createEditor(fileName, openAsModified, false, false, focus); } } } void MainWindow::tabCloseRequest(int tab) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(tab)); if (ce) { if (ce->document()->isModified()) { QMessageBox msg; msg.setText("The document has been modified."); msg.setInformativeText("Do you want to save your changes?"); msg.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); msg.setDefaultButton(QMessageBox::Save); int ret = msg.exec(); switch (ret) { case QMessageBox::Save: saveFile(ce,ce->filepath); if (ce->document()->isModified()) return; break; case QMessageBox::Discard: break; case QMessageBox::Cancel: return; default: return; } } ce->document()->setModified(false); if (!ce->filepath.isEmpty()) { IDE::instance()->removeEditor(ce->filepath,ce); } if (ce == curEditor) { curEditor = nullptr; } ce->deleteLater(); } else { ui->tabWidget->removeTab(tab); } getProject().openTabsChanged(getOpenFiles(), ui->tabWidget->currentIndex()); } void MainWindow::closeEvent(QCloseEvent* e) { // make sure any modifications in solver configurations are saved // on_conf_solver_conf_currentIndexChanged(ui->conf_solver_conf->currentIndex()); bool modified = false; for (int i=0; itabWidget->count(); i++) { auto ce = qobject_cast(ui->tabWidget->widget(i)); if (ce && ce->document()->isModified()) { modified = true; break; } } if (modified) { int ret = QMessageBox::warning(this, "MiniZinc IDE", "There are modified documents.\nDo you want to discard the changes or cancel?", QMessageBox::Discard | QMessageBox::Cancel); if (ret == QMessageBox::Cancel) { e->ignore(); return; } } for (auto& sc : ui->config_window->solverConfigs()) { if (sc->modified) { int ret = QMessageBox::warning(this, "MiniZinc IDE", "There are modified solver configurations.\nDo you want to discard the changes or cancel?", QMessageBox::Discard | QMessageBox::Cancel); if (ret == QMessageBox::Cancel) { e->ignore(); return; } break; } } if (getProject().isModified()) { int ret = QMessageBox::warning(this, "MiniZinc IDE", "The project has been modified.\nDo you want to discard the changes or cancel?", QMessageBox::Discard | QMessageBox::Cancel); if (ret == QMessageBox::Cancel) { e->ignore(); return; } } if (processRunning) { int ret = QMessageBox::warning(this, "MiniZinc IDE", "MiniZinc is currently running a solver.\nDo you want to quit anyway and stop the current process?", QMessageBox::Yes| QMessageBox::No); if (ret == QMessageBox::No) { e->ignore(); return; } } // At this point we're definitely closing emit terminating(); for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce) { ce->setDocument(nullptr); ce->filepath = ""; // if (ce->filepath != "") { // TODO: How is this possible? // IDE::instance()->removeEditor(ce->filepath,ce); // } } } if (getProject().hasProjectFile()) { IDE::instance()->projects.remove(getProject().projectFile()); } IDE::instance()->mainWindows.remove(this); QSettings settings; settings.beginGroup("MainWindow"); settings.setValue("darkMode", darkMode); settings.setValue("size", size()); settings.setValue("pos", pos()); settings.setValue("toolbarHidden", ui->toolBar->isHidden()); settings.setValue("outputWindowHidden", ui->outputDockWidget->isHidden()); settings.setValue("findWrapAround", ui->check_wrap->isChecked()); settings.setValue("findRegularExpression", ui->check_re->isChecked()); settings.setValue("findCaseSensitive", ui->check_case->isChecked()); settings.endGroup(); e->accept(); } void MainWindow::dragEnterEvent(QDragEnterEvent* event) { if (event->mimeData()->hasFormat("text/uri-list")) event->acceptProposedAction(); } void MainWindow::dropEvent(QDropEvent* event) { const QMimeData* mimeData = event->mimeData(); if (mimeData->hasUrls()) { QList urlList = mimeData->urls(); for (int i=0; iacceptProposedAction(); } void MainWindow::tabChange(int tab) { if (curEditor) { disconnect(ui->actionCopy, &QAction::triggered, curEditor, &CodeEditor::copy); disconnect(ui->actionPaste, &QAction::triggered, curEditor, &CodeEditor::paste); disconnect(ui->actionCut, &QAction::triggered, curEditor, &CodeEditor::cut); disconnect(ui->actionSelect_All, &QAction::triggered, curEditor, &CodeEditor::selectAll); disconnect(ui->actionUndo, &QAction::triggered, curEditor, &CodeEditor::undo); disconnect(ui->actionRedo, &QAction::triggered, curEditor, &CodeEditor::redo); disconnect(curEditor, &CodeEditor::copyAvailable, ui->actionCopy, &QAction::setEnabled); disconnect(curEditor, &CodeEditor::copyAvailable, ui->actionCut, &QAction::setEnabled); disconnect(curEditor->document(), &QTextDocument::modificationChanged, this, &MainWindow::setWindowModified); disconnect(curEditor->document(), &QTextDocument::undoAvailable, ui->actionUndo, &QAction::setEnabled); disconnect(curEditor->document(), &QTextDocument::redoAvailable, ui->actionRedo, &QAction::setEnabled); disconnect(curEditor, &CodeEditor::cursorPositionChanged, this, &MainWindow::editor_cursor_position_changed); disconnect(code_checker, &CodeChecker::finished, curEditor, &CodeEditor::checkFile); disconnect(curEditor, &CodeEditor::changedDebounced, this, &MainWindow::check_code); } curEditor = tab == -1 ? nullptr : qobject_cast(ui->tabWidget->widget(tab)); if (!curEditor) { setEditorMenuItemsEnabled(false); ui->findFrame->hide(); } else { setEditorMenuItemsEnabled(true); connect(ui->actionCopy, &QAction::triggered, curEditor, &CodeEditor::copy); connect(ui->actionPaste, &QAction::triggered, curEditor, &CodeEditor::paste); connect(ui->actionCut, &QAction::triggered, curEditor, &CodeEditor::cut); connect(ui->actionSelect_All, &QAction::triggered, curEditor, &CodeEditor::selectAll); connect(ui->actionUndo, &QAction::triggered, curEditor, &CodeEditor::undo); connect(ui->actionRedo, &QAction::triggered, curEditor, &CodeEditor::redo); connect(curEditor, &CodeEditor::copyAvailable, ui->actionCopy, &QAction::setEnabled); connect(curEditor, &CodeEditor::copyAvailable, ui->actionCut, &QAction::setEnabled); connect(curEditor->document(), &QTextDocument::modificationChanged, this, &MainWindow::setWindowModified); connect(curEditor->document(), &QTextDocument::undoAvailable, ui->actionUndo, &QAction::setEnabled); connect(curEditor->document(), &QTextDocument::redoAvailable, ui->actionRedo, &QAction::setEnabled); connect(curEditor, &CodeEditor::cursorPositionChanged, this, &MainWindow::editor_cursor_position_changed); connect(code_checker, &CodeChecker::finished, curEditor, &CodeEditor::checkFile); connect(curEditor, &CodeEditor::changedDebounced, this, &MainWindow::check_code); setWindowModified(curEditor->document()->isModified()); QString p; p += " "; p += QChar(0x2014); p += " "; if (getProject().hasProjectFile()) { QFileInfo fi(getProject().projectFile()); p += "Project: " + fi.baseName(); } else { p += "Untitled Project"; } if (curEditor->filepath.isEmpty()) { setWindowFilePath(curEditor->filename); setWindowTitle(curEditor->filename+p+"[*]"); } else { setWindowFilePath(curEditor->filepath); setWindowTitle(curEditor->filename+p+"[*]"); bool haveChecker = false; if (curEditor->filename.endsWith(".mzn")) { QString checkFile = curEditor->filepath; checkFile.replace(checkFile.length()-1,1,"c"); haveChecker = getProject().contains(checkFile) || getProject().contains(checkFile+".mzn"); } QSettings settings; settings.beginGroup("ide"); bool checkSolutions = haveChecker && settings.value("checkSolutions", true).toBool(); if (checkSolutions) { ui->actionRun->setText("Run + check"); } else { ui->actionRun->setText("Run"); } } ui->actionSave->setEnabled(true); ui->actionSave_as->setEnabled(true); ui->actionSelect_All->setEnabled(true); ui->actionUndo->setEnabled(curEditor->document()->isUndoAvailable()); ui->actionRedo->setEnabled(curEditor->document()->isRedoAvailable()); updateUiProcessRunning(processRunning); ui->actionFind->setEnabled(true); ui->actionFind_next->setEnabled(true); ui->actionFind_previous->setEnabled(true); ui->actionReplace->setEnabled(true); ui->actionShift_left->setEnabled(true); ui->actionShift_right->setEnabled(true); curEditor->setFocus(); MainWindow::check_code(); } updateProfileSearchButton(); } void MainWindow::on_actionClose_triggered() { int tab = ui->tabWidget->currentIndex(); tabCloseRequest(tab); } void MainWindow::on_actionOpen_triggered() { openFile(QString()); } void MainWindow::addOutput(const QString& s, bool html) { if (html) { ui->outputWidget->addHtml(s); } else { ui->outputWidget->addText(s); } } void MainWindow::on_actionRun_triggered() { compileOrRun(CM_RUN); } void MainWindow::compileOrRun( CompileMode cm, const SolverConfiguration* sc, const QString& model, const QStringList& data, const QString& checker, const QStringList& extraArgs) { if (!promptSaveModified()) { return; } if (curEditor != nullptr && curEditor->filepath.endsWith(".mzc.mzn") && cm == CM_COMPILE) { // Compile the current checker compileSolutionChecker(curEditor->filepath); return; } // Use either the provided solver config, or the currently active one auto solverConfig = sc ? sc : getCurrentSolverConfig(); if (!solverConfig) { // There is no solver config return; } // Use either the model given, or the currently opened model, or prompt for one auto modelFile = model.isEmpty() ? currentModelFile() : model; if (modelFile.isEmpty()) { // There is no model file at all return; } auto dataFiles = data; auto checkerFile = checker; auto extraArguments = extraArgs; QStringList inlineData; // If we didn't specify any data but the current file is a data file, use it if (dataFiles.isEmpty() && curEditor && (curEditor->filepath.endsWith(".dzn") || curEditor->filepath.endsWith(".json"))) { dataFiles << curEditor->filepath; } // Prompt for data if necessary if (!getModelParameters(*solverConfig, modelFile, dataFiles, extraArguments, inlineData)) { return; } // If we didn't specify a checker but the current file is one, use it if (checkerFile.isEmpty() && curEditor && (curEditor->filepath.endsWith(".mzc") || curEditor->filepath.endsWith(".mzc.mzn"))) { checkerFile = curEditor->filepath; } // Use checker that matches model file if it exists and checking is on QSettings settings; settings.beginGroup("ide"); if (settings.value("clearOutput", false).toBool()) { on_actionClear_output_triggered(); } on_actionSplit_triggered(); if (checkerFile.isEmpty() && settings.value("checkSolutions", true).toBool()) { auto mzc = modelFile; mzc.replace(mzc.length() - 1, 1, "c"); if (getProject().contains(mzc)) { checkerFile = mzc; } else if (getProject().contains(mzc + ".mzn")) { checkerFile = mzc + ".mzn"; } } settings.endGroup(); if (!checkerFile.isEmpty()) { dataFiles << checkerFile; } if (solverConfig->solverDefinition.stdFlags.contains("--output-html")) { extraArguments << "--output-html"; } // Compile/run switch (cm) { case CM_RUN: run(*solverConfig, modelFile, dataFiles, extraArguments, inlineData); break; case CM_COMPILE: compile(*solverConfig, modelFile, dataFiles, extraArguments, inlineData); break; case CM_PROFILE: compile(*solverConfig, modelFile, dataFiles, extraArguments, inlineData, true); break; } } bool MainWindow::getModelParameters(const SolverConfiguration& sc, const QString& model, QStringList& data, const QStringList& extraArgs, QStringList& inlineData) { if (!requireMiniZinc()) { return false; } // Get model interface to obtain parameters QStringList args; args << "-c" << "--model-interface-only" << data << model << extraArgs; MznProcess p; // Passing all flags can break --model-inteface-only, so just pass the necessary ones SolverConfiguration checkConfig(sc.solverDefinition); checkConfig.additionalData = sc.additionalData; checkConfig.extraOptions = sc.extraOptions; try { auto result = p.run(checkConfig, args, QFileInfo(model).absolutePath()); QStringList additionalDataFiles = data; if (result.exitCode == 0) { auto jdoc = QJsonDocument::fromJson(result.stdOut.toUtf8()); if (jdoc.isObject() && jdoc.object()["input"].isObject() && jdoc.object()["method"].isString()) { QJsonObject inputArgs = jdoc.object()["input"].toObject(); QStringList undefinedArgs = inputArgs.keys(); if (undefinedArgs.size() > 0) { QStringList params; paramDialog->getParams(undefinedArgs, getProject().dataFiles(), params, additionalDataFiles); if (additionalDataFiles.isEmpty()) { if (params.size()==0) { return false; } for (int i=0; ioutputWidget->errorCharFormat()); ui->outputWidget->addText(result.stdErr); QMessageBox::critical(this, "Internal error", "Could not determine model parameters"); return false; } } for (const auto& d : additionalDataFiles) { if (!data.contains(d)) { // Ensure we don't add the same data file twice data << d; } } return true; } catch (ProcessError& e) { QMessageBox::critical(this, "MiniZinc IDE", e.message()); return false; } } QString MainWindow::currentModelFile() { if (curEditor) { if (curEditor->filepath.endsWith(".fzn") || (curEditor->filepath.endsWith(".mzn") && !curEditor->filepath.endsWith(".mzc.mzn"))) { // The current file is a model return curEditor->filepath; } if (curEditor->filepath.isEmpty()) { // The current file is a playground buffer QTemporaryDir* modelTmpDir = new QTemporaryDir; if (!modelTmpDir->isValid()) { throw InternalError("Could not create temporary directory for compilation."); } else { cleanupTmpDirs.append(modelTmpDir); QString model = modelTmpDir->path() + "/untitled_model." + (curEditor->filename.endsWith(".fzn") ? "fzn" : "mzn"); QFile modelFile(model); if (modelFile.open(QIODevice::ReadWrite)) { QTextStream ts(&modelFile); #if QT_VERSION < 0x060000 ts.setCodec("UTF-8"); #endif ts << curEditor->document()->toPlainText(); modelFile.close(); } else { throw InternalError("Could not write temporary model file."); } curEditor->playgroundTempFile = model; return model; } } } auto modelFiles = getProject().modelFiles(); if (modelFiles.count() == 0) { // There are no model files return ""; } if (modelFiles.count() == 1) { return modelFiles[0]; } int selectedModel = paramDialog->getModel(modelFiles); if (selectedModel == -1) { return ""; } return modelFiles[selectedModel]; } bool MainWindow::promptSaveModified() { QVector modifiedDocs; for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce && !ce->filepath.isEmpty() && ce->document()->isModified()) { modifiedDocs << ce; } } // Prompt for saving modified files if (!modifiedDocs.empty()) { if (!saveBeforeRunning) { QMessageBox msgBox; if (modifiedDocs.size()==1) { msgBox.setText("One of the files is modified. You have to save it before running."); } else { msgBox.setText("Several files have been modified. You have to save them before running."); } msgBox.setInformativeText("Do you want to save now and then run?"); QAbstractButton *saveButton = msgBox.addButton(QMessageBox::Save); msgBox.addButton(QMessageBox::Cancel); QAbstractButton *alwaysButton = msgBox.addButton("Always save", QMessageBox::AcceptRole); msgBox.setDefaultButton(QMessageBox::Save); msgBox.exec(); if (msgBox.clickedButton()==alwaysButton) { saveBeforeRunning = true; } if (msgBox.clickedButton()!=saveButton && msgBox.clickedButton()!=alwaysButton) { return false; } } for (auto ce : modifiedDocs) { saveFile(ce,ce->filepath); if (ce->document()->isModified()) { return false; } } } return true; } QString MainWindow::setElapsedTime(qint64 elapsed_t) { auto elapsed = IDEUtils::formatTime(elapsed_t); QString timeLimit; auto sc = getCurrentSolverConfig(); if (sc && sc->timeLimit > 0) { timeLimit += " / "; timeLimit += IDEUtils::formatTime(sc->timeLimit); } statusLabel->setText(elapsed + timeLimit); return elapsed; } void MainWindow::statusTimerEvent(qint64 time) { QString txt = "Running."; int dots = time / 5000000000; for (int i = 0; i < dots; i++) { txt += "."; } ui->statusbar->showMessage(txt); setElapsedTime(time); } void MainWindow::compileSolutionChecker(const QString& checker) { if (!requireMiniZinc()) { return; } QFileInfo fi(checker); QSettings settings; settings.beginGroup("ide"); bool printCommand = settings.value("printCommand", false).toBool(); settings.endGroup(); auto proc = new MznProcess(this); QStringList args; args << "--compile-solution-checker" << checker; connect(this, &MainWindow::terminating, proc, [=] () { proc->terminate(); proc->deleteLater(); }); connect(ui->actionStop, &QAction::triggered, proc, [=] () { ui->actionStop->setDisabled(true); proc->stop(); ui->outputWidget->addText("Stopped."); }); connect(proc, &MznProcess::success, [=] (bool cancelled) { if (!cancelled) { openCompiledFzn(checker.left(checker.size() - 4)); } procFinished(0, proc->elapsedTime()); ui->outputWidget->endExecution(0, proc->elapsedTime()); }); connect(proc, &MznProcess::errorOutput, this, &MainWindow::on_minizincError); connect(proc, &MznProcess::warningOutput, this, &MainWindow::on_minizincError); connect(proc, &MznProcess::outputStdError, this, [=] (const QString& d) { QTextCharFormat f; f.setForeground(IDE::instance()->themeManager->current().commentColor.get(darkMode)); ui->outputWidget->addText(d, f, "Standard Error"); }); connect(proc, &MznProcess::finished, [=] () { proc->deleteLater(); }); connect(proc, &MznProcess::failure, [=](int exitCode, MznProcess::FailureType e) { if (e == MznProcess::FailedToStart) { QMessageBox::critical(this, "MiniZinc IDE", "Failed to start MiniZinc. Check your path settings."); exitCode = 0; } else if (e != MznProcess::NonZeroExit) { QMetaEnum metaEnum = QMetaEnum::fromType(); QMessageBox::critical(this, "MiniZinc IDE", "Unknown error while executing MiniZinc: " + QString(metaEnum.valueToKey(e))); } ui->outputWidget->endExecution(0, proc->elapsedTime()); procFinished(exitCode, proc->elapsedTime()); }); connect(proc, &MznProcess::timeUpdated, this, &MainWindow::statusTimerEvent); updateUiProcessRunning(true); ui->outputWidget->startExecution(QString("Compiling %1").arg(fi.fileName())); proc->start(args, fi.canonicalPath()); if (printCommand) { auto cmdMessage = QString("Command: %1\n").arg(proc->command()); ui->outputWidget->addText(cmdMessage, ui->outputWidget->infoCharFormat(), "Commands"); } } void MainWindow::compile(const SolverConfiguration& sc, const QString& model, const QStringList& data, const QStringList& extraArgs, const QStringList& inlineData, bool profile) { if (!requireMiniZinc()) { return; } QFileInfo fi(model); QSettings settings; settings.beginGroup("ide"); bool printCommand = settings.value("printCommand", false).toBool(); settings.endGroup(); QTemporaryDir* fznTmpDir = new QTemporaryDir; if (!fznTmpDir->isValid()) { QMessageBox::critical(this, "MiniZinc IDE", "Could not create temporary directory for compilation."); return; } cleanupTmpDirs.append(fznTmpDir); QString fzn = fznTmpDir->path() + "/" + fi.baseName() + ".fzn"; auto proc = new MznProcess(this); QStringList args; args << "-c" << "-o" << fzn << model << data << extraArgs; for (auto dzn : inlineData) { args << "-D" << dzn; } if (profile) { args << "--output-paths-to-stdout" << "--output-detailed-timing"; } connect(this, &MainWindow::terminating, proc, [=] () { proc->terminate(); proc->deleteLater(); }); connect(ui->actionStop, &QAction::triggered, proc, [=] () { ui->actionStop->setDisabled(true); proc->stop(); ui->outputWidget->addText("Stopped."); }); if (profile) { auto* timing = new QVector; auto* paths = new QVector; connect(proc, &MznProcess::profilingOutput, [=] (const QVector& t) { *timing << t; }); connect(proc, &MznProcess::pathsOutput, [=] (const QVector& p) { *paths << p; }); connect(proc, &MznProcess::success, [=] (bool cancelled) { profileCompiledFzn(*timing, *paths); procFinished(0, proc->elapsedTime()); }); connect(proc, &MznProcess::finished, [=] () { delete timing; delete paths; }); } else { connect(proc, &MznProcess::success, [=] (bool cancelled) { if (!cancelled) { openCompiledFzn(fzn); } procFinished(0, proc->elapsedTime()); ui->outputWidget->endExecution(0, proc->elapsedTime()); }); } connect(proc, &MznProcess::statisticsOutput, ui->outputWidget, &OutputWidget::addStatistics); connect(proc, &MznProcess::errorOutput, this, &MainWindow::on_minizincError); connect(proc, &MznProcess::warningOutput, this, &MainWindow::on_minizincError); connect(proc, &MznProcess::progressOutput, this, &MainWindow::on_progressOutput); connect(proc, &MznProcess::outputStdError, this, [=] (const QString& d) { QTextCharFormat f; f.setForeground(IDE::instance()->themeManager->current().commentColor.get(darkMode)); ui->outputWidget->addText(d, f, "Standard Error"); }); connect(proc, &MznProcess::finished, [=] () { proc->deleteLater(); }); connect(proc, &MznProcess::failure, [=](int exitCode, MznProcess::FailureType e) { if (e == MznProcess::FailedToStart) { QMessageBox::critical(this, "MiniZinc IDE", "Failed to start MiniZinc. Check your path settings."); exitCode = 0; } else if (e != MznProcess::NonZeroExit) { QMetaEnum metaEnum = QMetaEnum::fromType(); QMessageBox::critical(this, "MiniZinc IDE", "Unknown error while executing MiniZinc: " + QString(metaEnum.valueToKey(e))); } ui->outputWidget->endExecution(0, proc->elapsedTime()); procFinished(exitCode, proc->elapsedTime()); }); connect(proc, &MznProcess::timeUpdated, this, &MainWindow::statusTimerEvent); SolverConfiguration compileSc(sc); compileSc.outputTiming = false; // Remove solns2out options updateUiProcessRunning(true); QStringList files({QFileInfo(model).fileName()}); for (auto& d : data) { files << QFileInfo(d).fileName(); } auto label = files.join(", ").prepend("Compiling "); if (!inlineData.isEmpty()) { label.append(" with "); label.append(inlineData.join(", ")); } ui->outputWidget->startExecution(label); proc->start(compileSc, args, fi.canonicalPath()); if (printCommand) { auto cmdMessage = QString("Command: %1\n").arg(proc->command()); ui->outputWidget->addText(cmdMessage, ui->outputWidget->infoCharFormat(), "Commands"); auto confMessage = QString("Configuration:\n%1").arg(QString::fromUtf8(sc.toJSON())); ui->outputWidget->addText(confMessage, ui->outputWidget->infoCharFormat(), "Commands"); } } void MainWindow::run(const SolverConfiguration& sc, const QString& model, const QStringList& data, const QStringList& extraArgs, const QStringList& inlineData, QTextStream* ts) { if (!requireMiniZinc()) { return; } QFileInfo fi(model); auto workingDir = fi.canonicalPath(); QSettings settings; settings.beginGroup("ide"); int compressSolutions = settings.value("compressSolutions", 100).toInt(); bool printCommand = settings.value("printCommand", false).toBool(); settings.endGroup(); auto* proc = new MznProcess(this); connect(this, &MainWindow::terminating, proc, [=] () { proc->terminate(); proc->deleteLater(); }); QStringList args; args << model << data; bool compiledChecker = false; QStringList files; for (auto& arg : args) { QFileInfo fi(arg); files << fi.fileName(); if (fi.suffix() == "mzc") { compiledChecker = true; } } args << extraArgs; for (auto& dzn : inlineData) { args << "-D" << dzn; } QString label = "Running "; label.append(files.join(", ")); if (!inlineData.isEmpty()) { label.append(" with ").append(inlineData.join(", ")); } if (sc.solverDefinition.isGUIApplication) { // Detach GUI app auto* failureCtx = new QObject(this); ui->outputWidget->startExecution(label + " (detached)"); ui->outputWidget->addText("Process will continue running detached from the IDE.\n", ui->outputWidget->commentCharFormat()); connect(proc, &MznProcess::started, this, [=] () { ui->outputWidget->endExecution(0, proc->elapsedTime()); procFinished(0); delete failureCtx; // Disconnect the failure handler }); connect(proc, &MznProcess::failure, failureCtx, [=](int exitCode, MznProcess::FailureType e) { if (e == MznProcess::FailedToStart) { QMessageBox::critical(this, "MiniZinc IDE", "Failed to start MiniZinc. Check your path settings."); exitCode = 0; } else if (e != MznProcess::NonZeroExit) { QMetaEnum metaEnum = QMetaEnum::fromType(); QMessageBox::critical(this, "MiniZinc IDE", "Unknown error while executing MiniZinc: " + QString(metaEnum.valueToKey(e))); } ui->outputWidget->endExecution(exitCode, proc->elapsedTime()); procFinished(exitCode, proc->elapsedTime()); }); connect(proc, &MznProcess::finished, proc, &QObject::deleteLater); connect(proc, &MznProcess::finished, failureCtx, &QObject::deleteLater); proc->start(sc, args, workingDir, ts == nullptr); return; } connect(ui->actionStop, &QAction::triggered, proc, [=] () { ui->actionStop->setDisabled(true); proc->stop(); ui->outputWidget->addText("Stopped.\n", ui->outputWidget->noticeCharFormat()); }); if (ts) { // Write to a stream for the purposes of submission // (also disables JSON streaming output) connect(proc, &MznProcess::outputStdOut, [=](const QString& d) { *ts << d; }); } connect(proc, &MznProcess::statisticsOutput, ui->outputWidget, &OutputWidget::addStatistics); connect(proc, &MznProcess::solutionOutput, ui->outputWidget, &OutputWidget::addSolution); connect(proc, &MznProcess::checkerOutput, ui->outputWidget, &OutputWidget::addCheckerOutput); connect(proc, &MznProcess::errorOutput, this, &MainWindow::on_minizincError); connect(proc, &MznProcess::warningOutput, this, [=] (const QJsonObject& error, bool fromChecker) { if (!fromChecker || !compiledChecker) { // Suppress warnings from compiled checkers on_minizincError(error); } }); connect(proc, &MznProcess::finalStatus, ui->outputWidget, &OutputWidget::addStatus); connect(proc, &MznProcess::unknownOutput, [=](const QString& d) { ui->outputWidget->addText(d); }); connect(proc, &MznProcess::commentOutput, this, [=] (const QString& d) { ui->outputWidget->addText(d, ui->outputWidget->commentCharFormat(), "Comments"); }); connect(proc, &MznProcess::progressOutput, this, &MainWindow::on_progressOutput); connect(proc, &MznProcess::traceOutput, this, [=] (const QString& section, const QVariant& message) { if (section == "trace_exp") { TextLayoutLock lock(ui->outputWidget); auto obj = message.toJsonObject(); auto msg = obj["message"].toString(); QRegularExpression val("\\(≡.*?\\)"); QRegularExpressionMatchIterator val_i = val.globalMatch(msg); int pos = 0; auto loc = obj["location"].toObject(); auto link = locationToLink(loc["filename"].toString(), loc["firstLine"].toInt(), loc["firstColumn"].toInt(), loc["lastLine"].toInt(), loc["lastColumn"].toInt(), IDE::instance()->themeManager->current().warningColor.get(darkMode) ); auto prevLink = ui->outputWidget->lastTraceLoc(link); if (prevLink != link) { ui->outputWidget->addHtml(link, "trace"); ui->outputWidget->addText(":\n", "trace"); } ui->outputWidget->addText(" ", ui->outputWidget->infoCharFormat(), "trace"); while (val_i.hasNext()) { auto match = val_i.next(); if (match.capturedStart() > 0) { ui->outputWidget->addText(msg.mid(pos, match.capturedStart() - pos), ui->outputWidget->infoCharFormat(), "trace"); } ui->outputWidget->addText(match.captured(0), ui->outputWidget->commentCharFormat(), "trace"); pos = match.capturedEnd(); } if (pos < msg.size()) { ui->outputWidget->addText(msg.mid(pos, msg.size() - pos), ui->outputWidget->infoCharFormat(), "trace"); } ui->outputWidget->addText("\n", ui->outputWidget->infoCharFormat(), "trace"); } else { auto text = message.toString(); if (!text.isEmpty()) { ui->outputWidget->addTextToSection(section, text, ui->outputWidget->commentCharFormat()); } if (vis_connector == nullptr && section.startsWith("mzn_vis_")) { auto obj = message.toJsonObject(); startVisualisation(model, data, section, obj["url"].toString(), obj["userData"], proc); } } }); connect(proc, &MznProcess::outputStdError, this, [=] (const QString& d) { ui->outputWidget->addText(d, ui->outputWidget->commentCharFormat(), "Standard Error"); }); connect(proc, &MznProcess::success, [=]() { ui->outputWidget->endExecution(0, proc->elapsedTime()); procFinished(0, proc->elapsedTime()); }); connect(proc, &MznProcess::finished, proc, &QObject::deleteLater); connect(proc, &MznProcess::failure, [=](int exitCode, MznProcess::FailureType e) { if (e == MznProcess::FailedToStart) { QMessageBox::critical(this, "MiniZinc IDE", "Failed to start MiniZinc. Check your path settings."); exitCode = 0; } else if (e != MznProcess::NonZeroExit) { QMetaEnum metaEnum = QMetaEnum::fromType(); QMessageBox::critical(this, "MiniZinc IDE", "Unknown error while executing MiniZinc: " + QString(metaEnum.valueToKey(e))); } ui->outputWidget->endExecution(exitCode, proc->elapsedTime()); procFinished(exitCode, proc->elapsedTime()); }); connect(proc, &MznProcess::timeUpdated, this, &MainWindow::statusTimerEvent); updateUiProcessRunning(true); vis_connector = nullptr; ui->outputWidget->setSolutionLimit(compressSolutions); ui->outputWidget->startExecution(label); proc->start(sc, args, workingDir, ts == nullptr); if (printCommand) { auto cmdMessage = QString("Command: %1\n").arg(proc->command()); ui->outputWidget->addText(cmdMessage, ui->outputWidget->infoCharFormat(), "Commands"); auto confMessage = QString("Configuration:\n%1").arg(QString::fromUtf8(sc.toJSON())); ui->outputWidget->addText(confMessage, ui->outputWidget->infoCharFormat(), "Commands"); } } QString MainWindow::currentSolverConfigName(void) { SolverConfiguration* sc = getCurrentSolverConfig(); return sc ? sc->name() : "None"; } void MainWindow::procFinished(int exitCode, qint64 time) { procFinished(exitCode); QString elapsedTime = setElapsedTime(time); ui->statusbar->clearMessage(); } void MainWindow::procFinished(int exitCode) { updateUiProcessRunning(false); ui->statusbar->clearMessage(); emit(finished()); } void MainWindow::saveFile(CodeEditor* ce, const QString& f) { QString filepath = f; int tabIndex = ui->tabWidget->indexOf(ce); if (filepath=="") { if (ce != curEditor) { ui->tabWidget->setCurrentIndex(tabIndex); } QString dialogPath = ce->filepath.isEmpty() ? getLastPath()+"/"+ce->filename: ce->filepath; QString selectedFilter = "Other (*)"; if (dialogPath.endsWith(".mzn") || (ce->filepath.isEmpty() && ce->filename=="Playground")) selectedFilter = "MiniZinc model (*.mzn)"; else if (dialogPath.endsWith(".dzn") || dialogPath.endsWith(".json")) selectedFilter = "MiniZinc data (*.dzn *.json)"; else if (dialogPath.endsWith(".fzn")) selectedFilter = "FlatZinc (*.fzn)"; else if (dialogPath.endsWith(".mzc")) selectedFilter = "MiniZinc solution checker (*.mzc)"; filepath = QFileDialog::getSaveFileName(this,"Save file",dialogPath,"MiniZinc model (*.mzn);;MiniZinc data (*.dzn *.json);;MiniZinc solution checker (*.mzc);;FlatZinc (*.fzn);;Other (*)",&selectedFilter); if (!filepath.isNull()) { setLastPath(QFileInfo(filepath).absolutePath()+fileDialogSuffix); } } if (!filepath.isEmpty()) { if (filepath != ce->filepath && IDE::instance()->hasFile(filepath)) { QMessageBox::warning(this,"MiniZinc IDE","Cannot overwrite open file.", QMessageBox::Ok); } else { IDE::instance()->fsWatch.removePath(filepath); QFile file(filepath); if (file.open(QFile::WriteOnly | QFile::Text)) { QTextStream out(&file); #if QT_VERSION < 0x060000 out.setCodec(QTextCodec::codecForName("UTF-8")); #endif auto contents = ce->document()->toPlainText(); out << contents; file.close(); if (filepath != ce->filepath) { QTextDocument* newdoc = IDE::instance()->addDocument(filepath,ce->document(),ce); ce->setDocument(newdoc); if (ce->filepath != "") { IDE::instance()->removeEditor(ce->filepath,ce); } getProject().remove(ce->filepath); getProject().add(filepath); ce->filepath = filepath; } ce->document()->setModified(false); ce->filename = QFileInfo(filepath).fileName(); ui->tabWidget->setTabText(tabIndex,ce->filename); updateRecentFiles(filepath); if (ce==curEditor) tabChange(tabIndex); auto* history = getProject().history(); if (history != nullptr) { history->updateFileContents(filepath, contents); } } else { QMessageBox::warning(this,"MiniZinc IDE","Could not save file"); } IDE::instance()->fsWatch.addPath(filepath); } } } void MainWindow::fileRenamed(const QString& oldPath, const QString& newPath) { for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce && ce->filepath==oldPath) { ce->filepath = newPath; ce->filename = QFileInfo(newPath).fileName(); IDE::instance()->renameFile(oldPath,newPath); ui->tabWidget->setTabText(i,ce->filename); updateRecentFiles(newPath); if (ce==curEditor) tabChange(i); } } } void MainWindow::on_actionSave_triggered() { if (curEditor) { saveFile(curEditor,curEditor->filepath); } } void MainWindow::on_actionSave_as_triggered() { if (curEditor) { saveFile(curEditor,QString()); } } void MainWindow::on_actionQuit_triggered() { qApp->closeAllWindows(); if (IDE::instance()->mainWindows.size()==0) { IDE::instance()->quit(); } } void MainWindow::openCompiledFzn(const QString& fzn) { QFile file(fzn); int fsize = file.size() / (1024 * 1024); if (fsize > 10) { QMessageBox::StandardButton sb = QMessageBox::warning(this, "MiniZinc IDE", QString("Compilation resulted in a large FlatZinc file (")+ QString().setNum(fsize)+" MB). Opening " "the file may slow down the IDE and potentially " "affect its stability. Do you want to open it anyway, save it, or discard the file?", QMessageBox::Open | QMessageBox::Discard | QMessageBox::Save); switch (sb) { case QMessageBox::Save: { bool success = true; do { QString savepath = QFileDialog::getSaveFileName(this,"Save FlatZinc",getLastPath(),"FlatZinc files (*.fzn)"); if (!savepath.isNull() && !savepath.isEmpty()) { QFile oldfile(savepath); if (oldfile.exists()) { if (!oldfile.remove()) { success = false; } } file.copy(savepath); } } while (!success); return; } case QMessageBox::Discard: return; default: break; } } createEditor(fzn, !fzn.endsWith(".mzc"), !fzn.endsWith(".mzc"), false); } void MainWindow::profileCompiledFzn(const QVector& timing, const QVector& paths) { typedef QMap> CoverageMap; CoverageMap ce_coverage; for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce == nullptr) { continue; } auto path = ce->filepath.isEmpty() ? ce->playgroundTempFile : ce->filepath; if (!path.endsWith(".mzn")) { continue; } QTextBlock tb = ce->document()->begin(); QVector coverage; while (tb.isValid()) { BracketData* bd = static_cast(tb.userData()); if (bd==nullptr) { bd = new BracketData; tb.setUserData(bd); } bd->d.reset(); coverage.push_back(bd); tb = tb.next(); } ce_coverage.insert(path,coverage); } int totalCons=1; int totalVars=1; int totalTime=1; for (auto& it : paths) { int min_line_covered = -1; CoverageMap::iterator fileMatch; for (auto& segment : it.path().segments()) { auto tryMatch = ce_coverage.find(segment.filename); if (tryMatch != ce_coverage.end()) { fileMatch = tryMatch; min_line_covered = segment.firstLine; } } if (min_line_covered > 0 && min_line_covered <= fileMatch.value().size()) { if (it.constraintIndex() != -1) { fileMatch.value()[min_line_covered-1]->d.con++; totalCons++; } else { fileMatch.value()[min_line_covered-1]->d.var++; totalVars++; } } } for (auto& it : timing) { CoverageMap::iterator fileMatch = ce_coverage.find(it.filename()); if (fileMatch != ce_coverage.end()) { int line_no = it.line(); if (line_no > 0 && line_no <= fileMatch.value().size()) { fileMatch.value()[line_no-1]->d.ms = it.time(); totalTime += fileMatch.value()[line_no-1]->d.ms; } } } for (auto& coverage: ce_coverage) { for(auto data: coverage){ data->d.totalCon = totalCons; data->d.totalVar = totalVars; data->d.totalMs = totalTime; } } curEditor->repaint(); } void MainWindow::on_actionCompile_triggered() { compileOrRun(CM_COMPILE); } void MainWindow::on_actionClear_output_triggered() { progressBar->setHidden(true); ui->outputWidget->clear(); if (server != nullptr) { server->clear(); } } void MainWindow::setEditorFont(QFont font) { editorFont = font; ui->outputWidget->setBrowserFont(font); for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce) { ce->setEditorFont(font); } } } void MainWindow::setEditorIndent(int indentSize, bool useTabs) { for (int i = 0; i < ui->tabWidget->count(); i++) { auto* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce) { ce->setIndentSize(indentSize); ce->setIndentTab(useTabs); } } } void MainWindow::setEditorWordWrap(QTextOption::WrapMode mode) { for (int i = 0; i < ui->tabWidget->count(); i++) { auto* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce) { ce->setWordWrapMode(mode); } } } void MainWindow::on_actionBigger_font_triggered() { QSettings settings; settings.beginGroup("MainWindow"); auto zoom = std::min(settings.value("zoom", 100).toInt() + 10, 10000); settings.setValue("zoom", zoom); auto font = IDEUtils::fontFromString(settings.value("editorFont").toString()); settings.endGroup(); font.setPointSize(font.pointSize() * zoom / 100); IDE::instance()->setEditorFont(font); } void MainWindow::on_actionSmaller_font_triggered() { QSettings settings; settings.beginGroup("MainWindow"); auto zoom = std::max(10, settings.value("zoom", 100).toInt() - 10); settings.setValue("zoom", zoom); auto font = IDEUtils::fontFromString(settings.value("editorFont").toString()); settings.endGroup(); font.setPointSize(font.pointSize() * zoom / 100); IDE::instance()->setEditorFont(font); } void MainWindow::on_actionDefault_font_size_triggered() { QSettings settings; settings.beginGroup("MainWindow"); settings.setValue("zoom", 100); auto font = IDEUtils::fontFromString(settings.value("editorFont").toString()); settings.endGroup(); font.setPointSize(font.pointSize()); IDE::instance()->setEditorFont(font); } #ifndef MINIZINC_IDE_BUILD #define MINIZINC_IDE_BUILD "" #endif void MainWindow::on_actionAbout_MiniZinc_IDE_triggered() { QString buildString(MINIZINC_IDE_BUILD); if (!buildString.isEmpty()) buildString += "\n"; QMessageBox::about(this, "The MiniZinc IDE", QString("The MiniZinc IDE\n\nVersion ")+IDE::instance()->applicationVersion()+"\n"+ buildString+"\n" "Copyright Monash University, NICTA, Data61 2013-" + BUILD_YEAR + "\n\n"+ "This program is provided under the terms of the Mozilla Public License Version 2.0. It uses the Qt toolkit, available from qt-project.org."); } QVector MainWindow::collectCodeEditors(QVector& locs) { QVector ces; ces.resize(locs.size()); // Open each file in the path for (int p = 0; p < locs.size(); p++) { QStringList& elements = locs[p]; QString filename = elements[0]; QUrl url = QUrl::fromLocalFile(filename); QFileInfo urlinfo(url.toLocalFile()); bool notOpen = true; if (filename != "") { for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (!ce) { continue; } QFileInfo ceinfo(ce->filepath); if (ceinfo.canonicalFilePath() == urlinfo.canonicalFilePath()) { ces[p] = ce; if(p == locs.size()-1) { ui->tabWidget->setCurrentIndex(i); } notOpen = false; break; } } if (notOpen && filename.size() > 0) { openFile(url.toLocalFile(), false, false); CodeEditor* ce = static_cast(ui->tabWidget->widget(ui->tabWidget->count()-1)); QFileInfo ceinfo(ce->filepath); if (ceinfo.canonicalFilePath() == urlinfo.canonicalFilePath()) { ces[p] = ce; } else { throw InternalError("Code editor file path does not match URL file path"); } } } else { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(ui->tabWidget->currentIndex())); ces[p] = ce; } } return ces; } void MainWindow::find(bool fwd, bool forceNoWrapAround) { const QString& toFind = ui->find->text(); QTextDocument::FindFlags flags; if (!fwd) flags |= QTextDocument::FindBackward; bool ignoreCase = ui->check_case->isChecked(); if (!ignoreCase) flags |= QTextDocument::FindCaseSensitively; bool wrap = !forceNoWrapAround && ui->check_wrap->isChecked(); QTextCursor cursor(curEditor->textCursor()); int hasWrapped = wrap ? 0 : 1; while (hasWrapped < 2) { if (ui->check_re->isChecked()) { QRegularExpression re(toFind, ignoreCase ? QRegularExpression::CaseInsensitiveOption : QRegularExpression::NoPatternOption); if (!re.isValid()) { ui->not_found->setText("invalid"); return; } cursor = curEditor->document()->find(re,cursor,flags); } else { cursor = curEditor->document()->find(toFind,cursor,flags); } if (cursor.isNull()) { hasWrapped++; cursor = curEditor->textCursor(); if (fwd) { cursor.setPosition(0); } else { cursor.movePosition(QTextCursor::End); } } else { ui->not_found->setText(""); curEditor->setTextCursor(cursor); break; } } if (hasWrapped==2) { ui->not_found->setText("not found"); } } #define major_sep ';' #define minor_sep '|' QVector getBlocksFromPath(QString& path) { QVector locs; QStringList blocks = path.split(major_sep); foreach(QString block, blocks) { QStringList elements = block.split(minor_sep); if(elements.size() >= 5) { bool ok = false; if(elements.size() > 5) elements[5].toInt(&ok); elements.erase(elements.begin()+(ok ? 6 : 5), elements.end()); locs.append(elements); } } return locs; } void MainWindow::highlightPath(QString& path, int index) { // Build list of blocks to be highlighted QVector locs = getBlocksFromPath(path); if(locs.size() == 0) return; QVector ces = collectCodeEditors(locs); if(ces.size() != locs.size()) return; int b = Qt::red; int t = Qt::yellow; QColor colour = static_cast((index % (t-b)) + b); int strans = 25; int trans = strans; int tstep = (250-strans) / locs.size(); for(int p = 0; p < locs.size(); p++) { QStringList& elements = locs[p]; CodeEditor* ce = ces[p]; if (!ce) { continue; } bool ok; int sl = elements[1].toInt(&ok); int sc = elements[2].toInt(&ok); int el = elements[3].toInt(&ok); int ec = elements[4].toInt(&ok); if(elements.size() == 6) trans = elements[5].toInt(&ok); if (ok) { colour.setAlpha(trans); trans = trans < 250 ? trans+tstep : strans; Highlighter& hl = ce->getHighlighter(); hl.addFixedBg(sl,sc,el,ec,colour,path); hl.rehighlight(); ce->setTextCursor(QTextCursor(ce->document()->findBlockByLineNumber(el))); } } } void MainWindow::anchorClicked(const QUrl & anUrl) { QUrl url = anUrl; if(url.scheme() == "highlight") { // Reset the highlighters for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce) { Highlighter& hl = ce->getHighlighter(); hl.clearFixedBg(); hl.rehighlight(); } } QString query = url.query(); QStringList conflictSet = query.split("&"); for(int c = 0; cgetExecution(ex_id); if (ex != nullptr) { showExecutionWindow(conductor->getExecutionWindow(ex)); } } return; } if(url.scheme() == "http" || url.scheme() == "https") { QDesktopServices::openUrl(url); return; } QString query = url.query(); url.setQuery(""); url.setScheme("file"); QFileInfo urlinfo(url.toLocalFile()); IDE::instance()->stats.errorsClicked++; for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (!ce) { continue; } QFileInfo ceinfo(ce->filepath.isEmpty() ? ce->playgroundTempFile : ce->filepath); if (ceinfo.canonicalFilePath() == urlinfo.canonicalFilePath()) { QRegularExpression re_line("line=([0-9]+)"); auto re_line_match = re_line.match(query); if (re_line_match.hasMatch()) { bool ok; int line = re_line_match.captured(1).toInt(&ok); if (ok) { int col = 1; QRegularExpression re_col("column=([0-9]+)"); auto re_col_match = re_col.match(query); if (re_col_match.hasMatch()) { bool ok; col = re_col_match.captured(1).toInt(&ok); if (!ok) col = 1; } QTextBlock block = ce->document()->findBlockByNumber(line-1); if (block.isValid()) { QTextCursor cursor = ce->textCursor(); cursor.setPosition(block.position()+col-1); ce->setFocus(); ce->setTextCursor(cursor); ce->centerCursor(); ui->tabWidget->setCurrentIndex(i); } } } } } } void MainWindow::on_actionManage_solvers_triggered(bool addNew) { QSettings settings; settings.beginGroup("ide"); bool checkUpdates = settings.value("checkforupdates21",false).toBool(); settings.endGroup(); ui->config_window->stashModifiedConfigs(); PreferencesDialog pd(addNew, this); pd.exec(); checkDriver(); ui->config_window->loadConfigs(); ui->config_window->unstashModifiedConfigs(); settings.beginGroup("ide"); if (!checkUpdates && settings.value("checkforupdates21",false).toBool()) { settings.setValue("lastCheck21",QDate::currentDate().addDays(-2).toString()); IDE::instance()->checkUpdate(); } indentSize = settings.value("indentSize", 2).toInt(); useTabs = settings.value("indentTabs", false).toBool(); IDE::instance()->setEditorIndent(indentSize, useTabs); bool wordWrap = settings.value("wordWrap", true).toBool(); IDE::instance()->setEditorWordWrap(wordWrap ? QTextOption::WrapAtWordBoundaryOrAnywhere : QTextOption::NoWrap); settings.endGroup(); settings.beginGroup("MainWindow"); editorFont = IDEUtils::fontFromString(settings.value("editorFont").toString()); auto zoom = settings.value("zoom", 100).toInt(); editorFont.setPointSize(editorFont.pointSize() * zoom / 100); IDE::instance()->setEditorFont(editorFont); settings.endGroup(); settings.beginGroup("minizinc"); settings.setValue("mznpath", MznDriver::get().mznDistribPath()); settings.endGroup(); } void MainWindow::on_actionFind_triggered() { incrementalFindCursor = curEditor->textCursor(); incrementalFindCursor.setPosition(std::min(curEditor->textCursor().anchor(), curEditor->textCursor().position())); if (curEditor->textCursor().hasSelection()) { ui->find->setText(curEditor->textCursor().selectedText()); } ui->not_found->setText(""); ui->find->selectAll(); ui->findFrame->raise(); ui->findFrame->show(); ui->find->setFocus(); } void MainWindow::on_actionReplace_triggered() { incrementalFindCursor = curEditor->textCursor(); incrementalFindCursor.setPosition(std::min(curEditor->textCursor().anchor(), curEditor->textCursor().position())); if (curEditor->textCursor().hasSelection()) { ui->find->setText(curEditor->textCursor().selectedText()); } ui->not_found->setText(""); ui->find->selectAll(); ui->findFrame->raise(); ui->findFrame->show(); } void MainWindow::on_actionGo_to_line_triggered() { if (curEditor==nullptr) return; GoToLineDialog gtl; if (gtl.exec()==QDialog::Accepted) { bool ok; int line = gtl.getLine(&ok); if (ok) { QTextBlock block = curEditor->document()->findBlockByNumber(line-1); if (block.isValid()) { QTextCursor cursor = curEditor->textCursor(); cursor.setPosition(block.position()); curEditor->setTextCursor(cursor); } } } } void MainWindow::checkDriver() { auto& driver = MznDriver::get(); bool haveMzn = (driver.isValid() && driver.solvers().size() > 0); ui->actionRun->setEnabled(haveMzn); ui->actionProfile_compilation->setEnabled(haveMzn); ui->actionCompile->setEnabled(haveMzn); ui->actionEditSolverConfig->setEnabled(haveMzn); ui->actionSubmit_to_MOOC->setEnabled(haveMzn); if (!haveMzn) ui->configWindow_dockWidget->hide(); } void MainWindow::on_actionShift_left_triggered() { if (curEditor != nullptr) { curEditor->shiftLeft(); } } void MainWindow::on_actionShift_right_triggered() { if (curEditor != nullptr) { curEditor->shiftRight(); } } void MainWindow::on_actionHelp_triggered() { IDE::instance()->help(); } void MainWindow::on_actionNew_project_triggered() { MainWindow* mw = new MainWindow; QPoint p = pos(); mw->move(p.x()+20, p.y()+20); mw->show(); } bool MainWindow::isEmptyProject(void) { if (getProject().hasProjectFile() || !getProject().files().empty()) { return false; } for (int i = 0; i < ui->tabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce && (!ce->filepath.isEmpty() || ce->document()->isModified())) { return false; } } return true; } void MainWindow::openProject(const QString& fileName) { if (!fileName.isEmpty()) { IDE::PMap& pmap = IDE::instance()->projects; IDE::PMap::iterator it = pmap.find(fileName); if (it==pmap.end()) { if (isEmptyProject()) { int closeTab = ui->tabWidget->count()==1 ? 0 : -1; loadProject(fileName); if (closeTab > 0 && ui->tabWidget->count()>1) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(closeTab)); if (ce && ce->filepath == "") tabCloseRequest(closeTab); } } else { MainWindow* mw = new MainWindow(fileName); QPoint p = pos(); mw->move(p.x()+20, p.y()+20); mw->show(); } } else { it.value()->raise(); it.value()->activateWindow(); } } } void MainWindow::updateRecentProjects(const QString& p) { if (!p.isEmpty()) { IDE::instance()->addRecentProject(p); } ui->menuRecent_Projects->clear(); for (int i=0; irecentProjects.size(); i++) { QAction* na = ui->menuRecent_Projects->addAction(IDE::instance()->recentProjects[i]); na->setData(IDE::instance()->recentProjects[i]); } ui->menuRecent_Projects->addSeparator(); ui->menuRecent_Projects->addAction("Clear Menu"); } void MainWindow::updateRecentFiles(const QString& p) { if (!p.isEmpty()) { IDE::instance()->addRecentFile(p); } ui->menuRecent_Files->clear(); for (int i=0; irecentFiles.size(); i++) { QAction* na = ui->menuRecent_Files->addAction(IDE::instance()->recentFiles[i]); na->setData(IDE::instance()->recentFiles[i]); } ui->menuRecent_Files->addSeparator(); ui->menuRecent_Files->addAction("Clear Menu"); } void MainWindow::recentFileMenuAction(QAction* a) { if (a->text()=="Clear Menu") { IDE::instance()->recentFiles.clear(); updateRecentFiles(""); } else { openFile(a->data().toString()); } } void MainWindow::recentProjectMenuAction(QAction* a) { if (a->text()=="Clear Menu") { IDE::instance()->recentProjects.clear(); updateRecentProjects(""); } else { openProject(a->data().toString()); } } void MainWindow::saveProject(const QString& f) { QString filepath = f; if (filepath.isEmpty()) { filepath = QFileDialog::getSaveFileName(this, "Save project", getLastPath(), "MiniZinc projects (*.mzp)"); if (!filepath.isNull()) { setLastPath(QFileInfo(filepath).absolutePath() + fileDialogSuffix); } } if (filepath.isEmpty()) { return; } auto& p = getProject(); if (p.projectFile() != filepath && IDE::instance()->projects.contains(filepath)) { QMessageBox::warning(this,"MiniZinc IDE","Cannot overwrite existing open project.", QMessageBox::Ok); return; } if (p.projectFile() != filepath) { IDE::instance()->projects.remove(p.projectFile()); IDE::instance()->projects.insert(filepath, this); p.projectFile(filepath); } p.openTabsChanged(getOpenFiles(), ui->tabWidget->currentIndex()); p.activeSolverConfigChanged(getCurrentSolverConfig()); try { p.saveProject(); } catch (FileError& f) { QMessageBox::critical(this, "MiniZinc IDE", f.message(), QMessageBox::Ok); } updateRecentProjects(p.projectFile()); } void MainWindow::loadProject(const QString& filepath) { try { auto& p = getProject(); auto warnings = p.loadProject(filepath, ui->config_window); if (ui->projectExplorerDockWidget->isHidden()) { on_actionShow_project_explorer_triggered(); } if (!warnings.empty()) { QMessageBox::warning(this, "MiniZinc IDE", warnings.join("\n"), QMessageBox::Ok); } int openTab = p.openTab(); auto openFiles = p.openFiles(); for (int i = 0; i < openFiles.count(); i++) { openFile(openFiles[i], false, i == openTab); } p.activeSolverConfigChanged(getCurrentSolverConfig()); p.setModified(false); IDE::instance()->projects.insert(p.projectFile(), this); updateRecentProjects(p.projectFile()); } catch (Exception& e) { QMessageBox::warning(this, "MiniZinc IDE", e.message(), QMessageBox::Ok); } } void MainWindow::on_actionSave_project_triggered() { saveProject(getProject().projectFile()); } void MainWindow::on_actionSave_project_as_triggered() { saveProject(QString()); } void MainWindow::on_actionClose_project_triggered() { close(); } void MainWindow::on_actionFind_next_triggered() { on_b_next_clicked(); } void MainWindow::on_actionFind_previous_triggered() { on_b_prev_clicked(); } void MainWindow::on_actionSave_all_triggered() { for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce && ce->document()->isModified()) saveFile(ce,ce->filepath); } } void MainWindow::on_action_Un_comment_triggered() { if (curEditor==nullptr) return; QTextCursor cursor = curEditor->textCursor(); QTextBlock beginBlock = curEditor->document()->findBlock(cursor.anchor()); QTextBlock endblock = curEditor->document()->findBlock(cursor.position()); if (beginBlock.blockNumber() > endblock.blockNumber()) std::swap(beginBlock,endblock); endblock = endblock.next(); QRegularExpression comment("^(\\s*%|\\s*$)"); QRegularExpression comSpace("%\\s"); QRegularExpression emptyLine("^\\s*$"); QTextBlock block = beginBlock; bool isCommented = true; do { if (!comment.match(block.text()).hasMatch()) { isCommented = false; break; } block = block.next(); } while (block.isValid() && block != endblock); block = beginBlock; cursor.beginEditBlock(); do { cursor.setPosition(block.position()); QString t = block.text(); if (isCommented) { int cpos = t.indexOf("%"); if (cpos != -1) { cursor.setPosition(block.position()+cpos); bool haveSpace = (comSpace.match(t,cpos).capturedStart() == cpos); cursor.movePosition(QTextCursor::Right,QTextCursor::KeepAnchor,haveSpace ? 2:1); cursor.removeSelectedText(); } } else { if (!emptyLine.match(t).hasMatch()) cursor.insertText("% "); } block = block.next(); } while (block.isValid() && block != endblock); cursor.endEditBlock(); } void MainWindow::on_actionOnly_editor_triggered() { if (!ui->outputDockWidget->isFloating()) ui->outputDockWidget->hide(); } void MainWindow::on_actionSplit_triggered() { if (!ui->outputDockWidget->isFloating()) ui->outputDockWidget->show(); } void MainWindow::on_actionPrevious_tab_triggered() { if (ui->tabWidget->currentIndex() > 0) { ui->tabWidget->setCurrentIndex(ui->tabWidget->currentIndex()-1); } else { ui->tabWidget->setCurrentIndex(ui->tabWidget->count()-1); } } void MainWindow::on_actionNext_tab_triggered() { if (ui->tabWidget->currentIndex() < ui->tabWidget->count()-1) { ui->tabWidget->setCurrentIndex(ui->tabWidget->currentIndex()+1); } else { ui->tabWidget->setCurrentIndex(0); } } void MainWindow::on_actionHide_tool_bar_triggered() { if (ui->toolBar->isHidden()) { ui->toolBar->show(); ui->actionHide_tool_bar->setText("Hide tool bar"); } else { ui->toolBar->hide(); ui->actionHide_tool_bar->setText("Show tool bar"); } } void MainWindow::on_actionToggle_profiler_info_triggered() { profileInfoVisible = !profileInfoVisible; for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce) { ce->showDebugInfo(profileInfoVisible); } } } void MainWindow::on_actionShow_project_explorer_triggered() { if (ui->projectExplorerDockWidget->isHidden()) { ui->projectExplorerDockWidget->show(); ui->actionShow_project_explorer->setText("Hide project explorer"); } else { ui->projectExplorerDockWidget->hide(); ui->actionShow_project_explorer->setText("Show project explorer"); } } void MainWindow::onClipboardChanged() { ui->actionPaste->setEnabled(!QApplication::clipboard()->text().isEmpty()); } void MainWindow::editor_cursor_position_changed() { if (curEditor) { statusLineColLabel->setText(QString("Line: ")+QString().number(curEditor->textCursor().blockNumber()+1)+", Col: "+QString().number(curEditor->textCursor().columnNumber()+1)); } } void MainWindow::on_actionSubmit_to_MOOC_triggered() { // Check if any documents need saving QVector modifiedDocs; QSet files; for (auto model : getProject().moocAssignment().models) { files.insert(model.model); files.insert(model.data); } for (auto problem : getProject().moocAssignment().problems) { files.insert(problem.model); files.insert(problem.data); } for (int i = 0; i < ui->tabWidget->count(); i++) { auto ce = qobject_cast(ui->tabWidget->widget(i)); if (ce && ce->document()->isModified() && files.contains(ce->filepath)) { modifiedDocs.append(ce); } } if (!modifiedDocs.empty()) { if (!saveBeforeRunning) { QMessageBox msgBox; if (modifiedDocs.size()==1) { msgBox.setText("One of the files has been modified. You must save it before submitting."); } else { msgBox.setText("Several files have been modified. You must save them before submitting."); } msgBox.setInformativeText("Do you want to save now and then submit?"); QAbstractButton *saveButton = msgBox.addButton(QMessageBox::Save); msgBox.addButton(QMessageBox::Cancel); QAbstractButton *alwaysButton = msgBox.addButton("Always save", QMessageBox::AcceptRole); msgBox.setDefaultButton(QMessageBox::Save); msgBox.exec(); if (msgBox.clickedButton() == alwaysButton) { saveBeforeRunning = true; } else if (msgBox.clickedButton() != saveButton) { return; } } for (auto ce : modifiedDocs) { saveFile(ce,ce->filepath); if (ce->document()->isModified()) { return; } } } moocSubmission = new MOOCSubmission(this, getProject().moocAssignment()); connect(moocSubmission, &MOOCSubmission::finished, this, &MainWindow::moocFinished); setEnabled(false); moocSubmission->show(); } void MainWindow::moocFinished(int) { moocSubmission->deleteLater(); setEnabled(true); } bool MainWindow::eventFilter(QObject *obj, QEvent *ev) { if (obj == ui->findWidget) { if (ev->type() == QEvent::KeyPress) { auto* keyEvent = static_cast(ev); if (keyEvent->key() == Qt::Key_Escape) { on_closeFindWidget_clicked(); return true; } } } return QMainWindow::eventFilter(obj,ev); } void MainWindow::on_actionCheat_Sheet_triggered() { IDE::instance()->cheatSheet->show(); IDE::instance()->cheatSheet->raise(); IDE::instance()->cheatSheet->activateWindow(); } void MainWindow::check_code() { auto sc = getCurrentSolverConfig(); if (!MznDriver::get().isValid() || !ui->actionRun->isEnabled() || !curEditor || (!curEditor->filepath.isEmpty() && !curEditor->filepath.endsWith(".mzn")) || !sc) { return; } auto contents = curEditor->document()->toPlainText(); auto wd = QFileInfo(curEditor->filepath).absolutePath(); code_checker->start(contents, *sc, wd); } void MainWindow::setTheme(const Theme& theme, bool dark) { darkMode = dark; for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce) { ce->setTheme(theme, darkMode); } } ui->outputWidget->setTheme(theme, darkMode); if (conductor != nullptr) { conductor->setDarkMode(darkMode); } } void MainWindow::initTheme() { auto* tm = IDE::instance()->themeManager; auto* dm = IDE::instance()->darkModeNotifier; setTheme(tm->current(), dm->darkMode()); } void MainWindow::on_actionEditSolverConfig_triggered() { if (ui->configWindow_dockWidget->isHidden()) { ui->configWindow_dockWidget->show(); } else { ui->configWindow_dockWidget->hide(); } } void MainWindow::on_b_next_clicked() { find(true); incrementalFindCursor.setPosition(std::min(curEditor->textCursor().anchor(), curEditor->textCursor().position())); } void MainWindow::on_b_prev_clicked() { find(false); incrementalFindCursor.setPosition(std::min(curEditor->textCursor().anchor(), curEditor->textCursor().position())); } void MainWindow::on_b_replacefind_clicked() { on_b_replace_clicked(); find(true); incrementalFindCursor.setPosition(std::min(curEditor->textCursor().anchor(), curEditor->textCursor().position())); } void MainWindow::on_b_replace_clicked() { QTextCursor cursor = curEditor->textCursor(); if (cursor.hasSelection()) { cursor.insertText(ui->replace->text()); } } void MainWindow::on_b_replaceall_clicked() { int counter = 0; QTextCursor cursor = curEditor->textCursor(); QTextCursor origCursor = curEditor->textCursor(); cursor.movePosition(QTextCursor::Start); curEditor->setTextCursor(cursor); if (!cursor.hasSelection()) { find(true,true); cursor = curEditor->textCursor(); } cursor.beginEditBlock(); while (cursor.hasSelection()) { counter++; cursor.insertText(ui->replace->text()); find(true,true); cursor = curEditor->textCursor(); } cursor.endEditBlock(); if (counter > 0) { ui->not_found->setText(QString().number(counter)+" replaced"); } else { curEditor->setTextCursor(origCursor); } incrementalFindCursor.setPosition(std::min(curEditor->textCursor().anchor(), curEditor->textCursor().position())); } void MainWindow::on_closeFindWidget_clicked() { closeFindWidget(); } void MainWindow::closeFindWidget() { ui->findFrame->hide(); curEditor->setFocus(); } void MainWindow::on_find_textEdited(const QString &) { curEditor->setTextCursor(incrementalFindCursor); find(true); } void MainWindow::on_actionProfile_compilation_triggered() { compileOrRun(CM_PROFILE); if (!profileInfoVisible) { on_actionToggle_profiler_info_triggered(); } } void MainWindow::setEditorMenuItemsEnabled(bool enabled) { // These items only need to be enabled when there is an editor tab open ui->actionClose->setEnabled(enabled); ui->menuFind->setEnabled(enabled); ui->actionFind->setEnabled(enabled); ui->actionReplace->setEnabled(enabled); ui->actionFind_next->setEnabled(enabled); ui->actionFind_previous->setEnabled(enabled); ui->actionShift_left->setEnabled(enabled); ui->actionShift_right->setEnabled(enabled); ui->actionCut->setEnabled(enabled); ui->actionCopy->setEnabled(enabled); ui->actionPaste->setEnabled(enabled); ui->actionSelect_All->setEnabled(enabled); ui->actionGo_to_line->setEnabled(enabled); ui->action_Un_comment->setEnabled(enabled); ui->actionSave->setEnabled(enabled); ui->actionSave_as->setEnabled(enabled); ui->actionSave_all->setEnabled(enabled); ui->actionUndo->setEnabled(enabled); ui->actionRedo->setEnabled(enabled); ui->actionRun->setEnabled(enabled); ui->actionCompile->setEnabled(enabled); ui->actionProfile_compilation->setEnabled(enabled); ui->actionProfile_search->setEnabled(enabled); } void MainWindow::on_configWindow_dockWidget_visibilityChanged(bool visible) { if (visible) { ui->actionEditSolverConfig->setText("Hide configuration editor..."); } else { ui->actionEditSolverConfig->setText("Show configuration editor..."); } } void MainWindow::on_config_window_itemsChanged(const QStringList& items) { disconnect(solverConfCombo, QOverload::of(&QComboBox::currentIndexChanged), ui->config_window, &ConfigWindow::setCurrentIndex); solverConfCombo->clear(); solverConfCombo->addItems(items); solverConfCombo->setCurrentIndex(ui->config_window->currentIndex()); connect(solverConfCombo, QOverload::of(&QComboBox::currentIndexChanged), ui->config_window, &ConfigWindow::setCurrentIndex); ui->menuSolver_configurations->clear(); for (auto& item: items) { auto action = ui->menuSolver_configurations->addAction(item); action->setCheckable(true); action->setChecked(solverConfCombo->currentText() == item); } ui->menuSolver_configurations->addSeparator(); ui->menuSolver_configurations->addAction(ui->actionEditSolverConfig); bool canSaveAll = false; for (auto sc : ui->config_window->solverConfigs()) { if (!sc->isBuiltin && sc->modified) { if (sc->paramFile.isEmpty()) { canSaveAll = false; break; } else { canSaveAll = true; } } } ui->actionSave_all_solver_configurations->setEnabled(canSaveAll); } void MainWindow::on_config_window_selectedIndexChanged(int index) { solverConfCombo->setCurrentIndex(index); for (auto action : ui->menuSolver_configurations->actions()) { action->setChecked(solverConfCombo->currentText() == action->text()); } auto sc = getCurrentSolverConfig(); ui->actionSave_solver_configuration->setDisabled(!sc); updateProfileSearchButton(); } void MainWindow::on_menuSolver_configurations_triggered(QAction* action) { if (action == ui->actionEditSolverConfig) { return; } auto actions = ui->menuSolver_configurations->actions(); ui->config_window->setCurrentIndex(actions.indexOf(action)); } SolverConfiguration* MainWindow::getCurrentSolverConfig() { return ui->config_window->currentSolverConfig(); } const Solver* MainWindow::getCurrentSolver() { auto sc = getCurrentSolverConfig(); if (!sc) { return nullptr; } return &sc->solverDefinition; } bool MainWindow::requireMiniZinc() { if (!MznDriver::get().isValid()) { int ret = QMessageBox::warning(this, "MiniZinc IDE", "Could not find the minizinc executable.\nDo you want to open the solver settings dialog?", QMessageBox::Ok | QMessageBox::Cancel); if (ret == QMessageBox::Ok) { on_actionManage_solvers_triggered(); } return false; } return true; } void MainWindow::on_moocChanged(const MOOCAssignment* mooc) { if (mooc) { ui->actionSubmit_to_MOOC->setVisible(true); ui->actionSubmit_to_MOOC->setText("Submit to " + mooc->moocName); QIcon icon = mooc->moocName == "Coursera" ? QIcon(":/icons/images/coursera.png") : QIcon(":/icons/images/application-certificate.png"); ui->actionSubmit_to_MOOC->setIcon(icon); } else { ui->actionSubmit_to_MOOC->setVisible(false); } } QStringList MainWindow::getOpenFiles() { QStringList ret; for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce) { ret << ce->filepath; } else { ret << ""; } } return ret; } QList MainWindow::codeEditors() { QList ret; for (int i=0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce) { ret << ce; } } return ret; } void MainWindow::on_projectBrowser_runRequested(const QStringList& files) { QString model; QStringList data; QString checker; auto sc = getCurrentSolverConfig(); for (auto& f : files) { if (f.endsWith(".mpc")) { int i = ui->config_window->findConfigFile(f); if (i != -1) { sc = ui->config_window->solverConfigs()[i]; } } else if (f.endsWith(".mzc.mzn") || f.endsWith(".mzc")) { checker = f; } else if (model.isEmpty() && f.endsWith(".mzn")) { model = f; } else { data << f; } } compileOrRun(CM_RUN, sc, model, data, checker); } void MainWindow::on_projectBrowser_openRequested(const QStringList& files) { QStringList openFiles = getOpenFiles(); for (auto& f : files) { if (f.endsWith(".mpc")) { int i = 0; for (auto sc : ui->config_window->solverConfigs()) { if (sc->paramFile == f) { ui->config_window->setCurrentIndex(i); break; } i++; } continue; } int index = openFiles.indexOf(f); if (index == -1) { createEditor(f, false, false, false, true); } else { ui->tabWidget->setCurrentIndex(index); } } } void MainWindow::on_projectBrowser_removeRequested(const QStringList& files) { bool modified = false; QList editors; for (int i = 0; itabWidget->count(); i++) { CodeEditor* ce = qobject_cast(ui->tabWidget->widget(i)); if (ce && files.contains(QFileInfo(ce->filepath).absoluteFilePath())) { editors << ce; if (ce->document()->isModified()) { modified = true; } } } QVector scIndexes; for (auto& file : files) { if (file.endsWith(".mpc")) { int i = ui->config_window->findConfigFile(file); scIndexes << i; if (ui->config_window->solverConfigs()[i]->modified) { modified = true; } } } std::sort(scIndexes.begin(), scIndexes.end(), std::greater()); if (modified) { int ret = QMessageBox::warning(this, "MiniZinc IDE", "There are modified documents.\nDo you want to discard the changes or cancel?", QMessageBox::Discard | QMessageBox::Cancel); if (ret == QMessageBox::Cancel) { return; } } else { int ret = QMessageBox::warning(this, "MiniZinc IDE", "Are you sure you wish to remove the selected file(s) from the project?", QMessageBox::Ok | QMessageBox::Cancel); if (ret == QMessageBox::Cancel) { return; } } for (auto ce : editors) { ce->document()->setModified(false); ui->tabWidget->removeTab(ui->tabWidget->indexOf(ce)); IDE::instance()->removeEditor(ce->filepath,ce); delete ce; } for (auto i : scIndexes) { ui->config_window->removeConfig(i); } getProject().remove(files); getProject().openTabsChanged(getOpenFiles(), ui->tabWidget->currentIndex()); } void MainWindow::on_actionSave_solver_configuration_triggered() { ui->config_window->saveConfig(); } void MainWindow::on_actionSave_all_solver_configurations_triggered() { auto& configs = ui->config_window->solverConfigs(); for (int i = 0; i < configs.count(); i++) { if (!configs[i]->isBuiltin && configs[i]->modified) { ui->config_window->saveConfig(i); } } } void MainWindow::stop() { ui->actionStop->trigger(); } void MainWindow::on_actionShow_search_profiler_triggered() { if (!conductor) { auto profiler_layout = new QGridLayout(this); ui->cpprofiler->setLayout(profiler_layout); cpprofiler::Options opts; conductor = new cpprofiler::Conductor(opts, this); conductor->setDarkMode(darkMode); conductor->setWindowFlags(Qt::Widget); connect(conductor, &cpprofiler::Conductor::executionStart, [=] (cpprofiler::Execution* e) { ui->outputWidget->associateProfilerExecution(e->id()); }); connect(conductor, &cpprofiler::Conductor::showExecutionWindow, this, &MainWindow::showExecutionWindow); connect(conductor, &cpprofiler::Conductor::showMergeWindow, this, &MainWindow::showMergeWindow); profiler_layout->addWidget(conductor); } if (ui->cpprofiler_dockWidget->isVisible()) { ui->cpprofiler_dockWidget->hide(); } else { ui->cpprofiler_dockWidget->show(); } } void MainWindow::on_actionProfile_search_triggered() { if (!ui->cpprofiler_dockWidget->isVisible()) { on_actionShow_search_profiler_triggered(); } QStringList args; args << "--cp-profiler" << QString::number(conductor->getNextExecId()) + "," + QString::number(conductor->getListenPort()); compileOrRun(CM_RUN, nullptr, QString(), QStringList(), QString(), args); } void MainWindow::showExecutionWindow(cpprofiler::ExecutionWindow& e) { e.setWindowFlags(Qt::Widget); e.setParent(this); for (int i = 0; i < ui->tabWidget->count(); i++) { if (ui->tabWidget->widget(i) == &e) { ui->tabWidget->setCurrentIndex(i); return; } } auto name = QString::fromStdString(e.execution().name()); ui->tabWidget->addTab(&e, name); ui->tabWidget->setCurrentIndex(ui->tabWidget->count()-1); } void MainWindow::showMergeWindow(cpprofiler::analysis::MergeWindow& m) { m.setWindowFlags(Qt::Widget); m.setParent(this); for (int i = 0; i < ui->tabWidget->count(); i++) { if (ui->tabWidget->widget(i) == &m) { ui->tabWidget->setCurrentIndex(i); return; } } auto name = QString::fromStdString("Merge result"); ui->tabWidget->addTab(&m, name); ui->tabWidget->setCurrentIndex(ui->tabWidget->count()-1); } void MainWindow::on_cpprofiler_dockWidget_visibilityChanged(bool visible) { if (visible) { ui->actionShow_search_profiler->setText("Hide search profiler"); } else { ui->actionShow_search_profiler->setText("Show search profiler"); } } void MainWindow::updateProfileSearchButton() { auto sc = getCurrentSolverConfig(); ui->actionProfile_search->setDisabled(processRunning || !curEditor || !sc || !sc->solverDefinition.stdFlags.contains("--cp-profiler")); } void MainWindow::on_progressOutput(float progress) { progressBar->setHidden(false); progressBar->setValue(static_cast(progress)); } void MainWindow::on_minizincError(const QJsonObject& error) { bool isError = error["type"].toString() == "error"; auto& currentTheme = IDE::instance()->themeManager->current(); auto color = isError ? currentTheme.errorColor.get(darkMode) : currentTheme.warningColor.get(darkMode); QString messageType = isError ? "Errors" : "Warnings"; auto stack = error["stack"].toArray(); if (!stack.empty()) { QString lastFile = ""; int lastLine = -1; for (auto it : stack) { auto entry = it.toObject(); auto loc = entry["location"].toObject(); if (loc["filename"].toString() != lastFile || loc["firstLine"].toInt() != lastLine) { lastFile = loc["filename"].toString(); lastLine = loc["firstLine"].toInt(); ui->outputWidget->addHtml(locationToLink( loc["filename"].toString(), loc["firstLine"].toInt(), loc["firstColumn"].toInt(), loc["lastLine"].toInt(), loc["lastColumn"].toInt(), color ), messageType); ui->outputWidget->addText("\n", messageType); } ui->outputWidget->addText(QString(entry["isCompIter"].toBool() ? " with " : " in ") + entry["description"].toString(), messageType); ui->outputWidget->addText("\n", messageType); } } else if (error["location"].isObject()) { auto loc = error["location"].toObject(); ui->outputWidget->addHtml(locationToLink( loc["filename"].toString(), loc["firstLine"].toInt(), loc["firstColumn"].toInt(), loc["lastLine"].toInt(), loc["lastColumn"].toInt(), color ), messageType); ui->outputWidget->addText(":\n", messageType); } QString what = error["what"].toString(); QString message; if (error["includedFrom"].isArray()) { for (auto it : error["includedFrom"].toArray()) { message += QString(" (included from %1)\n").arg(it.toString()); } } if (isError) { message += "MiniZinc: "; } else { message += "Warning: "; } if (error["what"].isString()) { message += QString("%1: ").arg(what); } message += QString("%1\n").arg(error["message"].toString()); if (what == "cyclic include error") { for (auto it : error["cycle"].toArray()) { message += QString(" %1\n").arg(it.toString()); } } ui->outputWidget->addText(message, messageType); } QString MainWindow::locationToLink(const QString& file, int firstLine, int firstColumn, int lastLine, int lastColumn, const QColor& color) { QString label = QFileInfo(file).baseName(); if (file.endsWith("untitled_model.mzn")) { QFileInfo fi(file); for (QTemporaryDir* d : cleanupTmpDirs) { QFileInfo tmpFileInfo(d->path() + "/untitled_model.mzn"); if (fi.canonicalFilePath( )== tmpFileInfo.canonicalFilePath()) { label = "Playground"; break; } } } QString position; if (firstLine == lastLine) { if (firstColumn == lastColumn) { position = QString("%1.%2") .arg(firstLine) .arg(firstColumn); } else { position = QString("%1.%2-%3") .arg(firstLine) .arg(firstColumn) .arg(lastColumn); } } else { position = QString("%1-%2.%3-%4") .arg(firstLine) .arg(firstColumn) .arg(lastLine) .arg(lastColumn); } QUrl url = QUrl::fromLocalFile(file); url.setQuery(QString("line=%1&column=%2").arg(firstLine).arg(firstColumn)); url.setScheme("err"); return QString("%3:%4") .arg(color.name()) .arg(url.toString()) .arg(label) .arg(position); } void MainWindow::startVisualisation(const QString& model, const QStringList& data, const QString& key, const QString& url, const QJsonValue& userData, MznProcess* proc) { QSettings settings; settings.beginGroup("ide"); bool reuseVis = settings.value("reuseVis", false).toBool(); int httpPort = settings.value("visPort", 3000).toInt(); int wsPort = settings.value("visWsPort", 3100).toInt(); bool printUrl = settings.value("printVisUrl", false).toBool(); settings.endGroup(); if (server == nullptr) { server = new Server(this); } try { server->listen(httpPort, wsPort); } catch (ServerError& e) { QMessageBox::warning(this, "MiniZinc IDE", e.message(), QMessageBox::Ok); return; } QFileInfo modelFileInfo(model); QStringList files({modelFileInfo.fileName()}); for (auto& df : data) { QFileInfo fi(df); files << fi.fileName(); } auto workingDir = modelFileInfo.canonicalPath(); auto label = files.join(", "); QStringList roots({workingDir, MznDriver::get().mznStdlibDir()}); vis_connector = server->addConnector(label, roots); connect(vis_connector, &VisConnector::solveRequested, [=] (const QString& modelFile, bool dataFilesGiven, const QStringList& dataFiles, const QVariantMap& options) { auto* origSc = getCurrentSolverConfig(); if (origSc == nullptr) { return; } QStringList df; bool modelFileGiven = !modelFile.isEmpty(); auto m = modelFileGiven ? workingDir + "/" + modelFile : model; if (!IDEUtils::isChildPath(workingDir, m)) { return; } if (dataFilesGiven) { for (const auto& it : dataFiles) { auto d = workingDir + "/" + it; if (!IDEUtils::isChildPath(workingDir, d)) { return; } df << d; } } else if (!modelFileGiven) { // No model, no data, so use previous df = data; } SolverConfiguration rsc(*origSc); QMapIterator i(options); while (i.hasNext()) { i.next(); rsc.extraOptions[i.key()] = i.value(); } if (processRunning) { proc->terminate(); } run(rsc, m, df); }); connect(proc, &MznProcess::traceOutput, this, [=] (const QString& section, const QVariant& message) { if (section.startsWith("mzn_vis_")) { auto obj = message.toJsonObject(); vis_connector->addWindow(section, obj["url"].toString(), obj["userData"]); } }); connect(proc, &MznProcess::solutionOutput, [=](const QVariantMap& output, const QStringList& sections, qint64 time) { QJsonObject solution; for (auto it = output.begin(); it != output.end(); it++) { if (it.key().startsWith("mzn_vis_")) { solution[it.key()] = it.value().toJsonValue(); } } vis_connector->addSolution(solution, time == -1 ? proc->elapsedTime() : time); }); connect(proc, &MznProcess::finalStatus, [=](const QString& status, qint64 time) { vis_connector->setFinalStatus(status, time == -1 ? proc->elapsedTime() : time); }); connect(proc, &MznProcess::finished, vis_connector, &VisConnector::setFinished); vis_connector->addWindow(key, url, userData); ui->outputWidget->associateServerUrl(vis_connector->url().toString()); if (reuseVis) { QJsonObject obj({{"event", "navigate"}, {"url", vis_connector->url().toString()}}); if (!server->sendToLastClient(QJsonDocument(obj))) { QDesktopServices::openUrl(vis_connector->url()); } } else { QDesktopServices::openUrl(vis_connector->url()); } if (printUrl) { auto url = vis_connector->url().toString(); auto urlMessage = QString("%1").arg(url); ui->outputWidget->addText("Visualisation: ", ui->outputWidget->infoCharFormat(), "Visualisation"); ui->outputWidget->addHtml(urlMessage, "Visualisation"); ui->outputWidget->addText("\n", ui->outputWidget->infoCharFormat(), "Visualisation"); } } ================================================ FILE: MiniZincIDE/mainwindow.h ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "codeeditor.h" #include "solver.h" #include "paramdialog.h" #include "project.h" #include "moocsubmission.h" #include "solver.h" #include "process.h" #include "codechecker.h" #include "profilecompilation.h" #include "server.h" #include "theme.h" #include "../cp-profiler/src/cpprofiler/conductor.hh" #include "../cp-profiler/src/cpprofiler/execution_window.hh" #include "../cp-profiler/src/cpprofiler/analysis/merge_window.hh" namespace Ui { class MainWindow; } class FindDialog; class MainWindow; class QNetworkReply; class QTextStream; class MainWindow : public QMainWindow { Q_OBJECT friend class IDE; friend class TestIDE; public: explicit MainWindow(const QString& project = QString()); explicit MainWindow(const QStringList& files); ~MainWindow(); private: enum CompileMode { CM_RUN, CM_COMPILE, CM_PROFILE }; void init(const QString& project); void compileOrRun( CompileMode cm, const SolverConfiguration* sc = nullptr, const QString& model = QString(), const QStringList& data = QStringList(), const QString& checker = QString(), const QStringList& extraArgs = QStringList()); bool getModelParameters(const SolverConfiguration& sc, const QString& model, QStringList& data, const QStringList& extraArgs, QStringList& inlineData); QString currentModelFile(void); bool promptSaveModified(void); void setEditorMenuItemsEnabled(bool enabled); signals: /// emitted when compilation and running of a model has finished void finished(); /// emitted when the window is going to close void terminating(); public slots: void openFile(const QString &path = QString(), bool openAsModified=false, bool focus=true); void stop(); void setTheme(const Theme& theme, bool darkMode); void initTheme(); void closeFindWidget(); private slots: void on_actionClose_triggered(); void on_actionOpen_triggered(); void tabCloseRequest(int); void tabChange(int); void on_actionRun_triggered(); void procFinished(int, qint64 time); void procFinished(int); void on_actionSave_triggered(); void on_actionQuit_triggered(); void statusTimerEvent(qint64 time); void on_actionCompile_triggered(); void openCompiledFzn(const QString& file); void profileCompiledFzn(const QVector& timing, const QVector& paths); void on_actionSave_as_triggered(); void on_actionClear_output_triggered(); void on_actionBigger_font_triggered(); void on_actionSmaller_font_triggered(); void on_actionAbout_MiniZinc_IDE_triggered(); void anchorClicked(const QUrl&); void on_actionDefault_font_size_triggered(); void on_actionManage_solvers_triggered(bool addNew=false); void on_actionFind_triggered(); void on_actionReplace_triggered(); void on_actionGo_to_line_triggered(); void on_actionShift_left_triggered(); void on_actionShift_right_triggered(); void on_actionHelp_triggered(); void on_actionNew_project_triggered(); void on_actionSave_project_triggered(); void on_actionSave_project_as_triggered(); void on_actionClose_project_triggered(); void on_actionFind_next_triggered(); void on_actionFind_previous_triggered(); void on_actionSave_all_triggered(); void on_actionSave_solver_configuration_triggered(); void on_actionSave_all_solver_configurations_triggered(); void on_action_Un_comment_triggered(); void on_actionOnly_editor_triggered(); void on_actionSplit_triggered(); void on_actionPrevious_tab_triggered(); void on_actionNext_tab_triggered(); void recentFileMenuAction(QAction*); void recentProjectMenuAction(QAction*); void on_actionHide_tool_bar_triggered(); void on_actionToggle_profiler_info_triggered(); void on_actionShow_project_explorer_triggered(); void on_actionNewModel_file_triggered(); void on_actionNewData_file_triggered(); void on_actionSubmit_to_MOOC_triggered(); void fileRenamed(const QString&, const QString&); void onClipboardChanged(); void editor_cursor_position_changed(); void showWindowMenu(void); void windowMenuSelected(QAction*); void on_actionCheat_Sheet_triggered(); void check_code(); void moocFinished(int); void on_actionEditSolverConfig_triggered(); void on_b_next_clicked(); void on_b_prev_clicked(); void on_b_replacefind_clicked(); void on_b_replace_clicked(); void on_b_replaceall_clicked(); void on_closeFindWidget_clicked(); void on_find_textEdited(const QString &arg1); void on_actionProfile_compilation_triggered(); void on_configWindow_dockWidget_visibilityChanged(bool visible); void on_config_window_itemsChanged(const QStringList& items); void on_config_window_selectedIndexChanged(int ); void on_moocChanged(const MOOCAssignment* mooc); void on_projectBrowser_runRequested(const QStringList& files); void on_projectBrowser_openRequested(const QStringList& files); void on_projectBrowser_removeRequested(const QStringList& files); void on_actionShow_search_profiler_triggered(); void on_actionProfile_search_triggered(); void showExecutionWindow(cpprofiler::ExecutionWindow& e); void showMergeWindow(cpprofiler::analysis::MergeWindow& m); void on_cpprofiler_dockWidget_visibilityChanged(bool visible); void on_menuSolver_configurations_triggered(QAction* action); void on_progressOutput(float progress); void on_minizincError(const QJsonObject& error); protected: virtual void closeEvent(QCloseEvent*); virtual void dragEnterEvent(QDragEnterEvent *); virtual void dropEvent(QDropEvent *); bool eventFilter(QObject *, QEvent *); public: QString currentSolver(void) const; QString currentSolverConfigName(void); bool checkSolutions(void) const; void setCheckSolutions(bool b); SolverConfiguration* getCurrentSolverConfig(void); const Solver* getCurrentSolver(void); void compileSolutionChecker(const QString& checker); void compile(const SolverConfiguration& sc, const QString& model, const QStringList& data = QStringList(), const QStringList& extraArgs = QStringList(), const QStringList& inlineData = QStringList(), bool profile = false); void run(const SolverConfiguration& sc, const QString& model, const QStringList& data = QStringList(), const QStringList& extraArgs = QStringList(), const QStringList& inlineData = QStringList(), QTextStream* ts = nullptr); QList codeEditors(); bool isDarkMode() const { return darkMode; } private: Ui::MainWindow *ui; CodeEditor* curEditor; CodeChecker* code_checker; CodeEditor* curCheckEditor; QProgressBar* progressBar; QLabel* statusLabel; QLabel* statusLineColLabel; QFont editorFont; int indentSize; bool useTabs; bool darkMode; QVector solvers; QStringList currentAdditionalDataFiles; QTemporaryDir* tmpDir; QVector cleanupTmpDirs; QVector cleanupProcesses; QTextCursor incrementalFindCursor; bool saveBeforeRunning; ParamDialog* paramDialog; bool profileInfoVisible; Project* project; int newFileCounter; QComboBox* solverConfCombo; QAction* fakeRunAction; QAction* fakeStopAction; QAction* fakeCompileAction; QAction* minimizeAction; MOOCSubmission* moocSubmission; bool processRunning; QToolButton* runButton; cpprofiler::Conductor* conductor = nullptr; Server* server = nullptr; VisConnector* vis_connector = nullptr; void createEditor(const QString& path, bool openAsModified, bool isNewFile, bool readOnly=false, bool focus=true); enum ConfMode { CONF_CHECKARGS, CONF_COMPILE, CONF_RUN }; QStringList parseConf(const ConfMode& confMode, const QString& modelFile); void saveFile(CodeEditor* ce, const QString& filepath); void saveProject(const QString& filepath); void loadProject(const QString& filepath); void setEditorFont(QFont font); void setEditorIndent(int indentSize, bool useTabs); void setEditorWordWrap(QTextOption::WrapMode mode); void setLastPath(const QString& s); QString getLastPath(void); QString setElapsedTime(qint64 elapsed_t); void checkDriver(); void updateRecentProjects(const QString& p); void updateRecentFiles(const QString& p); void updateUiProcessRunning(bool pr); void highlightPath(QString& path, int index); QVector collectCodeEditors(QVector& locs); void find(bool fwd, bool forceNoWrapAround=false); bool requireMiniZinc(void); void outputStdErr(const QString& line); QStringList getOpenFiles(void); void updateProfileSearchButton(void); QString locationToLink(const QString& filename, int firstLine, int firstColumn, int lastLine, int lastColumn, const QColor& color); void startVisualisation(const QString& model, const QStringList& data, const QString& key, const QString& url, const QJsonValue& userData, MznProcess* proc); public: void addOutput(const QString& s, bool html=true); void openProject(const QString& fileName); bool isEmptyProject(void); Project& getProject(void) { return *project; } }; #endif // MAINWINDOW_H ================================================ FILE: MiniZincIDE/mainwindow.ui ================================================ MainWindow 0 0 1490 1609 :/mznide.ico:/mznide.ico 256 256 true 0 0 0 0 0 0 Qt::StrongFocus 0 true true true true 0 0 Configuration 16777215 80 QFrame::NoFrame QFrame::Raised 0 0 0 0 0 0 0 Replace && find Ignore case 300 20 Replace Replace 300 20 Replace all Find Close not found Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Regular expression Wrap around Qt::Horizontal 40 20 Next true Previous 0 0 1490 20 true &File Recent Files Recent Projects Edit Find MiniZinc Solver configurations View Help Window true 0 0 QDockWidget::AllDockWidgetFeatures Qt::BottomDockWidgetArea|Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea Output 8 toolBar false 32 32 Qt::ToolButtonTextUnderIcon false TopToolBarArea false false QDockWidget::DockWidgetMovable Qt::BottomDockWidgetArea|Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea Project 2 0 0 0 0 Configuration 2 0 0 0 0 QFrame::NoFrame true 0 0 68 786 0 0 0 0 Search Profiler 2 0 0 0 0 :/icons/images/document-open.png:/icons/images/document-open.png &Open... Ctrl+O false :/icons/images/document-save.png:/icons/images/document-save.png Save Ctrl+S false Save as... Ctrl+Shift+S false Close file Ctrl+W false :/icons/images/edit-undo.png:/icons/images/edit-undo.png Undo Ctrl+Z false :/icons/images/edit-redo.png:/icons/images/edit-redo.png Redo Ctrl+Shift+Z false :/icons/images/edit-cut.png:/icons/images/edit-cut.png Cut Ctrl+X false :/icons/images/edit-copy.png:/icons/images/edit-copy.png Copy false :/icons/images/edit-paste.png:/icons/images/edit-paste.png Paste Ctrl+V false Select All Ctrl+A Go to line... Ctrl+L :/icons/images/media-playback-start.png:/icons/images/media-playback-start.png Run Run Ctrl+R Quit :/icons/images/media-playback-stop.png:/icons/images/media-playback-stop.png Stop Ctrl+E Compile Ctrl+B Clear output Ctrl+K Bigger font Ctrl++ Smaller font Ctrl+- About MiniZinc IDE Default font size Preferences Replace... Ctrl+Shift+F Select font... Ctrl+T Only editor Ctrl+1 Show output Show output Ctrl+2 :/icons/images/format-indent-less.png:/icons/images/format-indent-less.png Shift selection left Shift left Ctrl+[ false :/icons/images/format-indent-more.png:/icons/images/format-indent-more.png Shift selection right Shift right Ctrl+] false Help... New project Ctrl+Shift+N Save project Save project as... Close project Ctrl+Shift+W :/icons/images/edit-find-replace.png:/icons/images/edit-find-replace.png Find... Ctrl+F false Find next Ctrl+G Find previous Ctrl+Shift+G Save all (Un)comment Ctrl+/ Previous tab Next tab Clear menu Clear menu Hide tool bar :/icons/images/package-x-generic.png:/icons/images/package-x-generic.png Show project explorer false :/icons/images/document-new.png:/icons/images/document-new.png New model New model Ctrl+N false :/icons/images/document-new.png:/icons/images/document-new.png New data New data false :/icons/images/coursera.png:/icons/images/coursera.png Submit to Coursera Ctrl+U Cheat Sheet... true default :/icons/images/applications-system.png:/icons/images/applications-system.png Show configuration editor... Show solver configuration editor Ctrl+Shift+C Profile compilation Ctrl+; Profiler Info Ctrl+. Save solver configuration Save all solver configurations Show search profiler false Profile search Default Blueberry Mango OutputDockWidget QDockWidget
outputdockwidget.h
1
EscLineEdit QLineEdit
esclineedit.h
ProjectBrowser QWidget
projectbrowser.h
1
ConfigWindow QWidget
configwindow.h
OutputWidget QWidget
outputwidget.h
1
tabWidget outputWidget find replace b_next b_prev closeFindWidget b_replace b_replacefind b_replaceall check_re check_case check_wrap
================================================ FILE: MiniZincIDE/minizincide.qrc ================================================ images/about.html images/mznicon.png cheat_sheet.mzn mznide.ico dark_mode.css images/document-new.png images/document-open.png images/document-save.png images/edit-find-replace.png images/edit-paste.png images/edit-redo.png images/edit-undo.png images/format-indent-less.png images/format-indent-more.png images/process-stop.png images/edit-copy.png images/edit-cut.png images/folder.png images/package-x-generic.png images/coursera.png images/coursera@2x.png images/application-certificate.png images/media-playback-start.png images/media-playback-stop.png images/applications-system.png server/index.html server/connector.js ================================================ FILE: MiniZincIDE/moocsubmission.cpp ================================================ #include "moocsubmission.h" #include "ui_moocsubmission.h" #include "ide.h" #include "mainwindow.h" #include "exception.h" #include #include #include #include #include #include #include #include #include #include MOOCAssignment::MOOCAssignment(const QString& fileName) : moocFile(fileName) { QFile f(fileName); QFileInfo fi(f); if (!f.open(QFile::ReadOnly)) { throw FileError("Failed to open mooc file"); } auto bytes = f.readAll(); auto doc = QJsonDocument::fromJson(bytes); if (doc.isObject()) { loadJSON(doc.object(), fi); } else { throw MoocError("Could not parse mooc file"); } } void MOOCAssignment::loadJSON(const QJsonObject& obj, const QFileInfo& fi) { if (obj.isEmpty() || !obj["assignmentKey"].isString() || !obj["name"].isString() || !obj["moocName"].isString() || !obj["moocPasswordString"].isString() || !obj["submissionURL"].isString() || !obj["solutionAssignments"].isArray() || !obj["modelAssignments"].isArray()) { throw MoocError("Could not parse mooc file"); } assignmentKey = obj["assignmentKey"].toString(); name = obj["name"].toString(); moocName = obj["moocName"].toString(); moocPasswordString = obj["moocPasswordString"].toString(); submissionURL = obj["submissionURL"].toString(); submissionTerms = obj["submissionTerms"].toString(); sendMeta = obj["sendMeta"].toBool(false); QJsonArray sols = obj["solutionAssignments"].toArray(); for (int i=0; itoJSON(); QFile file(moocFile); if (file.open(QFile::WriteOnly)) { file.write(QJsonDocument(json).toJson()); file.close(); } }); } } MOOCSubmission::MOOCSubmission(MainWindow* mw0, MOOCAssignment& cp) : QDialog(nullptr), _cur_phase(S_NONE), project(cp), mw(mw0), ui(new Ui::MOOCSubmission) { ui->setupUi(this); setWindowTitle("Submit to "+cp.moocName); ui->loginEmailLabel->setText(cp.moocName+" login email:"); ui->loginTokenLabel->setText(cp.moocPasswordString+":"); ui->courseraTokenWarningLabel->setHidden(cp.moocName!="Coursera"); ui->selectedSolver->setText(mw->currentSolverConfigName()); QVBoxLayout* modelLayout = new QVBoxLayout; ui->modelBox->setLayout(modelLayout); QVBoxLayout* problemLayout = new QVBoxLayout; ui->problemBox->setLayout(problemLayout); QSet mooc_mzns; for (auto& p : project.models) { mooc_mzns << p.model; } for (auto& p : project.problems) { mooc_mzns << p.model; } QStringList nonSubmissionFiles; for (auto* ce : mw->codeEditors()) { if (ce->filename.endsWith(".mzn") && !mooc_mzns.contains(ce->filepath)) { nonSubmissionFiles << ce->filename; } } if (nonSubmissionFiles.empty()) { ui->nonSubmissionGroup->setVisible(false); } else { ui->nonSubmissionLabel->setText(nonSubmissionFiles.join("\n")); ui->nonSubmissionGroup->setVisible(true); } if (project.submissionTerms.isEmpty()) { ui->submissionTerms_groupBox->setVisible(false); ui->runButton->setEnabled(true); } else { ui->submissionTerms_textBrowser->setText(cp.submissionTerms); ui->submissionTerms_checkBox->setChecked(false); ui->runButton->setEnabled(false); ui->submissionTerms_groupBox->setVisible(true); } for (int i=0; isetChecked(true); cb->setEnabled(!item.required); modelLayout->addWidget(cb); } for (int i=0; isetChecked(true); cb->setEnabled(!item.required); problemLayout->addWidget(cb); } if (project.models.empty()) modelLayout->addWidget(new QLabel("none")); if (project.problems.empty()) problemLayout->addWidget(new QLabel("none")); _output_stream.setString(&_output_string); QSettings settings; settings.beginGroup("coursera"); ui->storePassword->setChecked(settings.value("storeLogin",false).toBool()); ui->login->setText(settings.value("courseraEmail").toString()); settings.beginGroup(project.assignmentKey); ui->password->setText(settings.value("token").toString()); settings.endGroup(); settings.endGroup(); } MOOCSubmission::~MOOCSubmission() { QSettings settings; settings.beginGroup("coursera"); bool storeLogin = ui->storePassword->isChecked(); settings.setValue("storeLogin", storeLogin); if (storeLogin) { settings.setValue("courseraEmail",ui->login->text()); settings.beginGroup(project.assignmentKey); settings.setValue("token",ui->password->text()); settings.endGroup(); } settings.endGroup(); delete ui; } void MOOCSubmission::disableUI() { ui->loginGroup->setEnabled(false); ui->modelBox->setEnabled(false); ui->problemBox->setEnabled(false); ui->submissionTerms_checkBox->setEnabled(false); ui->runButton->setText("Abort"); } void MOOCSubmission::enableUI() { ui->loginGroup->setEnabled(true); ui->modelBox->setEnabled(true); ui->problemBox->setEnabled(true); ui->submissionTerms_checkBox->setEnabled(true); ui->runButton->setText("Run and submit"); } void MOOCSubmission::cancelOperation() { switch (_cur_phase) { case S_NONE: return; case S_WAIT_PWD: disconnect(mw, &MainWindow::finished, this, &MOOCSubmission::rcvLoginCheckResponse); break; case S_WAIT_SOLVE: disconnect(mw, &MainWindow::finished, this, &MOOCSubmission::solverFinished); mw->stop(); break; case S_WAIT_SUBMIT: disconnect(reply, &QNetworkReply::finished, this, &MOOCSubmission::rcvSubmissionResponse); break; } ui->textBrowser->insertPlainText("Aborted.\n"); _cur_phase = S_NONE; enableUI(); } void MOOCSubmission::reject() { if (_cur_phase != S_NONE && QMessageBox::warning(this, "MiniZinc IDE", "Do you want to close this window and abort the "+project.moocName+" submission?", QMessageBox::Close| QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Cancel) { return; } cancelOperation(); QDialog::reject(); } void MOOCSubmission::solveNext() { int n_problems = project.problems.size(); bool done = false; do { _current_model++; if (_current_model < n_problems) { QCheckBox* cb = qobject_cast(ui->problemBox->layout()->itemAt(_current_model)->widget()); done = cb->isChecked(); } else { done = true; } } while (!done); if (_current_model < n_problems) { MOOCAssignmentItem& item = project.problems[_current_model]; connect(mw, &MainWindow::finished, this, &MOOCSubmission::solverFinished); ui->textBrowser->insertPlainText("Running "+item.name+" with time out " + QString().setNum(item.timeout)+ "s\n"); _cur_phase = S_WAIT_SOLVE; _output_string = ""; mw->addOutput("
Running "+project.moocName+" submission "+item.name+"

\n"); auto sc = mw->getCurrentSolverConfig(); SolverConfiguration solverConfig(*sc); solverConfig.timeLimit = item.timeout * 1000; // Override config time limit QStringList files = { item.data }; // When we have a checker, use --output-mode checker auto mzc = item.model; mzc.replace(mzc.length() - 1, 1, "c"); if (mw->getProject().contains(mzc)) { files << mzc; solverConfig.extraOptions["output-mode"] = "checker"; } else if (mw->getProject().contains(mzc + ".mzn")) { files << mzc + ".mzn"; solverConfig.extraOptions["output-mode"] = "checker"; } mw->run(solverConfig, item.model, files, QStringList(), QStringList(), &_output_stream); return; } else { submitToMOOC(); } } void MOOCSubmission::submitToMOOC() { QUrl url(project.submissionURL); QNetworkRequest request; request.setUrl(url); request.setRawHeader(QByteArray("Cache-Control"),QByteArray("no-cache")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); // add models QStringList allfiles = mw->getProject().files(); for (int i=0; i(ui->modelBox->layout()->itemAt(i)->widget()); if (cb->isChecked()) { QFile file(item.model); if (file.open(QFile::ReadOnly | QFile::Text)) { QJsonObject output; QTextStream ts(&file); output["output"] = ts.readAll(); _parts[item.id] = output; } else { ui->textBrowser->insertPlainText("Error: could not open "+item.name+"\n"); ui->textBrowser->insertPlainText("Skipping.\n"); } } } _submission["assignmentKey"] = project.assignmentKey; _submission["secret"] = ui->password->text(); _submission["submitterEmail"] = ui->login->text(); _submission["parts"] = _parts; if (project.sendMeta) { QJsonObject metadata; metadata["version"] = MznDriver::get().version().toString(); metadata["versionString"] = MznDriver::get().minizincVersionString(); metadata["solver"] = mw->getCurrentSolver()->id + " " + mw->getCurrentSolver()->version; metadata["arch"] = QSysInfo::currentCpuArchitecture(); metadata["kernelType"] = QSysInfo::kernelType(); metadata["kernelVersion"] = QSysInfo::kernelVersion(); metadata["productType"] = QSysInfo::productType(); metadata["productVersion"] = QSysInfo::productVersion(); metadata["prettyProductName"] = QSysInfo::prettyProductName(); _submission["metadata"] = metadata; } if (project.history != nullptr) { _submission["history"] = project.history->toJSON(); } QJsonDocument doc(_submission); _cur_phase = S_WAIT_SUBMIT; reply = IDE::instance()->networkManager->post(request,doc.toJson()); #if QT_VERSION >= 0x060000 connect(reply, &QNetworkReply::errorOccurred, this, &MOOCSubmission::rcvErrorResponse); #else connect(reply, QOverload::of(&QNetworkReply::error), this, &MOOCSubmission::rcvErrorResponse); #endif connect(reply, &QNetworkReply::finished, this, &MOOCSubmission::rcvSubmissionResponse); ui->textBrowser->insertPlainText("Submitting to "+project.moocName+" for grading...\n"); } void MOOCSubmission::rcvSubmissionResponse() { disconnect(reply, &QNetworkReply::finished, this, &MOOCSubmission::rcvSubmissionResponse); #if QT_VERSION >= 0x060000 disconnect(reply, &QNetworkReply::errorOccurred, this, &MOOCSubmission::rcvErrorResponse); #else disconnect(reply, QOverload::of(&QNetworkReply::error), this, &MOOCSubmission::rcvErrorResponse); #endif reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (status >= 400) { ui->textBrowser->insertPlainText(QString("Error %1: ").arg(status)); } else if (project.history != nullptr) { project.history->commit(); } QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); if (doc.object().contains("message")) { ui->textBrowser->insertPlainText("== "+doc.object()["message"].toString()+"\n"); } if (doc.object().contains("details")) { QJsonObject details = doc.object()["details"].toObject(); if (details.contains("learnerMessage")) { ui->textBrowser->insertPlainText("== "+details["learnerMessage"].toString()+"\n"); } } ui->textBrowser->insertPlainText("Done.\n"); _cur_phase = S_NONE; ui->runButton->setText("Done."); ui->runButton->setEnabled(false); ui->buttonBox->button(QDialogButtonBox::Close)->setDefault(true); } void MOOCSubmission::solverFinished() { disconnect(mw, &MainWindow::finished, this, &MOOCSubmission::solverFinished); QStringList solutions = _output_string.split("----------"); if (solutions.size() >= 2) { _output_string = solutions[solutions.size()-2]+"----------"+solutions[solutions.size()-1]; } if (_output_string.size()==0 || _output_string[_output_string.size()-1] != '\n') _output_string += "\n"; QJsonObject output; output["output"] = _output_string; _parts[project.problems[_current_model].id] = output; ui->textBrowser->insertPlainText("Finished\n"); solveNext(); } void MOOCSubmission::on_runButton_clicked() { if (_cur_phase==S_NONE) { ui->textBrowser->clear(); QString email = ui->login->text(); if (email.isEmpty()) { QMessageBox::warning(this, "MiniZinc IDE", "Enter an email address for "+project.moocName+" login!"); return; } if (ui->password->text().isEmpty()) { QMessageBox::warning(this, "MiniZinc IDE", "Enter an assignment key!"); return; } // Send empty request to check password QUrl url(project.submissionURL); QNetworkRequest request; request.setUrl(url); request.setRawHeader(QByteArray("Cache-Control"),QByteArray("no-cache")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QJsonObject checkPwdSubmission; checkPwdSubmission["assignmentKey"] = project.assignmentKey; checkPwdSubmission["secret"] = ui->password->text(); checkPwdSubmission["submitterEmail"] = ui->login->text(); QJsonObject emptyParts; checkPwdSubmission["parts"] = emptyParts; QJsonDocument doc(checkPwdSubmission); _cur_phase = S_WAIT_PWD; disableUI(); reply = IDE::instance()->networkManager->post(request,doc.toJson()); #if QT_VERSION >= 0x060000 connect(reply, &QNetworkReply::errorOccurred, this, &MOOCSubmission::rcvErrorResponse); #else connect(reply, QOverload::of(&QNetworkReply::error), this, &MOOCSubmission::rcvErrorResponse); #endif connect(reply, &QNetworkReply::finished, this, &MOOCSubmission::rcvLoginCheckResponse); ui->textBrowser->insertPlainText("Checking login and assignment token...\n"); } else { cancelOperation(); } } void MOOCSubmission::rcvLoginCheckResponse() { disconnect(reply, &QNetworkReply::finished, this, &MOOCSubmission::rcvLoginCheckResponse); #if QT_VERSION >= 0x060000 disconnect(reply, &QNetworkReply::errorOccurred, this, &MOOCSubmission::rcvErrorResponse); #else disconnect(reply, QOverload::of(&QNetworkReply::error), this, &MOOCSubmission::rcvErrorResponse); #endif reply->deleteLater(); QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); if (doc.object().contains("message") && (doc.object()["message"].toString().endsWith("but found: Set()") || doc.object()["message"].toString().endsWith("Success"))) { ui->textBrowser->insertPlainText("Done.\n"); _current_model = -1; for (int i=0; itextBrowser->insertPlainText("== "+doc.object()["message"].toString()+"\n"); } if (doc.object().contains("details")) { QJsonObject details = doc.object()["details"].toObject(); if (details.contains("learnerMessage")) { ui->textBrowser->insertPlainText(">> "+details["learnerMessage"].toString()+"\n"); } } ui->textBrowser->insertPlainText("Done.\n"); _cur_phase = S_NONE; enableUI(); } } void MOOCSubmission::rcvErrorResponse(QNetworkReply::NetworkError e) { switch (e) { case QNetworkReply::ProtocolInvalidOperationError: case QNetworkReply::AuthenticationRequiredError: // ProtocolInvalidOperationError expected when checking auth succeeds // AuthenticationRequiredError expected when checking auth fails break; default: // Unknown networking error // TODO: can probably remove this check since it should be covered by the ProtocolInvalidOperationError if (!reply->errorString().endsWith("Bad Request")) { ui->textBrowser->insertPlainText("Error:\n" + reply->errorString() + "\n\n"); } } } void MOOCSubmission::on_storePassword_toggled(bool checked) { if (!checked) { QSettings settings; settings.beginGroup("coursera"); settings.remove(""); settings.setValue("storeLogin", false); settings.endGroup(); } } void MOOCSubmission::on_submissionTerms_checkBox_stateChanged(int arg1) { ui->runButton->setEnabled(arg1); } ================================================ FILE: MiniZincIDE/moocsubmission.h ================================================ #ifndef MOOCSUBMISSION_H #define MOOCSUBMISSION_H #include #include #include #include #include #include "history.h" class QNetworkReply; class MainWindow; namespace Ui { class MOOCSubmission; } class MOOCAssignmentItem { public: QString id; QString model; QString data; int timeout; QString name; bool required; MOOCAssignmentItem(QString id0, QString model0, QString data0, QString timeout0, QString name0, bool required0 = false) : id(id0), model(model0), data(data0), timeout(timeout0.toInt()), name(name0), required(required0) {} MOOCAssignmentItem(QString id0, QString model0, QString name0, bool required0 = false) : id(id0), model(model0), timeout(-1), name(name0), required(required0) {} }; class MOOCAssignment { public: QString name; QString assignmentKey; QString moocName; QString moocPasswordString; QString submissionURL; QString submissionTerms; bool sendMeta; QList problems; QList models; History* history = nullptr; QString moocFile; QJsonObject json; MOOCAssignment(void) {} MOOCAssignment(const QString& file); ~MOOCAssignment() { delete history; } private: void loadJSON(const QJsonObject& obj, const QFileInfo& fi); }; class MOOCSubmission : public QDialog { Q_OBJECT friend class TestIDE; public: explicit MOOCSubmission(MainWindow* mw, MOOCAssignment& cp); ~MOOCSubmission(); protected: enum State { S_NONE, S_WAIT_PWD, S_WAIT_SUBMIT, S_WAIT_SOLVE } _cur_phase; int _current_model; QTextStream _output_stream; QString _output_string; QJsonObject _submission; QJsonObject _parts; MOOCAssignment& project; MainWindow* mw; QNetworkReply* reply; void disableUI(void); void enableUI(void); void cancelOperation(void); void solveNext(void); bool checkerEnabled; public slots: void reject(); private slots: void submitToMOOC(); void rcvSubmissionResponse(); void solverFinished(); void on_runButton_clicked(); void rcvLoginCheckResponse(); void rcvErrorResponse(QNetworkReply::NetworkError); void on_storePassword_toggled(bool checked); void on_submissionTerms_checkBox_stateChanged(int arg1); private: Ui::MOOCSubmission *ui; }; #endif // MOOCSUBMISSION_H ================================================ FILE: MiniZincIDE/moocsubmission.ui ================================================ MOOCSubmission 0 0 590 720 0 0 Submit to Coursera 0 0 0 0 0 true 0 0 574 1009 true QGroupBox::title { color: red; } WARNING There are models currently open which are not part of the submission and will not be included: 6 Problems to run and submit Models to submit Login information QLayout::SetMinimumSize Coursera login email: QLayout::SetMinimumSize Assignment token: QLineEdit::Password 0 0 Note that this is your assignment submission token, not your Coursera password. Qt::RichText true QLayout::SetMinimumSize Remember login details Qt::Horizontal 40 20 Selected solver configuration for running models Selected solver Qt::Vertical QSizePolicy::Minimum 20 10 Terms of submission I have read and accept the above terms and conditions Run and submit false true Qt::Vertical QSizePolicy::Minimum 20 10 Submission output Qt::Horizontal QDialogButtonBox::Close buttonBox accepted() MOOCSubmission accept() 248 254 157 274 buttonBox rejected() MOOCSubmission reject() 316 260 286 274 ================================================ FILE: MiniZincIDE/mznide-makefile.plist ================================================ NSPrincipalClass NSApplication CFBundleIconFile mznide.icns CFBundlePackageType APPL NSHighResolutionCapable True CFBundleExecutable MiniZincIDE CFBundleDisplayName MiniZincIDE CFBundleVersion @FULL_VERSION@ CFBundleShortVersionString @FULL_VERSION@ CFBundleIdentifier org.minizinc.MiniZincIDE CFBundleDocumentTypes CFBundleTypeRole Editor CFBundleTypeName MiniZinc Project CFBundleTypeIconFile mznide.icns CFBundleTypeExtensions mzp CFBundleTypeRole Editor CFBundleTypeName FlatZinc instance CFBundleTypeIconFile mznide.icns CFBundleTypeExtensions fzn CFBundleTypeRole Editor CFBundleTypeName MiniZinc Data CFBundleTypeIconFile mznide.icns CFBundleTypeExtensions dzn CFBundleTypeRole Editor CFBundleTypeName MiniZinc model CFBundleTypeIconFile mznide.icns CFBundleTypeExtensions mzn UTExportedTypeDeclarations UTTypeIdentifier org.minizinc.minizinc UTTypeReferenceURL http://www.minizinc.org/ UTTypeDescription MiniZinc model UTTypeIconFile mznide.icns UTTypeConformsTo public.plain-text UTTypeTagSpecification public.filename-extension mzn UTTypeIdentifier org.minizinc.flatzinc UTTypeReferenceURL http://www.minizinc.org/ UTTypeDescription FlatZinc model UTTypeIconFile mznide.icns UTTypeConformsTo public.plain-text UTTypeTagSpecification public.filename-extension fzn UTTypeIdentifier org.minizinc.minizinc-data UTTypeReferenceURL http://www.minizinc.org/ UTTypeDescription MiniZinc data UTTypeIconFile mznide.icns UTTypeConformsTo public.plain-text UTTypeTagSpecification public.filename-extension dzn ================================================ FILE: MiniZincIDE/mznide-xcode.plist ================================================ NSPrincipalClass NSApplication CFBundleIconFile mznide.icns CFBundlePackageType APPL NSHighResolutionCapable True CFBundleExecutable MiniZincIDE CFBundleDisplayName MiniZincIDE CFBundleVersion ${DYLIB_CURRENT_VERSION} CFBundleShortVersionString ${DYLIB_CURRENT_VERSION} CFBundleIdentifier org.minizinc.MiniZincIDE CFBundleDocumentTypes CFBundleTypeRole Editor CFBundleTypeName MiniZinc Project CFBundleTypeIconFile mznide.icns CFBundleTypeExtensions mzp CFBundleTypeRole Editor CFBundleTypeName FlatZinc instance CFBundleTypeIconFile mznide.icns CFBundleTypeExtensions fzn CFBundleTypeRole Editor CFBundleTypeName MiniZinc Data CFBundleTypeIconFile mznide.icns CFBundleTypeExtensions dzn CFBundleTypeRole Editor CFBundleTypeName MiniZinc model CFBundleTypeIconFile mznide.icns CFBundleTypeExtensions mzn UTExportedTypeDeclarations UTTypeIdentifier org.minizinc.minizinc UTTypeReferenceURL http://www.minizinc.org/ UTTypeDescription MiniZinc model UTTypeIconFile mznide.icns UTTypeConformsTo public.plain-text UTTypeTagSpecification public.filename-extension mzn UTTypeIdentifier org.minizinc.flatzinc UTTypeReferenceURL http://www.minizinc.org/ UTTypeDescription FlatZinc model UTTypeIconFile mznide.icns UTTypeConformsTo public.plain-text UTTypeTagSpecification public.filename-extension fzn UTTypeIdentifier org.minizinc.minizinc-data UTTypeReferenceURL http://www.minizinc.org/ UTTypeDescription MiniZinc data UTTypeIconFile mznide.icns UTTypeConformsTo public.plain-text UTTypeTagSpecification public.filename-extension dzn ================================================ FILE: MiniZincIDE/outputdockwidget.cpp ================================================ #include "outputdockwidget.h" #include void OutputDockWidget::closeEvent(QCloseEvent *event) { if (isFloating()) setFloating(false); else hide(); event->ignore(); } ================================================ FILE: MiniZincIDE/outputdockwidget.h ================================================ #ifndef OUTPUTDOCKWIDGET_H #define OUTPUTDOCKWIDGET_H #include class OutputDockWidget : public QDockWidget { Q_OBJECT public: explicit OutputDockWidget(QWidget *parent = 0) : QDockWidget(parent) {} signals: public slots: protected: void closeEvent(QCloseEvent *event); }; #endif // OUTPUTDOCKWIDGET_H ================================================ FILE: MiniZincIDE/outputwidget.cpp ================================================ #include "outputwidget.h" #include "ui_outputwidget.h" #include "highlighter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ideutils.h" #include "highlighter.h" OutputWidget::OutputWidget(QWidget *parent) : QWidget(parent), ui(new Ui::OutputWidget) { ui->setupUi(this); auto* sectionMenu = new QMenu(this); ui->sectionMenu_pushButton->hide(); ui->sectionMenu_pushButton->setMenu(sectionMenu); connect(sectionMenu, &QMenu::aboutToShow, this, [=] () { for (auto* action : sectionMenu->actions()) { action->setChecked(isSectionVisible(action->text())); } }); auto* messageTypeMenu = new QMenu(this); ui->messageTypeMenu_pushButton->hide(); ui->messageTypeMenu_pushButton->setMenu(messageTypeMenu); connect(messageTypeMenu, &QMenu::aboutToShow, this, [=] () { for (auto* action : messageTypeMenu->actions()) { action->setChecked(isMessageTypeVisible(action->text())); } }); ui->textBrowser->installEventFilter(this); ui->textBrowser->viewport()->installEventFilter(this); ui->textBrowser->setOpenLinks(false); connect(ui->textBrowser, &QTextBrowser::anchorClicked, this, &OutputWidget::onAnchorClicked); auto* arrow = new OutputWidgetArrow; arrow->setParent(this); ui->textBrowser->document()->documentLayout()->registerHandler(TextObject::Arrow, arrow); _frame = ui->textBrowser->document()->rootFrame(); _headerTableFormat.setWidth(QTextLength(QTextLength::PercentageLength, 100)); _headerTableFormat.setBorder(0); _headerTableFormat.setCellSpacing(0); _headerTableFormat.setProperty(Property::ToggleFrame, true); QFontMetrics metrics(ui->textBrowser->font()); auto spaceNeeded = metrics.height() + 4; auto cwc = { QTextLength(QTextLength::FixedLength, spaceNeeded) }; _headerTableFormat.setColumnWidthConstraints(cwc); _arrowFormat.setObjectType(TextObject::Arrow); _arrowFormat.setForeground(Qt::gray); _frameFormat.setLeftMargin(spaceNeeded); _frameFormat.setProperty(Property::Expanded, true); _rightAlignBlockFormat.setAlignment(Qt::AlignRight); _rightAlignBlockFormat.setLeftMargin(10); #ifdef Q_OS_MAC ui->toggleAll_pushButton->setMinimumWidth(85); layout()->setSpacing(8); ui->buttons_widget->setAttribute(Qt::WA_LayoutUsesWidgetRect); ui->sectionButtons_widget->setAttribute(Qt::WA_LayoutUsesWidgetRect); ui->messageTypeButtons_widget->setAttribute(Qt::WA_LayoutUsesWidgetRect); ui->sectionMenu_pushButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); ui->messageTypeMenu_pushButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); ui->toggleAll_pushButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); #else setStyleSheet("QPushButton { padding: 2px 6px; }"); #endif _contextMenu = new QMenu(this); _contextMenu->addAction("Copy selected", this, [=] () { copySelectionToClipboard(false); }); _contextMenu->addAction("Copy selected including hidden", this, [=] () { copySelectionToClipboard(true); }); _contextMenu->addSeparator(); _contextMenu->addAction("Select All", this, [=] () { ui->textBrowser->selectAll(); }); connect(ui->textBrowser, &QTextBrowser::customContextMenuRequested, this, &OutputWidget::onBrowserContextMenu); ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu); } OutputWidget::~OutputWidget() { delete ui; } QString OutputWidget::lastTraceLoc(const QString &newTraceLoc) { QString ltl = _lastTraceLoc; _lastTraceLoc = newTraceLoc; return ltl; } void OutputWidget::setTheme(const Theme& theme, bool darkMode) { _defaultCharFormat.setForeground(theme.textColor.get(darkMode)); _noticeCharFormat.setForeground(theme.functionColor.get(darkMode)); _errorCharFormat.setForeground(theme.errorColor.get(darkMode)); _errorCharFormat.setForeground(theme.errorColor.get(darkMode)); _infoCharFormat.setForeground(Qt::gray); _commentCharFormat.setForeground(theme.commentColor.get(darkMode)); ui->textBrowser->viewport()->setStyleSheet(theme.styleSheet(darkMode)); } void OutputWidget::scrollToBottom() { auto* scrollbar = ui->textBrowser->verticalScrollBar(); scrollbar->setValue(scrollbar->maximum()); } void OutputWidget::startExecution(const QString& label) { _checkerOutput.clear(); TextLayoutLock lock(this); auto c = ui->textBrowser->textCursor(); c.movePosition(QTextCursor::End); // Create table containing heading auto* table = c.insertTable(1, 3, _headerTableFormat); auto leftSideCursor = table->cellAt(0, 0).firstCursorPosition(); leftSideCursor.insertText(QString(QChar::ObjectReplacementCharacter), _arrowFormat); auto headerCursor = table->cellAt(0, 1).firstCursorPosition(); headerCursor.insertText(label, noticeCharFormat()); auto rightSideCursor = table->cellAt(0, 2).firstCursorPosition(); rightSideCursor.setBlockFormat(_rightAlignBlockFormat); _statusCursor = rightSideCursor; // Create frame which will contain children c.movePosition(QTextCursor::End); _frame = c.insertFrame(_frameFormat); _hadServerUrl = false; _solutionCount = 0; _effectiveSolutionLimit = _solutionLimit; } void OutputWidget::associateProfilerExecution(int executionId) { _statusCursor.insertHtml(QString("search profiler ") .arg(executionId)); } void OutputWidget::associateServerUrl(const QString& url) { if (!_hadServerUrl){ _statusCursor.insertHtml(QString("visualisation ").arg(url)); _hadServerUrl = true; } } void OutputWidget::addSolution(const QVariantMap& output, const QStringList& order, qint64 time) { TextLayoutLock lock(this); QTextCursor cursor = nextInsertAt(); if (!_checkerOutput.isEmpty()) { cursor.insertText("% Solution checker report:\n", _commentCharFormat); for (auto& it : _checkerOutput) { auto section = it.first; if (section == "raw" || it.second.toString().isEmpty()) { continue; } addSection(section); QTextBlockFormat format; format.setProperty(Property::Section, section); cursor.setBlockFormat(format); cursor.block().setVisible(_sections[section]); if (section == "html" || section.endsWith("_html")) { addHtmlToSection(section, it.second.toString()); } else { auto lines = it.second.toString().split("\n"); if (lines.last().isEmpty()) { lines.pop_back(); } bool first = true; for (auto& line : lines) { addTextToSection(section, (first ? "% " : "\n% ") + line, _commentCharFormat); first = false; } } if (!cursor.block().text().isEmpty()) { cursor.insertBlock(); } } _checkerOutput.clear(); } for (auto& section : order) { if (section == "raw" || output[section].toString().isEmpty()) { continue; } addSection(section); QTextBlockFormat format; format.setProperty(Property::Section, section); cursor.setBlockFormat(format); cursor.block().setVisible(_sections[section]); if (section == "html" || section.endsWith("_html")) { addHtmlToSection(section, output[section].toString()); } else { addTextToSection(section, output[section].toString()); } if (!cursor.block().text().isEmpty()) { cursor.insertBlock(); } } if (time != -1) { QTextBlockFormat f; f.setProperty(Property::MessageType, "Timing"); cursor.setBlockFormat(f); addMessageType("Timing"); if (!isMessageTypeVisible("Timing")) { cursor.block().setVisible(false); } cursor.insertText(QString("% time elapsed: "), _noticeCharFormat); cursor.insertText(IDEUtils::formatTime(time), _defaultCharFormat); cursor.insertText("\n", _defaultCharFormat); } QTextBlockFormat format; cursor.setBlockFormat(format); cursor.insertText("----------\n", _defaultCharFormat); _solutionCount++; _lastSolutions[0] = _lastSolutions[1]; _lastSolutions[1] = cursor; _lastSolutions[1].setKeepPositionOnInsert(true); if (_solutionLimit > 0 && _solutionCount >= _solutionLimit) { if (_solutionBuffer == nullptr) { _solutionBuffer = new QTextDocument(this); _firstCompressedSolution = _solutionCount; _effectiveSolutionLimit *= 2; } else if (_solutionCount >= _effectiveSolutionLimit - 1) { auto* doc = _solutionBuffer; _solutionBuffer = nullptr; if (!doc->isEmpty()) { QTextDocumentFragment contents(doc); // Create table containing heading auto* t = nextInsertAt().insertTable(1, 2, _headerTableFormat); t->cellAt(0, 0).firstCursorPosition().insertText(QString(QChar::ObjectReplacementCharacter), _arrowFormat); auto label = QString("[ %1 more solutions ]").arg(_solutionCount - _firstCompressedSolution); t->cellAt(0, 1).firstCursorPosition().insertText(label, noticeCharFormat()); // Create frame which will contain children auto* frame = nextInsertAt().insertFrame(_frameFormat); frame->firstCursorPosition().insertFragment(contents); if (frame->lastCursorPosition().block().text().isEmpty()) { // Remove trailing newline frame->lastCursorPosition().deletePreviousChar(); } toggleFrameVisibility(frame); } delete doc; } } } void OutputWidget::addCheckerOutput(const QVariantMap& output, const QStringList& order) { for (auto& it : order) { _checkerOutput.append({it, output[it]}); } } void OutputWidget::addText(const QString& text, const QString& messageType) { addText(text, QTextCharFormat(), messageType); } void OutputWidget::addText(const QString& text, const QTextCharFormat& format, const QString& messageType) { TextLayoutLock lock(this); auto cursor = nextInsertAt(); if (messageType != "trace") { _lastTraceLoc = ""; } if (messageType.isEmpty()) { cursor.insertText(text, format); } else { addMessageType(messageType); auto start = cursor.position(); QTextBlockFormat f; f.setProperty(Property::MessageType, messageType); cursor.setBlockFormat(f); cursor.insertText(text, format); auto end = cursor.position(); if (!isMessageTypeVisible(messageType)) { cursor.setPosition(start); while (cursor.position() < end) { cursor.block().setVisible(false); cursor.movePosition(QTextCursor::NextBlock); } } } } void OutputWidget::addHtml(const QString& html, const QString& messageType) { TextLayoutLock lock(this); auto cursor = nextInsertAt(); if (messageType != "trace") { _lastTraceLoc = ""; } if (messageType.isEmpty()) { cursor.insertHtml(html); } else { addMessageType(messageType); auto start = cursor.position(); QTextBlockFormat f; f.setProperty(Property::MessageType, messageType); cursor.insertHtml(html); auto end = cursor.position(); cursor.setPosition(start); while (cursor.position() < end) { cursor.mergeBlockFormat(f); cursor.block().setVisible(isMessageTypeVisible(messageType)); cursor.movePosition(QTextCursor::NextBlock); } } } void OutputWidget::addTextToSection(const QString& section, const QString& text) { addTextToSection(section, text, QTextCharFormat()); } void OutputWidget::addTextToSection(const QString& section, const QString& text, const QTextCharFormat& format) { TextLayoutLock lock(this); auto cursor = nextInsertAt(); _lastTraceLoc = ""; auto start = cursor.position(); addSection(section); QTextBlockFormat f; f.setProperty(Property::Section, section); cursor.setBlockFormat(f); cursor.insertText(text, format); auto end = cursor.position(); if (!_sections[section]) { cursor.setPosition(start); while (cursor.position() < end) { cursor.block().setVisible(false); cursor.movePosition(QTextCursor::NextBlock); } } } void OutputWidget::addHtmlToSection(const QString& section, const QString& html) { TextLayoutLock lock(this); auto cursor = nextInsertAt(); _lastTraceLoc = ""; auto start = cursor.position(); addSection(section); QTextBlockFormat f; f.setProperty(Property::Section, section); cursor.insertHtml(html); auto end = cursor.position(); cursor.setPosition(start); while (cursor.position() < end) { cursor.mergeBlockFormat(f); cursor.block().setVisible(_sections[section]); cursor.movePosition(QTextCursor::NextBlock); } } void OutputWidget::addStatistics(const QVariantMap& statistics) { TextLayoutLock lock(this); auto cursor = nextInsertAt(); _lastTraceLoc = ""; QTextBlockFormat f; f.setProperty(Property::MessageType, "Statistics"); cursor.setBlockFormat(f); addMessageType("Statistics"); for (auto it = statistics.begin(); it != statistics.end(); it++) { if (!isMessageTypeVisible("Statistics")) { cursor.block().setVisible(false); } cursor.insertText("%%%mzn-stat: ", _noticeCharFormat); cursor.insertText(it.key(), _defaultCharFormat); cursor.insertText("=", _defaultCharFormat); cursor.insertText(it.value().toString(), _defaultCharFormat); cursor.insertText("\n", _noticeCharFormat); } if (!isMessageTypeVisible("Statistics")) { cursor.block().setVisible(false); } cursor.insertText("%%%mzn-stat-end\n", _noticeCharFormat); } void OutputWidget::addStatus(const QString& status, qint64 time) { TextLayoutLock lock(this); _lastTraceLoc = ""; QMap status_map = { {"ALL_SOLUTIONS", "=========="}, {"OPTIMAL_SOLUTION", "=========="}, {"UNSATISFIABLE", "=====UNSATISFIABLE====="}, {"UNSAT_OR_UNBOUNDED", "=====UNSATorUNBOUNDED====="}, {"UNBOUNDED", "=====UNBOUNDED====="}, {"UNKNOWN", "=====UNKNOWN====="}, {"ERROR", "=====ERROR====="} }; auto it = status_map.find(status); if (it != status_map.end()) { nextInsertAt().insertText(*it + "\n", _defaultCharFormat); } } void OutputWidget::endExecution(int exitCode, qint64 time) { TextLayoutLock lock(this); if (_solutionBuffer != nullptr) { auto* doc = _solutionBuffer; _solutionBuffer = nullptr; // Print compressed solutions QTextCursor cursor(doc); cursor.setPosition(_lastSolutions[0].position(), QTextCursor::KeepAnchor); if (cursor.hasSelection()) { auto* t = nextInsertAt().insertTable(1, 2, _headerTableFormat); t->cellAt(0, 0).firstCursorPosition().insertText(QString(QChar::ObjectReplacementCharacter), _arrowFormat); auto label = QString("[ %1 more solutions ]").arg(_solutionCount - _firstCompressedSolution - 1); t->cellAt(0, 1).firstCursorPosition().insertText(label, noticeCharFormat()); auto* f = nextInsertAt().insertFrame(_frameFormat); f->firstCursorPosition().insertFragment(QTextDocumentFragment(cursor)); if (f->lastCursorPosition().block().text().isEmpty()) { // Remove trailing newline f->lastCursorPosition().deletePreviousChar(); } toggleFrameVisibility(f); } // Print last solution and final status/stats cursor.setPosition(_lastSolutions[0].position()); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); nextInsertAt().insertFragment(QTextDocumentFragment(cursor)); delete doc; } _lastTraceLoc = ""; if (exitCode != 0) { QString msg = "Process finished with non-zero exit code %1.\n"; nextInsertAt().insertText(msg.arg(exitCode), errorCharFormat()); } auto t = IDEUtils::formatTime(time); nextInsertAt().insertText(QString("Finished in %1.").arg(t), noticeCharFormat()); _statusCursor.insertText(t, infoCharFormat()); _frame = ui->textBrowser->document()->rootFrame(); } void OutputWidget::addMessageType(const QString &messageType) { if (_messageTypeVisible.contains(messageType)) { return; } _messageTypeVisible[messageType] = true; #ifdef Q_OS_MAC auto* button = new QCheckBox(messageType); #else auto* button = new QPushButton(messageType); button->setCursor(Qt::PointingHandCursor); button->setFlat(true); button->setCheckable(true); #endif button->setChecked(true); auto* menuItem = ui->messageTypeMenu_pushButton->menu()->addAction(messageType, this, [=] () { toggleMessageTypeVisibility(messageType); }); menuItem->setCheckable(true); connect(button, &QAbstractButton::toggled, this, [=](bool checked) { setMessageTypeVisibility(messageType, checked); }); connect(this, &OutputWidget::messageTypeToggled, button, [=] (const QString& mt, bool visible) { if (messageType == mt) { button->setChecked(visible); menuItem->setChecked(visible); } }); button->setMinimumWidth(1); ui->messageTypeButtons_horizontalLayout->addWidget(button); layoutButtons(); } void OutputWidget::setMessageTypeVisibility(const QString& messageType, bool visible) { if (_messageTypeVisible[messageType] == visible) { return; } TextLayoutLock lock(this, false); auto cursor = ui->textBrowser->textCursor(); cursor.movePosition(QTextCursor::Start); auto* rootFrame = cursor.currentFrame(); auto rootFrameFormat = rootFrame->frameFormat(); while (!cursor.atEnd()) { auto frameVisible = isFrameVisible(cursor.currentFrame()); if (!frameVisible) { cursor = cursor.currentFrame()->lastCursorPosition(); cursor.movePosition(QTextCursor::NextBlock); continue; } auto block = cursor.block(); auto blockFormat = block.blockFormat(); if (blockFormat.hasProperty(Property::MessageType) && blockFormat.property(Property::MessageType).toString() == messageType) { block.setVisible(visible); } cursor.movePosition(QTextCursor::NextBlock); } rootFrame->setFrameFormat(rootFrameFormat); // Force relayout _messageTypeVisible[messageType] = visible; emit messageTypeToggled(messageType, visible); } void OutputWidget::clear() { if (_frame != ui->textBrowser->document()->rootFrame()) { // Can't clear in middle of run return; } _sections.clear(); _messageTypeVisible.clear(); ui->textBrowser->clear(); ui->toggleAll_pushButton->setEnabled(false); ui->sectionMenu_pushButton->hide(); ui->sectionMenu_pushButton->menu()->clear(); ui->messageTypeMenu_pushButton->hide(); ui->messageTypeMenu_pushButton->menu()->clear(); _frame = ui->textBrowser->document()->rootFrame(); qDeleteAll(ui->sectionButtons_widget->findChildren("", Qt::FindDirectChildrenOnly)); qDeleteAll(ui->messageTypeButtons_widget->findChildren("", Qt::FindDirectChildrenOnly)); ui->sectionButtons_widget->show(); ui->messageTypeButtons_widget->show(); } void OutputWidget::setBrowserFont(const QFont& font) { ui->textBrowser->setFont(font); QTextCharFormat format; format.setFont(font); auto cursor = ui->textBrowser->textCursor(); cursor.movePosition(QTextCursor::Start); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); cursor.mergeCharFormat(format); QFontMetrics metrics(font); auto spaceNeeded = metrics.height() + 4; auto cwc = { QTextLength(QTextLength::FixedLength, spaceNeeded) }; _headerTableFormat.setColumnWidthConstraints(cwc); _frameFormat.setLeftMargin(spaceNeeded); } void OutputWidget::onAnchorClicked(const QUrl& link) { emit anchorClicked(link); } void OutputWidget::onClickTable(QTextTable* table) { TextLayoutLock lock(this, false); auto tableFormat = table->format(); if (tableFormat.hasProperty(Property::ToggleFrame)) { auto tableCursor = table->lastCursorPosition(); tableCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, 2); auto* frame = tableCursor.currentFrame(); toggleFrameVisibility(frame); } } void OutputWidget::toggleFrameVisibility(QTextFrame* frame) { TextLayoutLock lock(this, false); auto frameFormat = frame->frameFormat(); auto visible = !frameFormat.property(Property::Expanded).toBool(); frameFormat.setProperty(Property::Expanded, visible); frame->setFrameFormat(frameFormat); auto parentVisible = isFrameVisible(frame->parentFrame()); if (!parentVisible) { return; } // Set visibility of TextBlocks auto cursor = frame->firstCursorPosition(); auto end = frame->lastCursorPosition(); while (cursor <= end) { auto* childFrame = cursor.currentFrame(); if (childFrame != nullptr && childFrame != frame) { auto childFormat = frame->frameFormat(); auto childVisible = !childFormat.property(Property::Expanded).toBool(); if (childVisible == visible) { cursor = childFrame->lastCursorPosition(); cursor.movePosition(QTextCursor::NextBlock); continue; } } auto block = cursor.block(); auto blockFormat = block.blockFormat(); auto blockVisible = true; if (blockFormat.hasProperty(Property::Section)) { blockVisible = _sections[blockFormat.property(Property::Section).toString()]; } else if (blockFormat.hasProperty(Property::MessageType)) { blockVisible = _messageTypeVisible[blockFormat.property(Property::MessageType).toString()]; } block.setVisible(isFrameVisible(cursor.currentFrame()) && blockVisible); cursor.movePosition(QTextCursor::NextBlock); } } void OutputWidget::addSection(const QString& section) { if (_sections.contains(section)) { return; } _sections[section] = true; #ifdef Q_OS_MAC auto* button = new QCheckBox(section); #else auto* button = new QPushButton(section); button->setCursor(Qt::PointingHandCursor); button->setFlat(true); button->setCheckable(true); #endif button->setChecked(true); connect(button, &QAbstractButton::toggled, this, [=](bool checked) { setSectionVisibility(section, checked); }); auto* menu = new QMenu(button); auto handler = [=] () { toggleSectionVisibility(section); }; auto* toggleSection = menu->addAction("Show section", this, handler); menu->addSeparator(); menu->addAction("Show only this section", this, [=] () { setAllSectionsVisibility(false); setSectionVisibility(section, true); }); menu->addAction("Show all but this section", this, [=] () { setAllSectionsVisibility(true); setSectionVisibility(section, false); }); button->setContextMenuPolicy(Qt::CustomContextMenu); connect(button, &QWidget::customContextMenuRequested, this, [=] (const QPoint &p) { toggleSection->setText(_sections[section] ? "Hide section" : "Show section"); menu->exec(button->mapToGlobal(p)); }); auto* menuItem = ui->sectionMenu_pushButton->menu()->addAction(section, this, handler); menuItem->setCheckable(true); connect(this, &OutputWidget::sectionToggled, button, [=] (const QString& s, bool visible) { if (section == s) { button->setChecked(visible); menuItem->setChecked(visible); } }); ui->toggleAll_pushButton->setEnabled(true); button->setMinimumWidth(1); ui->sectionButtons_horizontalLayout->addWidget(button); layoutButtons(); } void OutputWidget::setSectionVisibility(const QString& section, bool visible) { if (_sections[section] == visible) { return; } TextLayoutLock lock(this, false); auto cursor = ui->textBrowser->textCursor(); cursor.movePosition(QTextCursor::Start); auto* rootFrame = cursor.currentFrame(); auto rootFrameFormat = rootFrame->frameFormat(); while (!cursor.atEnd()) { auto frameVisible = isFrameVisible(cursor.currentFrame()); if (!frameVisible) { cursor = cursor.currentFrame()->lastCursorPosition(); cursor.movePosition(QTextCursor::NextBlock); continue; } auto block = cursor.block(); auto blockFormat = block.blockFormat(); if (blockFormat.hasProperty(Property::Section) && blockFormat.property(Property::Section).toString() == section) { block.setVisible(visible); } cursor.movePosition(QTextCursor::NextBlock); } _sections[section] = visible; rootFrame->setFrameFormat(rootFrameFormat); // Force relayout emit sectionToggled(section, visible); for (auto v : _sections) { if (!v) { ui->toggleAll_pushButton->setText("Show all"); return; } } ui->toggleAll_pushButton->setText("Hide all"); } void OutputWidget::setAllSectionsVisibility(bool visible) { TextLayoutLock lock(this, false); auto cursor = ui->textBrowser->textCursor(); cursor.movePosition(QTextCursor::Start); auto* rootFrame = cursor.currentFrame(); auto rootFrameFormat = rootFrame->frameFormat(); while (!cursor.atEnd()) { auto frameVisible = isFrameVisible(cursor.currentFrame()); if (!frameVisible) { cursor = cursor.currentFrame()->lastCursorPosition(); cursor.movePosition(QTextCursor::NextBlock); continue; } auto block = cursor.block(); auto blockFormat = block.blockFormat(); if (blockFormat.hasProperty(Property::Section)) { block.setVisible(visible); } cursor.movePosition(QTextCursor::NextBlock); } for (auto it = _sections.begin(); it != _sections.end(); it++) { bool toggled = it.value() != visible; _sections[it.key()] = visible; if (toggled) { emit sectionToggled(it.key(), visible); } } rootFrame->setFrameFormat(rootFrameFormat); // Force relayout ui->toggleAll_pushButton->setText(visible ? "Hide all" : "Show all"); } int OutputWidget::mouseToPosition(const QPoint& mousePos, Qt::HitTestAccuracy accuracy) { auto* hbar = ui->textBrowser->horizontalScrollBar(); auto hOffset = isRightToLeft() ? (hbar->maximum() - hbar->value()) : hbar->value(); auto vOffset = ui->textBrowser->verticalScrollBar()->value(); auto* layout = ui->textBrowser->document()->documentLayout(); return layout->hitTest(QPointF(mousePos.x() + hOffset, mousePos.y() + vOffset), accuracy); } QTextCursor OutputWidget::fragmentCursor(int position) { if (position != -1) { auto cursor = ui->textBrowser->textCursor(); cursor.setPosition(position); auto block = cursor.block(); for (auto it = block.begin(); it != block.end(); it++) { auto fragment = it.fragment(); if (fragment.contains(position)) { cursor.setPosition(fragment.position()); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, fragment.length()); return cursor; } } } return QTextCursor(); } bool OutputWidget::eventFilter(QObject* object, QEvent* event) { if (object == ui->textBrowser->viewport() && event->type() == QEvent::MouseButtonPress) { auto* e = static_cast(event); if (e->button() == Qt::LeftButton) { _pressed = true; } } else if (object == ui->textBrowser->viewport() && event->type() == QEvent::MouseMove) { auto* e = static_cast(event); if (_pressed) { _dragging = true; } } else if (object == ui->textBrowser->viewport() && event->type() == QEvent::MouseButtonRelease) { auto* e = static_cast(event); if (e->button() == Qt::LeftButton) { _pressed = false; if (_dragging) { _dragging = false; } else { auto position = mouseToPosition(e->pos()); auto cursor = fragmentCursor(position); if (cursor.isNull()) { // See if we're clicking an entire table position = mouseToPosition(e->pos(), Qt::FuzzyHit); if (position != -1) { cursor = ui->textBrowser->textCursor(); cursor.setPosition(position); auto* table = cursor.currentTable(); if (table != nullptr) { onClickTable(table); } } } else if (!cursor.charFormat().isAnchor()) { auto* table = cursor.currentTable(); if (table != nullptr) { onClickTable(table); } } } } } else if (object == ui->textBrowser && event->type() == QEvent::KeyPress) { auto* e = static_cast(event); if (e == QKeySequence::Copy || e == QKeySequence::Cut) { copySelectionToClipboard(false); return true; } } return false; } void OutputWidget::copySelectionToClipboard(bool includeHidden) { auto cursor = ui->textBrowser->textCursor(); if (!cursor.hasSelection()) { return; } // Copy text to new document and remove hidden/object replacement characters auto* doc = new QTextDocument(this); QTextCursor c(doc); c.insertFragment(ui->textBrowser->textCursor().selection()); c.setPosition(0); while (!c.atEnd()) { auto* table = c.currentTable(); if (table != nullptr) { auto f = table->format(); if (f.hasProperty(Property::Expanded)) { auto cell = table->cellAt(0, 0); auto cursor = cell.firstCursorPosition(); cursor.setPosition(cell.lastPosition(), QTextCursor::KeepAnchor); cursor.removeSelectedText(); if (!f.property(Property::Expanded).toBool()) { cursor.setPosition(table->lastPosition() + 2); cursor.setPosition(cursor.currentFrame()->lastPosition(), QTextCursor::KeepAnchor); cursor.removeSelectedText(); } } } if (!includeHidden) { auto format = c.block().blockFormat(); bool remove = format.hasProperty(Property::Section) && !isSectionVisible(format.property(Property::Section).toString()); remove |= format.hasProperty(Property::MessageType) && !isMessageTypeVisible(format.property(Property::MessageType).toString()); if (remove) { c.setPosition(c.block().position()); if (!c.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor)) { c.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); } c.removeSelectedText(); continue; } } if (!c.movePosition(QTextCursor::NextBlock)) { break; } } IDEUtils::MimeDataExporter te; te.setDocument(doc); te.selectAll(); QMimeData* mimeData = te.md(); QApplication::clipboard()->setMimeData(mimeData); delete doc; } bool OutputWidget::isFrameVisible(QTextFrame* frame) { while (frame != nullptr) { auto format = frame->frameFormat(); if (format.hasProperty(Property::Expanded) && !format.property(Property::Expanded).toBool()) { return false; } frame = frame->parentFrame(); } return true; } TextLayoutLock::TextLayoutLock(OutputWidget* o, bool scroll) : _o(o), _scroll(scroll) { if (_o->_frame != _o->ui->textBrowser->document()->rootFrame()) { auto frameVisible = _o->isFrameVisible(_o->_frame); if (!frameVisible) { _o->toggleFrameVisibility(_o->_frame); } } _o->ui->textBrowser->textCursor().beginEditBlock(); } TextLayoutLock::~TextLayoutLock() { _o->ui->textBrowser->textCursor().endEditBlock(); if (_scroll) { _o->scrollToBottom(); } } QSizeF OutputWidgetArrow::intrinsicSize(QTextDocument* doc, int posInDocument, const QTextFormat& format) { QFontMetrics metrics(format.toCharFormat().font()); return QSizeF(metrics.height(), metrics.height()); } void OutputWidgetArrow::drawObject(QPainter* painter, const QRectF& rect, QTextDocument* doc, int posInDocument, const QTextFormat& format) { QFontMetrics metrics(format.toCharFormat().font()); QTextCursor cursor(doc); cursor.setPosition(posInDocument); auto t = cursor.currentTable(); assert(t != nullptr); cursor.setPosition(t->lastPosition() + 2); auto* f = cursor.currentFrame(); assert(f != nullptr); auto ff = f->frameFormat(); bool expanded = ff.property(OutputWidget::Property::Expanded).toBool(); auto tf = t->format(); auto requiredFormat = static_cast(parent())->headerTableFormat(); if (tf.columnWidthConstraints()[0].rawValue() != requiredFormat.columnWidthConstraints()[0].rawValue()) { // Re-align t->setFormat(requiredFormat); f->setFrameFormat(static_cast(parent())->frameFormat()); } auto h2 = metrics.ascent() / 2; auto w2 = h2 / 2; QPainterPath p; p.moveTo(-w2, -h2); p.lineTo(w2, 0); p.lineTo(-w2, h2); p.closeSubpath(); painter->translate(rect.center()); if (expanded) { painter->rotate(90); } auto infoFormat = static_cast(parent())->infoCharFormat(); painter->fillPath(p, infoFormat.foreground()); } void OutputWidget::on_toggleAll_pushButton_clicked() { for (auto it = _sections.begin(); it != _sections.end(); it++) { if (!it.value()) { // Some invisible, so show all setAllSectionsVisibility(true); return; } } // Hide all sections since they were all previously visible setAllSectionsVisibility(false); } void OutputWidget::resizeEvent(QResizeEvent *e) { layoutButtons(); } void OutputWidget::layoutButtons() { auto neededWidth = 0; auto spacing = ui->sectionButtons_horizontalLayout->spacing(); for (auto* b : ui->sectionButtons_widget->findChildren()) { neededWidth += b->sizeHint().width() + spacing; } for (auto* b : ui->messageTypeButtons_widget->findChildren()) { neededWidth += b->sizeHint().width() + spacing; } auto availableWidth = ui->buttons_widget->width(); if (availableWidth > neededWidth) { // Normal layout if (ui->sectionMenu_pushButton->isVisible()) { ui->sectionMenu_pushButton->hide(); ui->messageTypeMenu_pushButton->hide(); ui->sectionButtons_widget->show(); ui->messageTypeButtons_widget->show(); } } else { // Compact layout ui->sectionMenu_pushButton->setVisible(!_sections.empty()); ui->messageTypeMenu_pushButton->setVisible(!_messageTypeVisible.empty()); ui->sectionButtons_widget->hide(); ui->messageTypeButtons_widget->hide(); } } QTextCursor OutputWidget::nextInsertAt() const { if (_solutionBuffer != nullptr) { QTextCursor cursor(_solutionBuffer); cursor.movePosition(QTextCursor::End); return cursor; } return _frame->lastCursorPosition(); } void OutputWidget::onBrowserContextMenu(const QPoint& pos) { _contextMenu->popup(ui->textBrowser->viewport()->mapToGlobal(pos)); } ================================================ FILE: MiniZincIDE/outputwidget.h ================================================ #ifndef OUTPUTWIDGET_H #define OUTPUTWIDGET_H #include #include #include #include #include #include #include "theme.h" namespace Ui { class OutputWidget; } class OutputWidget : public QWidget { Q_OBJECT friend class TestIDE; public: explicit OutputWidget(QWidget *parent = nullptr); ~OutputWidget(); enum TextObject { Arrow = QTextFormat::UserObject + 1 }; enum Property { Expanded = QTextFormat::UserProperty + 1, ToggleFrame, Section, MessageType }; bool isSectionVisible(const QString& section) { return _sections.value(section, false); } bool isMessageTypeVisible(const QString& messageType) { return _messageTypeVisible.value(messageType, false); } const QTextCharFormat& defaultCharFormat() const { return _defaultCharFormat; } const QTextCharFormat& noticeCharFormat() const { return _noticeCharFormat; } const QTextCharFormat& errorCharFormat() const { return _errorCharFormat; } const QTextCharFormat& infoCharFormat() const { return _infoCharFormat; } const QTextCharFormat& commentCharFormat() const { return _commentCharFormat; } const QTextTableFormat headerTableFormat() const { return _headerTableFormat; } const QTextFrameFormat frameFormat() const { return _frameFormat; } int solutionLimit() { return _solutionLimit; } QString lastTraceLoc(const QString& newTraceLoc); public slots: void setTheme(const Theme& theme, bool darkMode); void copySelectionToClipboard(bool includeHidden = false); void startExecution(const QString& label); void associateProfilerExecution(int executionId); void associateServerUrl(const QString& url); void addSolution(const QVariantMap& output, const QStringList& order, qint64 time = -1); void addCheckerOutput(const QVariantMap& output, const QStringList& order); void addText(const QString& text, const QString& messageType = QString()); void addText(const QString& text, const QTextCharFormat& format, const QString& messageType = QString()); void addHtml(const QString& html, const QString& messageType = QString()); void addTextToSection(const QString& section, const QString& text); void addTextToSection(const QString& section, const QString& text, const QTextCharFormat& format); void addHtmlToSection(const QString& section, const QString& html); void addStatistics(const QVariantMap& statistics); void addStatus(const QString& status, qint64 time = -1); void endExecution(int exitCode, qint64 time = -1); void clear(); void setSolutionLimit(int solutionLimit) { _solutionLimit = solutionLimit; } void setBrowserFont(const QFont& font); void setSectionVisibility(const QString& section, bool visible); void toggleSectionVisibility(const QString& section) { setSectionVisibility(section, !isSectionVisible(section)); } void setAllSectionsVisibility(bool visible); void setMessageTypeVisibility(const QString& messageType, bool visible); void toggleMessageTypeVisibility(const QString& messageType) { setMessageTypeVisibility(messageType, !isMessageTypeVisible(messageType)); } void scrollToBottom(); signals: void sectionToggled(const QString& section, bool visible); void messageTypeToggled(const QString& messageType, bool visible); void anchorClicked(const QUrl& url); private: Ui::OutputWidget *ui; friend class TextLayoutLock; QMenu* _contextMenu = nullptr; bool _dragging = false; bool _pressed = false; int _solutionLimit = 100; int _firstCompressedSolution = 0; QTextCursor _statusCursor; QTextFrame* _frame = nullptr; QTextDocument* _solutionBuffer = nullptr; QTextCursor _lastSolutions[2]; QMap _sections; QMap _messageTypeVisible; bool _hadServerUrl; QTextCharFormat _defaultCharFormat; QTextCharFormat _noticeCharFormat; QTextCharFormat _errorCharFormat; QTextCharFormat _infoCharFormat; QTextCharFormat _commentCharFormat; QTextCharFormat _arrowFormat; QTextBlockFormat _rightAlignBlockFormat; QTextTableFormat _headerTableFormat; QTextFrameFormat _frameFormat; QVector> _checkerOutput; int _solutionCount = 0; int _effectiveSolutionLimit = 0; QString _lastTraceLoc; void addSection(const QString& section); void addMessageType(const QString& messageType); void onClickTable(QTextTable* table); void toggleFrameVisibility(QTextFrame* frame); bool eventFilter(QObject* object, QEvent* event) override; void resizeEvent(QResizeEvent* e) override; bool isFrameVisible(QTextFrame* frame); void layoutButtons(); QTextCursor nextInsertAt() const; int mouseToPosition(const QPoint& mousePos, Qt::HitTestAccuracy accuracy = Qt::ExactHit); QTextCursor fragmentCursor(int position); private slots: void onAnchorClicked(const QUrl& link); void on_toggleAll_pushButton_clicked(); void onBrowserContextMenu(const QPoint& pos); }; class OutputWidgetArrow : public QObject, public QTextObjectInterface { Q_OBJECT Q_INTERFACES(QTextObjectInterface) public: QSizeF intrinsicSize(QTextDocument* doc, int posInDocument, const QTextFormat& format) override; void drawObject(QPainter* painter, const QRectF& rect, QTextDocument* doc, int posInDocument, const QTextFormat& format) override; }; class TextLayoutLock { public: explicit TextLayoutLock(OutputWidget* o, bool scroll = true); ~TextLayoutLock(); private: bool _scroll; OutputWidget* _o; }; #endif // OUTPUTWIDGET_H ================================================ FILE: MiniZincIDE/outputwidget.ui ================================================ OutputWidget 0 0 400 300 Form 0 0 0 0 false Hide all false 0 0 0 0 0 Toggle section 0 0 0 0 Qt::Horizontal 0 0 0 0 0 0 Toggle message ================================================ FILE: MiniZincIDE/paramdialog.cpp ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "paramdialog.h" #include "ui_paramdialog.h" #include #include #include #include #include #include ParamDialog::ParamDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ParamDialog) { ui->setupUi(this); } int ParamDialog::getModel(const QStringList& modelFiles) { setWindowTitle("Select model to run"); selectedFiles = new QListWidget(this); selectedFiles->setSelectionMode(QAbstractItemView::SingleSelection); for (int i=0; iaddItem(lwi); if (previousModelFile==modelFiles[i]) { selectedFiles->setCurrentItem(lwi); } } if (selectedFiles->selectedItems().size()==0) { selectedFiles->item(0)->setSelected(true); } QFormLayout* mainLayout = new QFormLayout; mainLayout->addRow(selectedFiles); ui->frame->setLayout(mainLayout); int selectedModel = -1; selectedFiles->setFocus(); if (QDialog::exec()==QDialog::Accepted) { selectedModel = selectedFiles->currentRow(); previousModelFile=modelFiles[selectedModel]; } delete ui->frame; ui->frame = new QFrame; ui->frame->setFrameShape(QFrame::StyledPanel); ui->frame->setFrameShadow(QFrame::Raised); ui->verticalLayout->insertWidget(0, ui->frame); return selectedModel; } void ParamDialog::getParams(QStringList params, const QStringList& dataFiles, QStringList &values, QStringList &additionalDataFiles) { setWindowTitle("Model parameters"); QVector le; QFormLayout* mainLayout = new QFormLayout; QTabWidget* tw = new QTabWidget(this); QWidget* manual = new QWidget; auto* formLayout = new QFormLayout; manual->setLayout(formLayout); bool fillPrevious = previousParams == params; for (int i=0; iaddRow(new QLabel(params[i]+" ="), le.back()); } QScrollArea* scrollArea = new QScrollArea; scrollArea->setWidget(manual); tw->addTab(scrollArea, "Enter parameters"); selectedFiles = nullptr; if (dataFiles.size() > 0) { selectedFiles = new QListWidget(this); selectedFiles->setSelectionMode(QAbstractItemView::ExtendedSelection); for (int i=0; iaddItem(lwi); if (previousDataFiles.contains(dataFiles[i]) || additionalDataFiles.contains(dataFiles[i])) { lwi->setSelected(true); selectedFiles->setCurrentItem(lwi); selectedFiles->scrollToItem(lwi); } } tw->addTab(selectedFiles, ""); if (!fillPrevious || !previousWasManual) { tw->setCurrentIndex(1); } // Set now to workaround Qt bug where first tab doesn't appear tw->setTabText(1, "Select data file"); } mainLayout->addRow(tw); ui->frame->setLayout(mainLayout); le[0]->setFocus(); if (selectedFiles && selectedFiles->selectedItems().size()==0) { selectedFiles->item(0)->setSelected(true); } if (QDialog::exec()==QDialog::Accepted) { additionalDataFiles.clear(); previousWasManual = (tw->currentIndex()==0); if (tw->currentIndex()==1) { previousDataFiles.clear(); for (int i=0; iitem(i); if (lwi && lwi->isSelected()) { additionalDataFiles.append(dataFiles[i]); previousDataFiles.append(dataFiles[i]); } } } else { for (int i=0; itext(); previousParams = params; previousValues = values; } } delete ui->frame; ui->frame = new QFrame; ui->frame->setFrameShape(QFrame::StyledPanel); ui->frame->setFrameShadow(QFrame::Raised); ui->verticalLayout->insertWidget(0, ui->frame); } ParamDialog::~ParamDialog() { delete ui; } ================================================ FILE: MiniZincIDE/paramdialog.h ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef PARAMDIALOG_H #define PARAMDIALOG_H #include #include #include #include namespace Ui { class ParamDialog; } class ParamDialog : public QDialog { Q_OBJECT public: explicit ParamDialog(QWidget *parent = 0); ~ParamDialog(); void getParams(QStringList params, const QStringList& dataFiles, QStringList& values, QStringList& additionalDataFiles); int getModel(const QStringList& modelFiles); private: Ui::ParamDialog *ui; QListWidget* selectedFiles; QStringList previousParams; QStringList previousValues; QStringList previousDataFiles; QString previousModelFile; bool previousWasManual; }; #endif // PARAMDIALOG_H ================================================ FILE: MiniZincIDE/paramdialog.ui ================================================ ParamDialog 0 0 400 84 Model Parameters true QFrame::NoFrame QFrame::Raised Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() ParamDialog accept() 248 254 157 274 buttonBox rejected() ParamDialog reject() 316 260 286 274 ================================================ FILE: MiniZincIDE/preferencesdialog.cpp ================================================ #include "preferencesdialog.h" #include "ui_preferencesdialog.h" #include "exception.h" #include "ideutils.h" #include "mainwindow.h" #include "ide.h" #include #include #include #include #include #include #include PreferencesDialog::PreferencesDialog(bool addNewSolver, QWidget *parent) : QDialog(parent), ui(new Ui::PreferencesDialog) { ui->setupUi(this); QSettings settings; // Get font so we can populate preview settings.beginGroup("MainWindow"); QFont editorFont = IDEUtils::fontFromString(settings.value("editorFont").toString()); auto zoom = settings.value("zoom", 100).toInt(); _origDarkMode = settings.value("darkMode", false).toBool(); settings.endGroup(); if (_ce == nullptr) { // Populate preview QString fileContents; QFile file(":/cheat_sheet.mzn"); if (file.open(QFile::ReadOnly)) { fileContents = file.readAll(); } else { qDebug() << "internal error: cannot open cheat sheet."; } auto& origTheme = IDE::instance()->themeManager->get(_origThemeIndex); _ce = new CodeEditor(nullptr, ":/cheat_sheet.mzn", false, false, editorFont, 2, false, origTheme, _origDarkMode, nullptr, this); _ce->document()->setPlainText(fileContents); _ce->setReadOnly(true); ui->preview_verticalLayout->addWidget(_ce); } // Now everything else can get populated // Separate font family from size so that combo box stays the same size auto font = qApp->font(); font.setFamily(editorFont.family()); ui->fontComboBox->setCurrentFont(font); ui->fontSize_spinBox->setValue(editorFont.pointSize()); ui->zoom_spinBox->setValue(zoom); auto& driver = MznDriver::get(); _origMznDistribPath = driver.mznDistribPath(); ui->mznDistribPath->setText(_origMznDistribPath); ui->mzn2fzn_version->setText(driver.minizincVersionString()); settings.beginGroup("ide"); ui->check_updates->setChecked(settings.value("checkforupdates21",false).toBool()); ui->checkSolutions_checkBox->setChecked(settings.value("checkSolutions", true).toBool()); ui->clearOutput_checkBox->setChecked(settings.value("clearOutput", false).toBool()); int compressSolutions = settings.value("compressSolutions", 100).toInt(); if (compressSolutions > 0) { ui->compressSolutions_spinBox->setValue(compressSolutions); ui->compressSolutions_checkBox->setChecked(true); } else { ui->compressSolutions_checkBox->setChecked(false); } ui->reuseVis_checkBox->setChecked(settings.value("reuseVis", false).toBool()); ui->visPort_spinBox->setValue(settings.value("visPort", 3000).toInt()); ui->visWsPort_spinBox->setValue(settings.value("visWsPort", 3100).toInt()); ui->visUrl_checkBox->setChecked(settings.value("printVisUrl", false).toBool()); ui->printCommand_checkBox->setChecked(settings.value("printCommand", false).toBool()); ui->indentSize_spinBox->setValue(settings.value("indentSize", 2).toInt()); bool indentTabs = settings.value("indentTabs", false).toBool(); ui->indentSpaces_radioButton->setChecked(!indentTabs); ui->indentTabs_radioButton->setChecked(indentTabs); ui->lineWrapping_checkBox->setChecked(settings.value("wordWrap", true).toBool()); _origThemeIndex = settings.value("theme", 0).toInt(); ui->theme_comboBox->setCurrentIndex(_origThemeIndex); auto* d = IDE::instance()->darkModeNotifier; if (d->hasSystemSetting()) { _origDarkMode = d->darkMode(); ui->darkMode_checkBox->hide(); } auto* t = IDE::instance()->themeManager; _ce->setTheme(t->get(_origThemeIndex), _origDarkMode); ui->darkMode_checkBox->setChecked(_origDarkMode); settings.endGroup(); // Load user solver search paths ui->configuration_groupBox->setEnabled(driver.isValid()); auto& userConfigFile = driver.userConfigFile(); QFile uc(userConfigFile); if (uc.exists() && uc.open(QFile::ReadOnly)) { QJsonDocument doc = QJsonDocument::fromJson(uc.readAll()); auto obj = doc.object(); if (obj.contains("mzn_solver_path")) { for (auto it : obj["mzn_solver_path"].toArray()) { auto path = it.toString(); if (!path.isEmpty()) { ui->extraSearchPath_listWidget->addItem(path); } } } if (obj.contains("solverDefaults")) { for (auto it : obj["solverDefaults"].toArray()) { auto arr = it.toArray(); auto solverId = arr[0].toString(); auto flag = arr[1].toString(); _userDefaultFlags.insert(solverId, flag); } } } IDEUtils::watchChildChanges(ui->solverFrame, this, [=] () { auto& driver = MznDriver::get(); auto& solvers = driver.solvers(); if (!solvers.isEmpty()) { _editingSolverIndex = ui->solvers_combo->currentIndex(); } }); connect(ui->name, &QLineEdit::textChanged, this, &PreferencesDialog::updateSolverLabel); connect(ui->version, &QLineEdit::textChanged, this, &PreferencesDialog::updateSolverLabel); if (addNewSolver) { ui->tabWidget->setCurrentIndex(1); ui->solvers_combo->setCurrentIndex(ui->solvers_combo->count()-1); } } PreferencesDialog::~PreferencesDialog() { delete ui; delete _ce; } void PreferencesDialog::on_fontComboBox_currentFontChanged(const QFont &f) { updateCodeEditorFont(); } void PreferencesDialog::on_fontSize_spinBox_valueChanged(int size) { updateCodeEditorFont(); } void PreferencesDialog::on_lineWrapping_checkBox_stateChanged(int checkstate) { bool checked = checkstate == Qt::Checked; if (checked) { _ce->setLineWrapMode(QPlainTextEdit::WidgetWidth); _ce->setWordWrapMode(QTextOption::WrapAnywhere); } else { _ce->setLineWrapMode(QPlainTextEdit::NoWrap); _ce->setWordWrapMode(QTextOption::NoWrap); } } void PreferencesDialog::updateCodeEditorFont() { auto editorFont = ui->fontComboBox->currentFont(); editorFont.setPointSize(ui->fontSize_spinBox->value() * ui->zoom_spinBox->value() / 100); _ce->setEditorFont(editorFont); } void PreferencesDialog::on_theme_comboBox_currentIndexChanged(int index) { auto* t = IDE::instance()->themeManager; t->current(index); IDE::instance()->refreshTheme(); _ce->setTheme(t->current(), ui->darkMode_checkBox->isChecked()); } QByteArray PreferencesDialog::allowFileRestore(const QString& path) { QFileInfo fi(path); auto filePath = fi.canonicalFilePath(); if (_restore.contains(filePath)) { // Already processed return _restore[filePath]; } if (_remove.contains(filePath)) { // Already processed return QByteArray(); } QFile f(path); if (f.exists()) { // Restore to old contents on cancel if (f.open(QIODevice::ReadOnly)) { auto contents = f.readAll(); _restore[filePath] = contents; f.close(); return contents; } } else { // Remove on cancel _remove << filePath; } return QByteArray(); } void PreferencesDialog::loadDriver(bool showError) { auto& driver = MznDriver::get(); auto& solvers = driver.solvers(); _solversPopulated = false; auto dest = ui->solvers_combo->currentIndex(); bool destExists = dest < solvers.size(); bool addNew = dest == ui->solvers_combo->count() - 1; QString matchId; QString matchVersion; if (dest == _editingSolverIndex) { // Was editing, so use widget values matchId = ui->solverId->text(); matchVersion = ui->version->text(); destExists = true; } else if (destExists) { // Switching to different solver matchId = solvers[dest]->id; matchVersion = solvers[dest]->version; } try { driver.setLocation(ui->mznDistribPath->text()); ui->mzn2fzn_version->setText(driver.minizincVersionString()); } catch (Exception& e) { if (showError) { showMessageBox(e.message()); } ui->mzn2fzn_version->setText(e.message()); } // Load user solver search paths ui->extraSearchPath_listWidget->clear(); ui->configuration_groupBox->setEnabled(driver.isValid()); _userDefaultFlags.clear(); auto& userConfigFile = driver.userConfigFile(); QFile uc(userConfigFile); if (uc.exists() && uc.open(QFile::ReadOnly)) { QJsonDocument doc = QJsonDocument::fromJson(uc.readAll()); auto obj = doc.object(); if (obj.contains("mzn_solver_path")) { for (auto it : obj["mzn_solver_path"].toArray()) { auto path = it.toString(); if (!path.isEmpty()) { ui->extraSearchPath_listWidget->addItem(path); } } } if (obj.contains("solverDefaults")) { for (auto it : obj["solverDefaults"].toArray()) { auto arr = it.toArray(); auto solverId = arr[0].toString(); auto flag = arr[1].toString(); _userDefaultFlags.insert(solverId, flag); } } } populateSolvers(); int index = addNew ? ui->solvers_combo->count() - 1 : 0; if (destExists) { int i = 0; for (auto* solver : solvers) { if (solver->id == matchId && solver->version == matchVersion) { index = i; break; } i++; } } if (ui->solvers_combo->currentIndex() == index) { on_solvers_combo_currentIndexChanged(index); } else { ui->solvers_combo->setCurrentIndex(index); } } void PreferencesDialog::populateSolvers() { if (_solversPopulated) { return; } _editingSolverIndex = -1; auto& driver = MznDriver::get(); auto& solvers = driver.solvers(); ui->solvers_combo->clear(); for (auto& solver : solvers) { ui->solvers_combo->addItem(solver->name + " " + solver->version); } if (solvers.empty()) { ui->solvers_combo->addItem("(No solvers found)"); } ui->solvers_combo->addItem("Add new..."); ui->solvers_tab->setEnabled(driver.isValid()); _solversPopulated = true; } bool PreferencesDialog::updateSolver() { if (_editingSolverIndex == -1) { return true; } auto& driver = MznDriver::get(); auto& solvers = driver.solvers(); auto& userConfigFile = driver.userConfigFile(); auto& userSolverConfigDir = driver.userSolverConfigDir(); if (!ui->requiredFlags->isHidden()) { // Update the required flags/user set flags QGridLayout* rfLayout = static_cast(ui->requiredFlags->layout()); QVector defaultFlags; for (int row=0; rowrowCount(); row++) { if (QLayoutItem* li = rfLayout->itemAtPosition(row, 0)) { QString key = static_cast(li->widget())->text(); QLayoutItem* li2 = rfLayout->itemAtPosition(row, 1); QString val = static_cast(li2->widget())->text(); if (!val.isEmpty()) { QStringList sl({ui->solverId->text(), key, val}); defaultFlags.push_back(sl); } } } auto doc = QJsonDocument::fromJson(allowFileRestore(userConfigFile)); auto jo = doc.object(); QJsonArray a0; if (!jo.contains("solverDefaults")) { // Fresh object, initialise for (auto& df : defaultFlags) { QJsonArray a1; for (auto& f : df) { a1.append(f); } a0.append(a1); } } else { QJsonArray previous = jo["solverDefaults"].toArray(); // First remove all entries for this solver for (auto triple : previous) { if (triple.isArray()) { QJsonArray previousA1 = triple.toArray(); if (previousA1.size()==3) { if (previousA1[0]!=ui->solverId->text()) { a0.append(triple); } } } } // Now add the new values for (auto& df : defaultFlags) { QJsonArray a1; for (auto& f : df) { a1.append(f); } a0.append(a1); } } jo["solverDefaults"] = a0; doc.setObject(jo); QFileInfo uc_info(userConfigFile); if (!QDir().mkpath(uc_info.absoluteDir().absolutePath())) { showMessageBox("Cannot create user configuration directory "+uc_info.absoluteDir().absolutePath()); return false; } QFile uc(userConfigFile); if (uc.open(QFile::ReadWrite | QIODevice::Truncate)) { uc.write(doc.toJson()); uc.close(); } else { showMessageBox("Cannot write user configuration file "+userConfigFile); return false; } } if (ui->solverFrame->isEnabled()) { if (ui->name->text().trimmed().isEmpty()) { showMessageBox("You need to specify a name for the solver."); return false; } if (ui->solverId->text().trimmed().isEmpty()) { showMessageBox("You need to specify a solver ID for the solver."); return false; } if (ui->executable->text().trimmed().isEmpty()) { showMessageBox("You need to specify an executable for the solver."); return false; } if (ui->version->text().trimmed().isEmpty()) { showMessageBox("You need to specify a version for the solver."); return false; } int index = _editingSolverIndex; for (int i=0; isolverId->text().trimmed()==solvers[i]->id) { showMessageBox("A solver with that solver ID already exists."); return false; } } if (index==solvers.size()) { auto* s = new Solver; s->configFile = userSolverConfigDir+"/"+ui->solverId->text().trimmed()+".msc"; if (!QDir().mkpath(userSolverConfigDir)) { showMessageBox("Cannot create user configuration directory "+userSolverConfigDir); return false; } solvers.append(s); allowFileRestore(s->configFile); } solvers[index]->executable = ui->executable->text().trimmed(); solvers[index]->mznlib = ui->mznpath->text(); solvers[index]->name = ui->name->text().trimmed(); solvers[index]->id = ui->solverId->text().trimmed(); solvers[index]->version = ui->version->text().trimmed(); solvers[index]->isGUIApplication= ui->detach->isChecked(); Solver::SolverInputType inputTypeMap[4] = { Solver::I_FZN, Solver::I_JSON, Solver::I_MZN, Solver::I_NL }; solvers[index]->inputType = inputTypeMap[ui->inputType_comboBox->currentIndex()]; solvers[index]->needsSolns2Out = ui->needs_solns2out->isChecked(); solvers[index]->stdFlags.removeAll("-a"); if (ui->has_stdflag_a->isChecked()) { solvers[index]->stdFlags.push_back("-a"); } solvers[index]->stdFlags.removeAll("-n"); if (ui->has_stdflag_n->isChecked()) { solvers[index]->stdFlags.push_back("-n"); } solvers[index]->stdFlags.removeAll("-p"); if (ui->has_stdflag_p->isChecked()) { solvers[index]->stdFlags.push_back("-p"); } solvers[index]->stdFlags.removeAll("-s"); if (ui->has_stdflag_s->isChecked()) { solvers[index]->stdFlags.push_back("-s"); } solvers[index]->stdFlags.removeAll("-v"); if (ui->has_stdflag_v->isChecked()) { solvers[index]->stdFlags.push_back("-v"); } solvers[index]->stdFlags.removeAll("-r"); if (ui->has_stdflag_r->isChecked()) { solvers[index]->stdFlags.push_back("-r"); } solvers[index]->stdFlags.removeAll("-f"); if (ui->has_stdflag_f->isChecked()) { solvers[index]->stdFlags.push_back("-f"); } solvers[index]->stdFlags.removeAll("-t"); if (ui->has_stdflag_t->isChecked()) { solvers[index]->stdFlags.push_back("-t"); } QJsonObject json = solvers[index]->json; json.remove("extraInfo"); json["executable"] = ui->executable->text().trimmed(); json["mznlib"] = ui->mznpath->text(); json["name"] = ui->name->text().trimmed(); json["id"] = ui->solverId->text().trimmed(); json["version"] = ui->version->text().trimmed(); json["isGUIApplication"] = ui->detach->isChecked(); QStringList inputTypeStringMap = { "FZN", "JSON", "MZN", "NL" }; json["inputType"] = inputTypeStringMap[ui->inputType_comboBox->currentIndex()]; json.remove("supportsFzn"); json.remove("supportsMzn"); json.remove("supportsNL"); json["needsSolns2Out"] = ui->needs_solns2out->isChecked(); json["stdFlags"] = QJsonArray::fromStringList(solvers[index]->stdFlags); QJsonDocument jdoc(json); QFile jdocFile(solvers[index]->configFile); allowFileRestore(solvers[index]->configFile); if (!jdocFile.open(QIODevice::ReadWrite | QIODevice::Truncate)) { showMessageBox("Cannot save configuration file "+solvers[index]->configFile); return false; } if (jdocFile.write(jdoc.toJson())==-1) { showMessageBox("Cannot save configuration file "+solvers[index]->configFile); return false; } } _editingSolverIndex = -1; loadDriver(false); return true; } namespace { void clearRequiredFlagsLayout(QGridLayout* l) { QVector items; for (int i=0; irowCount(); i++) { for (int j=0; jcolumnCount(); j++) { if (QLayoutItem* li = l->itemAtPosition(i,j)) { if (QWidget* w = li->widget()) { w->hide(); w->deleteLater(); } items.push_back(li); } } } for (auto i : items) { l->removeItem(i); delete i; } } } void PreferencesDialog::on_deleteButton_clicked() { auto& driver = MznDriver::get(); auto& solvers = driver.solvers(); int index = ui->solvers_combo->currentIndex(); if (index >= solvers.size()) { _editingSolverIndex = -1; if (solvers.size() == 0) { // No solver to switch back to, so create dummy ui->solvers_combo->insertItem(0, "(No solvers found)"); index++; } ui->solvers_combo->setCurrentIndex(index > 0 ? index - 1 : 0); ui->solvers_combo->removeItem(index); return; } if (QMessageBox::warning(this, "MiniZinc IDE", "Delete solver " + solvers[index]->name + "?", QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { auto configFile = solvers[index]->configFile; allowFileRestore(configFile); QFile sf(configFile); if (!sf.remove()) { showMessageBox("Cannot remove configuration file "+solvers[index]->configFile); return; } solvers.removeAt(index); if (solvers.size() == 0) { // No solver to switch back to, so create dummy ui->solvers_combo->insertItem(0, "(No solvers found)"); index++; } ui->solvers_combo->setCurrentIndex(index > 0 ? index - 1 : 0); ui->solvers_combo->removeItem(index); } } void PreferencesDialog::on_mznpath_select_clicked() { QFileDialog fd(this,"Select MiniZinc distribution path (bin directory)"); QDir dir(ui->mznDistribPath->text()); fd.setDirectory(dir); fd.setFileMode(QFileDialog::Directory); fd.setOption(QFileDialog::ShowDirsOnly, true); if (fd.exec()) { ui->mznDistribPath->setText(fd.selectedFiles().first()); loadDriver(true); } } void PreferencesDialog::on_exec_select_clicked() { QFileDialog fd(this,"Select solver executable"); fd.selectFile(ui->executable->text()); fd.setFileMode(QFileDialog::ExistingFile); if (fd.exec()) { ui->executable->setText(fd.selectedFiles().first()); } QFileInfo fi(ui->executable->text()); ui->exeNotFoundLabel->setHidden(fi.exists()); } void PreferencesDialog::on_mznlib_select_clicked() { QFileDialog fd(this,"Select solver library path"); QDir dir(ui->mznpath->text()); fd.setDirectory(dir); fd.setFileMode(QFileDialog::Directory); fd.setOption(QFileDialog::ShowDirsOnly, true); if (fd.exec()) { ui->mznpath->setText(fd.selectedFiles().first()); } } void PreferencesDialog::on_mznDistribPath_returnPressed() { loadDriver(true); } void PreferencesDialog::on_check_solver_clicked() { loadDriver(true); } void PreferencesDialog::on_extraSearchPathAdd_pushButton_clicked() { QFileDialog fd(this ,"Select path containing solver configuration (.msc) files"); auto selected = ui->extraSearchPath_listWidget->selectedItems(); if (!selected.isEmpty()) { QDir dir(selected.first()->text()); fd.setDirectory(dir); } fd.setFileMode(QFileDialog::Directory); fd.setOption(QFileDialog::ShowDirsOnly, true); if (fd.exec()) { for (auto& dir : fd.selectedFiles()) { ui->extraSearchPath_listWidget->addItem(dir); } _extraSearchPathsChanged = true; } } void PreferencesDialog::on_extraSearchPathEdit_pushButton_clicked() { QFileDialog fd(this, "Select path containing solver configuration (.msc) files"); auto selected = ui->extraSearchPath_listWidget->selectedItems(); if (selected.isEmpty()) { return; } QDir dir(selected.first()->text()); fd.setDirectory(dir); fd.setFileMode(QFileDialog::Directory); fd.setOption(QFileDialog::ShowDirsOnly, true); if (fd.exec()) { selected.first()->setText(fd.selectedFiles().first()); _extraSearchPathsChanged = true; } } void PreferencesDialog::on_extraSearchPathDelete_pushButton_clicked() { auto items = ui->extraSearchPath_listWidget->selectedItems(); if (!items.isEmpty()) { delete items.first(); _extraSearchPathsChanged = true; } } void PreferencesDialog::on_extraSearchPath_listWidget_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) { bool hasSelection = current != nullptr; ui->extraSearchPathEdit_pushButton->setEnabled(hasSelection); ui->extraSearchPathDelete_pushButton->setEnabled(hasSelection); } void PreferencesDialog::updateSearchPaths() { if (!_extraSearchPathsChanged) { return; } auto& driver = MznDriver::get(); auto& userConfigFile = driver.userConfigFile(); QFile uc(userConfigFile); auto doc = QJsonDocument::fromJson(allowFileRestore(userConfigFile)); auto obj = doc.object(); auto* list = ui->extraSearchPath_listWidget; if (list->count() > 0) { QJsonArray arr; for (auto i = 0; i < list->count(); i++) { auto path = list->item(i)->text(); if (!path.isEmpty()) { arr.append(path); } } obj["mzn_solver_path"] = arr; } else if (obj.contains("mzn_solver_path")) { obj.remove("mzn_solver_path"); } QFileInfo uc_info(userConfigFile); if (!QDir().mkpath(uc_info.absoluteDir().absolutePath())) { showMessageBox("Cannot create user configuration directory "+uc_info.absoluteDir().absolutePath()); } if (uc.open(QFile::ReadWrite | QIODevice::Truncate)) { uc.write(QJsonDocument(obj).toJson()); uc.close(); _extraSearchPathsChanged = false; loadDriver(true); } else { showMessageBox("Cannot write user configuration file " + userConfigFile); } } void PreferencesDialog::on_solvers_combo_currentIndexChanged(int index) { if (!_solversPopulated || index == _editingSolverIndex) { return; } if (_editingSolverIndex != -1) { if (!updateSolver()) { // Cannot edit another solver unless this one is valid ui->solvers_combo->setCurrentIndex(_editingSolverIndex); } return; } auto& driver = MznDriver::get(); auto& solvers = driver.solvers(); QGridLayout* rfLayout = static_cast(ui->requiredFlags->layout()); clearRequiredFlagsLayout(rfLayout); QString userSolverConfigCanonical = QFileInfo(driver.userSolverConfigDir()).canonicalPath(); if (indexname->setText(solvers[index]->name); ui->solverId->setText(solvers[index]->id); ui->version->setText(solvers[index]->version); ui->executable->setText(solvers[index]->executable); ui->exeNotFoundLabel->setVisible(!solvers[index]->executable.isEmpty() && solvers[index]->executable_resolved.isEmpty()); ui->detach->setChecked(solvers[index]->isGUIApplication); switch (solvers[index]->inputType) { case Solver::I_FZN: ui->inputType_comboBox->setCurrentIndex(0); break; case Solver::I_JSON: ui->inputType_comboBox->setCurrentIndex(1); break; case Solver::I_MZN: ui->inputType_comboBox->setCurrentIndex(2); break; case Solver::I_NL: ui->inputType_comboBox->setCurrentIndex(3); break; default: ui->inputType_comboBox->setCurrentIndex(0); break; } ui->needs_solns2out->setChecked(solvers[index]->needsSolns2Out); ui->mznpath->setText(solvers[index]->mznlib); bool solverConfigIsUserEditable = false; if (!solvers[index]->configFile.isEmpty()) { QFileInfo configFileInfo(solvers[index]->configFile); if (configFileInfo.canonicalPath().startsWith(userSolverConfigCanonical)) { solverConfigIsUserEditable = true; } } ui->deleteButton->setEnabled(solverConfigIsUserEditable); ui->solverFrame->setEnabled(solverConfigIsUserEditable); ui->has_stdflag_a->setChecked(solvers[index]->stdFlags.contains("-a")); ui->has_stdflag_p->setChecked(solvers[index]->stdFlags.contains("-p")); ui->has_stdflag_r->setChecked(solvers[index]->stdFlags.contains("-r")); ui->has_stdflag_n->setChecked(solvers[index]->stdFlags.contains("-n")); ui->has_stdflag_s->setChecked(solvers[index]->stdFlags.contains("-s")); ui->has_stdflag_f->setChecked(solvers[index]->stdFlags.contains("-f")); ui->has_stdflag_v->setChecked(solvers[index]->stdFlags.contains("-v")); ui->has_stdflag_t->setChecked(solvers[index]->stdFlags.contains("-t")); auto flags = _userDefaultFlags.values(solvers[index]->id); for (auto& rf : solvers[index]->requiredFlags) { if (!flags.contains(rf)) { flags << rf; } } if (flags.empty()) { ui->requiredFlags->hide(); } else { ui->requiredFlags->show(); int row = 0; for (auto& rf : flags) { QString val; int foundFlag = solvers[index]->defaultFlags.indexOf(rf); if (foundFlag != -1 && foundFlag < solvers[index]->defaultFlags.size()-1) { val = solvers[index]->defaultFlags[foundFlag+1]; } rfLayout->addWidget(new QLabel(rf), row, 0); rfLayout->addWidget(new QLineEdit(val), row, 1); row++; } IDEUtils::watchChildChanges(ui->requiredFlags, this, [=] () { auto& driver = MznDriver::get(); auto& solvers = driver.solvers(); if (!solvers.isEmpty()) { _editingSolverIndex = ui->solvers_combo->currentIndex(); } }); } _editingSolverIndex = -1; } else { ui->name->setText(""); ui->solverId->setText(""); ui->version->setText(""); ui->executable->setText(""); ui->detach->setChecked(false); ui->inputType_comboBox->setCurrentIndex(0); ui->needs_solns2out->setChecked(true); ui->mznpath->setText(""); ui->solverFrame->setEnabled(true); ui->deleteButton->setEnabled(true); ui->has_stdflag_a->setChecked(false); ui->has_stdflag_p->setChecked(false); ui->has_stdflag_r->setChecked(false); ui->has_stdflag_n->setChecked(false); ui->has_stdflag_s->setChecked(false); ui->has_stdflag_v->setChecked(false); ui->has_stdflag_f->setChecked(false); ui->has_stdflag_t->setChecked(false); ui->requiredFlags->hide(); bool addNew = index == ui->solvers_combo->count() - 1; if (addNew) { if (solvers.isEmpty()) { // Remove dummy solver ui->solvers_combo->removeItem(0); return; } ui->solvers_combo->setItemText(index, "New solver"); ui->solvers_combo->addItem("Add new..."); _editingSolverIndex = index; } else { _editingSolverIndex = -1; } ui->deleteButton->setEnabled(addNew); ui->solverFrame->setEnabled(addNew); } } void PreferencesDialog::showMessageBox(const QString& message) { // Defer message box so that we can return immediately QTimer::singleShot(0, this, [=] () { QMessageBox::warning(this, "MiniZinc IDE", message, QMessageBox::Ok); }); } void PreferencesDialog::on_tabWidget_currentChanged(int index) { updateSearchPaths(); if (index == 1 && !_solversPopulated) { populateSolvers(); on_solvers_combo_currentIndexChanged(0); } // Cannot change out of solver tab if changes invalid if (index != 1 && !updateSolver()) { ui->tabWidget->setCurrentIndex(1); } } void PreferencesDialog::accept() { if (ui->tabWidget->currentIndex() == 1 && !updateSolver()) { // Can't accept until updateSolver() succeeds return; } QDialog::accept(); } void PreferencesDialog::on_PreferencesDialog_rejected() { // Undo theme changes ui->theme_comboBox->setCurrentIndex(_origThemeIndex); ui->darkMode_checkBox->setChecked(_origDarkMode); if (_restore.empty() && _remove.empty()) { // Nothing to do return; } // Undo any file changes on cancellation for (auto it = _restore.begin(); it != _restore.end(); it++) { QFile f(it.key()); if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { f.write(it.value()); f.close(); } } for (auto& it : _remove) { QFile::remove(it); } _restore.clear(); _remove.clear(); // Reload old driver (may have updated things that have been reverted) try { MznDriver::get().setLocation(_origMznDistribPath); } catch (Exception& e) {} } void PreferencesDialog::on_PreferencesDialog_accepted() { updateSearchPaths(); QSettings settings; settings.beginGroup("ide"); settings.setValue("checkforupdates21", ui->check_updates->isChecked()); settings.setValue("checkSolutions", ui->checkSolutions_checkBox->isChecked()); settings.setValue("clearOutput", ui->clearOutput_checkBox->isChecked()); settings.setValue("compressSolutions", ui->compressSolutions_checkBox->isChecked() ? ui->compressSolutions_spinBox->value() : 0); settings.setValue("printCommand", ui->printCommand_checkBox->isChecked()); settings.setValue("reuseVis", ui->reuseVis_checkBox->isChecked()); settings.setValue("visPort", ui->visPort_spinBox->value()); settings.setValue("visWsPort", ui->visWsPort_spinBox->value()); settings.setValue("printVisUrl", ui->visUrl_checkBox->isChecked()); settings.setValue("theme", ui->theme_comboBox->currentIndex()); settings.setValue("indentTabs", ui->indentTabs_radioButton->isChecked()); settings.setValue("indentSize", ui->indentSize_spinBox->value()); settings.setValue("wordWrap", ui->lineWrapping_checkBox->isChecked()); settings.endGroup(); settings.beginGroup("MainWindow"); settings.setValue("darkMode", ui->darkMode_checkBox->isChecked()); auto editorFont = ui->fontComboBox->currentFont(); editorFont.setPointSize(ui->fontSize_spinBox->value()); settings.setValue("editorFont", editorFont.toString()); settings.setValue("zoom", ui->zoom_spinBox->value()); settings.endGroup(); settings.beginGroup("minizinc"); settings.setValue("mznpath", ui->mznDistribPath->text()); settings.endGroup(); } void PreferencesDialog::updateSolverLabel() { if (!_solversPopulated || _editingSolverIndex == -1) { return; } auto name = ui->name->text().isEmpty() ? "Untitled Solver" : ui->name->text(); ui->solvers_combo->setItemText(_editingSolverIndex, name + " " + ui->version->text()); } void PreferencesDialog::on_darkMode_checkBox_stateChanged(int checked) { auto* d = IDE::instance()->darkModeNotifier; auto* t = IDE::instance()->themeManager; bool dark = checked == Qt::Checked; d->requestChangeDarkMode(dark); _ce->setTheme(t->current(), d->darkMode()); } void PreferencesDialog::on_zoom_spinBox_valueChanged(int value) { updateCodeEditorFont(); } ================================================ FILE: MiniZincIDE/preferencesdialog.h ================================================ #ifndef PREFERENCESDIALOG_H #define PREFERENCESDIALOG_H #include #include #include #include "codeeditor.h" namespace Ui { class PreferencesDialog; } class PreferencesDialog : public QDialog { Q_OBJECT friend class TestIDE; public: explicit PreferencesDialog(bool addNewSolver, QWidget *parent = nullptr); ~PreferencesDialog(); void accept() Q_DECL_OVERRIDE; private slots: void on_fontComboBox_currentFontChanged(const QFont &f); void on_fontSize_spinBox_valueChanged(int arg1); void on_lineWrapping_checkBox_stateChanged(int arg1); void on_theme_comboBox_currentIndexChanged(int index); void on_solvers_combo_currentIndexChanged(int index); void on_tabWidget_currentChanged(int index); void on_PreferencesDialog_rejected(); void on_deleteButton_clicked(); void on_mznpath_select_clicked(); void on_exec_select_clicked(); void on_PreferencesDialog_accepted(); void on_mznDistribPath_returnPressed(); void on_check_solver_clicked(); void on_mznlib_select_clicked(); void on_extraSearchPathAdd_pushButton_clicked(); void on_extraSearchPathEdit_pushButton_clicked(); void on_extraSearchPathDelete_pushButton_clicked(); void on_extraSearchPath_listWidget_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); void updateSolverLabel(); void on_darkMode_checkBox_stateChanged(int arg1); void on_zoom_spinBox_valueChanged(int arg1); private: Ui::PreferencesDialog *ui; CodeEditor* _ce = nullptr; bool _solversPopulated = false; int _editingSolverIndex = -1; bool _extraSearchPathsChanged = false; QMap _restore; QSet _remove; QString _origMznDistribPath; bool _origDarkMode = false; int _origThemeIndex = 0; QMultiMap _userDefaultFlags; QByteArray allowFileRestore(const QString& path); void loadDriver(bool showError); void populateSolvers(); bool updateSolver(); void updateSearchPaths(); void showMessageBox(const QString& message); void updateCodeEditorFont(); }; #endif // PREFERENCESDIALOG_H ================================================ FILE: MiniZincIDE/preferencesdialog.ui ================================================ PreferencesDialog 0 0 690 652 Preferences true true 0 MiniZinc Compiler MiniZinc path Select false Check QFrame::StyledPanel QFrame::Raised Found MiniZinc installation: none Check for updates to MiniZinc on startup once a day Configuration Extra solver search paths: QAbstractItemView::NoDragDrop Add new false Edit false Delete Qt::Horizontal 40 20 Solvers Solver 1 0 Delete QFrame::StyledPanel QFrame::Raised Name Solver ID Version Executable 0 0 Select true false <html><head/><body><p><span style=" color:#fc0107;">Executable not found!</span></p></body></html> Qt::RichText Detach from IDE Run with solns2out Input type 0 0 FlatZinc (FZN) FlatZinc-JSON (JSON) MiniZinc (MZN) Non-linear (NL) Solver library path Select Supported standard command line flags: -a -n -s -v -p -r -f -t Required solver flags TextLabel Qt::Vertical 20 40 Editing Font Font family: 0 0 Font size: 0 0 5 1000 10 Zoom: % 10 10000 10 100 Editing Indent using: Spaces Tabs Indent size: 0 0 1 1000 2 Line wrapping Appearance 0 0 Theme: Default Blueberry Mango Dark Mode Preview Output Behaviour <html><head/><body><p>Run corresponding solution checker (.mzc or .mzc.mzn) if present</p></body></html> Check solutions (if solution checker model is present in project) true Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 999999 100 Compress solution output after this many solutions: <html><head/><body><p>Clear the output window each time Run is pressed</p></body></html> Clear output window before each run Visualisation Reuse existing visualisation window when starting a new run HTTP server port (0 for auto): 65535 WebSocket server port (0 for auto): 65535 Print the visualisation server URL in the output window Debugging Print the MiniZinc command used when starting each run Qt::Vertical 20 40 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() PreferencesDialog accept() 248 254 157 274 buttonBox rejected() PreferencesDialog reject() 316 260 286 274 ================================================ FILE: MiniZincIDE/process.cpp ================================================ /* * Main authors: * Jason Nguyen * Guido Tack */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include "process.h" #include "ide.h" #include "mainwindow.h" #include "exception.h" #ifndef Q_OS_WIN #include #include #include #include #else #include #endif #ifdef Q_OS_MAC #include #endif #ifdef Q_OS_WIN #define pathSep ";" #else #define pathSep ":" #endif void Process::start(const QString &program, const QStringList &arguments, const QString &path) { QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QString curPath = env.value("PATH"); QString addPath = IDE::instance()->appDir(); if (!path.isEmpty()) addPath = path + pathSep + addPath; env.insert("PATH", addPath + pathSep + curPath); setProcessEnvironment(env); #ifdef Q_OS_WIN _wputenv_s(L"PATH", (addPath + pathSep + curPath).toStdWString().c_str()); if (IsWindows8OrGreater()) { jobObject = CreateJobObject(nullptr, nullptr); connect(this, &QProcess::started, this, &Process::attachJob); } else { // Workaround PCA automatically adding to a job for Windows 7 setCreateProcessArgumentsModifier([] (QProcess::CreateProcessArguments *args) { args->flags |= CREATE_BREAKAWAY_FROM_JOB; }); } #else #if QT_VERSION >= 0x060000 setChildProcessModifier(Process::setpgid); #endif setenv("PATH", (addPath + pathSep + curPath).toStdString().c_str(), 1); #endif QProcess::start(program,arguments, QIODevice::Unbuffered | QIODevice::ReadWrite); #ifdef Q_OS_WIN _wputenv_s(L"PATH", curPath.toStdWString().c_str()); #else setenv("PATH", curPath.toStdString().c_str(), 1); #endif } void Process::terminate() { if (state() != QProcess::NotRunning) { #ifdef Q_OS_WIN if (IsWindows8OrGreater()) { TerminateJobObject(jobObject, EXIT_FAILURE); } else { // We can't use job objects in Windows 7, since MiniZinc already uses them QProcess::kill(); } #else ::killpg(processId(), SIGKILL); #endif if (!waitForFinished(500)) { kill(); waitForFinished(); } } } void Process::sendInterrupt() { if (state() == QProcess::NotRunning) { return; } #ifdef Q_OS_WIN QString pipe; QTextStream ts(&pipe); ts << "\\\\.\\pipe\\minizinc-" << processId(); auto pipeName = pipe.toStdString(); HANDLE hNamedPipe = CreateFileA(pipeName.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hNamedPipe != INVALID_HANDLE_VALUE) { DWORD bytesWritten; WriteFile(hNamedPipe, nullptr, 0, &bytesWritten, nullptr); CloseHandle(hNamedPipe); } #else ::killpg(processId(), SIGINT); #endif } #if QT_VERSION >= 0x060000 void Process::setpgid() #else void Process::setupChildProcess() #endif { #ifndef Q_OS_WIN if (::setpgid(0,0)) { std::cerr << "Error: Failed to create sub-process\n"; } #endif } #ifdef Q_OS_WIN void Process::attachJob() { HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, true, processId()); if (hProcess != nullptr) { AssignProcessToJobObject(jobObject, hProcess); } } #endif void MznDriver::setLocation(const QString &mznDistribPath) { clear(); _mznDistribPath = mznDistribPath; MznProcess p; QRegularExpression version_regexp("version (\\d+)\\.(\\d+)\\.(\\d+)"); _minizincExecutable = QStringList({"minizinc"}); #ifdef Q_OS_MAC int isTranslated = 0; { size_t size = sizeof(isTranslated); if (sysctlbyname("sysctl.proc_translated", &isTranslated, &size, NULL, 0) == -1) { isTranslated = 0; } } if (isTranslated) { _minizincExecutable = QStringList({"arch", "-arch", "arm64", "minizinc"}); } for (int i = 0; i <= isTranslated; i++) { if (i == 1) { _minizincExecutable = QStringList({"minizinc"}); } try { auto result = p.run({"--version"}); _versionString = result.stdOut + result.stdErr; QRegularExpressionMatch path_match = version_regexp.match(_versionString); if (path_match.hasMatch()) { break; } } catch (ProcessError& e) { if (i == isTranslated) { clear(); throw; } } } #else try { auto result = p.run({"--version"}); _versionString = result.stdOut + result.stdErr; } catch (ProcessError&) { clear(); throw; } #endif QRegularExpressionMatch path_match = version_regexp.match(_versionString); if (path_match.hasMatch()) { _version = QVersionNumber( path_match.captured(1).toInt(), path_match.captured(2).toInt(), path_match.captured(3).toInt() ); } else { QString message = _versionString; clear(); throw DriverError(message); } auto minVersion = QVersionNumber(2, 8, 0); if (_version < minVersion) { clear(); throw DriverError("Versions of MiniZinc before " + minVersion.toString() + " are not supported."); } QString allOutput = p.run({"--config-dirs"}).stdOut; QJsonDocument jd = QJsonDocument::fromJson(allOutput.toUtf8()); if (!jd.isNull()) { QJsonObject sj = jd.object(); _userSolverConfigDir = sj["userSolverConfigDir"].toString(); _userConfigFile = sj["userConfigFile"].toString(); _mznStdlibDir = sj["mznStdlibDir"].toString(); } allOutput = p.run({"--solvers-json"}).stdOut.toUtf8(); jd = QJsonDocument::fromJson(allOutput.toUtf8()); if (!jd.isNull()) { for (auto* s : _solvers) { delete s; } _solvers.clear(); QJsonArray allSolvers = jd.array(); for (auto item : allSolvers) { QJsonObject sj = item.toObject(); _solvers.append(new Solver(sj)); } } } Solver* MznDriver::defaultSolver(void) { for (auto* solver : solvers()) { if (solver->isDefaultSolver) { return solver; } } return nullptr; } void MznDriver::setDefaultSolver(const Solver& s) { for (auto* solver : solvers()) { solver->isDefaultSolver = *solver == s; } QFile uc(userConfigFile()); QJsonObject jo; if (uc.exists()) { if (uc.open(QFile::ReadOnly)) { QJsonDocument doc = QJsonDocument::fromJson(uc.readAll()); if (doc.isNull()) { throw DriverError("Cannot modify user configuration file " + userConfigFile()); } jo = doc.object(); uc.close(); } } QJsonArray tagdefs = jo.contains("tagDefaults") ? jo["tagDefaults"].toArray() : QJsonArray(); bool hadDefault = false; for (int i=0; i::of(&Process::finished), this, [=](int code, QProcess::ExitStatus e) { flushOutput(); timer.stop(); if (code == 0 || cancelled) { emit success(cancelled); } else { emit failure(code, FailureType::NonZeroExit); } p.disconnect(); emit(finished(elapsedTime())); }); connect(&p, &QProcess::errorOccurred, this, [=](QProcess::ProcessError e) { timer.stop(); if (!cancelled) { flushOutput(); switch (e) { case QProcess::FailedToStart: emit failure(0, FailureType::FailedToStart); break; case QProcess::Crashed: emit failure(p.exitCode(), FailureType::Crashed); break; default: emit failure(p.exitCode(), FailureType::UnknownError); break; } p.disconnect(); emit(finished(elapsedTime())); } }); connect(&p, &QProcess::readyReadStandardOutput, this, &MznProcess::readStdOut); connect(&p, &QProcess::readyReadStandardError, this, &MznProcess::readStdErr); Q_ASSERT(p.state() == QProcess::NotRunning); cancelled = false; const QStringList& exec = MznDriver::get().minizincExecutable(); QStringList execArgs = args; for (unsigned int i = exec.size() - 1; i > 0; i--) { execArgs.push_front(exec[i]); } p.start(exec[0], execArgs, MznDriver::get().mznDistribPath()); } void MznProcess::start(const SolverConfiguration& sc, const QStringList& args, const QString& cwd, bool jsonStream) { auto* temp = new QTemporaryFile(QDir::tempPath() + "/mzn_XXXXXX.mpc", this); if (!temp->open()) { emit failure(0, FailureType::FailedToStart); return; } QString paramFile = temp->fileName(); temp->write(sc.toJSON()); temp->close(); connect(this, &MznProcess::finished, temp, [=] () { delete temp; }); QStringList newArgs; parse = jsonStream; if (jsonStream) { newArgs << "--json-stream"; } if (!sc.paramFile.isEmpty()) { QFileInfo fi(sc.paramFile); newArgs << "--push-working-directory" << fi.canonicalPath(); } newArgs << "--param-file-no-push" << paramFile; if (!sc.paramFile.isEmpty()) { newArgs << "--pop-working-directory"; } newArgs << args; if (sc.timeLimit != 0) { auto* hardTimer = new QTimer(this); hardTimer->setSingleShot(true); connect(hardTimer, &QTimer::timeout, this, [=] () { stop(); }); connect(this, &MznProcess::started, hardTimer, [=] () { hardTimer->start(sc.timeLimit + 10000); }); connect(this, &MznProcess::finished, hardTimer, [=] () { delete hardTimer; }); } start(newArgs, cwd); } MznProcess::RunResult MznProcess::run(const QStringList& args, const QString& cwd) { Q_ASSERT(p.state() == QProcess::NotRunning); p.setWorkingDirectory(cwd); const QStringList& exec = MznDriver::get().minizincExecutable(); QStringList execArgs = args; for (unsigned int i = exec.size() - 1; i > 0; i--) { execArgs.push_front(exec[i]); } p.start(exec[0], execArgs, MznDriver::get().mznDistribPath()); if (!p.waitForStarted()) { throw ProcessError(QString("Failed to find or start %1 %2 in '%3'.") .arg(exec[0]) .arg(args.join(" ")) .arg(MznDriver::get().mznDistribPath()) ); } if (!p.waitForFinished()) { p.terminate(); } return { p.exitCode(), p.readAllStandardOutput(), p.readAllStandardError() }; } MznProcess::RunResult MznProcess::run(const SolverConfiguration& sc, const QStringList& args, const QString& cwd) { QTemporaryFile temp(QDir::tempPath() + "/mzn_XXXXXX.mpc"); if (!temp.open()) { throw ProcessError("Failed to create temporary file"); } QString paramFile = temp.fileName(); temp.write(sc.toJSON()); temp.close(); QStringList newArgs; newArgs << "--param-file" << paramFile << args; return run(newArgs, cwd); } void MznProcess::stop() { if (p.state() == QProcess::NotRunning) { return; } cancelled = true; p.sendInterrupt(); auto* killTimer = new QTimer(this); killTimer->setSingleShot(true); connect(killTimer, &QTimer::timeout, this, [=] () { if (p.state() == QProcess::Running) { terminate(); } }); connect(this, &MznProcess::finished, killTimer, [=] () { killTimer->stop(); delete killTimer; }); killTimer->start(1000); } void MznProcess::terminate() { if (p.state() == QProcess::NotRunning) { return; } p.disconnect(); p.terminate(); timer.stop(); emit success(cancelled); emit finished(timer.elapsed()); } qint64 MznProcess::elapsedTime() { return timer.elapsed(); } void MznProcess::writeStdIn(const QString &data) { p.write(data.toUtf8()); } void MznProcess::closeStdIn() { p.closeWriteChannel(); } QString MznProcess::command() const { return p.program() + " " + p.arguments().join(" "); } void MznProcess::processOutput() { p.setReadChannel(QProcess::ProcessChannel::StandardOutput); while (p.canReadLine()) { auto line = QString::fromUtf8(p.readLine()); onStdOutLine(line); } p.setReadChannel(QProcess::ProcessChannel::StandardError); while (p.canReadLine()) { auto line = QString::fromUtf8(p.readLine()); emit outputStdError(line); } } void MznProcess::readStdOut() { p.setReadChannel(QProcess::ProcessChannel::StandardOutput); if (p.canReadLine()) { auto fragment = QString::fromUtf8(p.readAllStandardError()); if (!fragment.isEmpty()) { emit outputStdError(fragment); } } while (p.canReadLine()) { auto line = QString::fromUtf8(p.readLine()); onStdOutLine(line); } } void MznProcess::readStdErr() { p.setReadChannel(QProcess::ProcessChannel::StandardError); if (p.canReadLine()) { auto fragment = QString::fromUtf8(p.readAllStandardOutput()); if (!fragment.isEmpty()) { onStdOutLine(fragment); } } while (p.canReadLine()) { auto line = QString::fromUtf8(p.readLine()); emit outputStdError(line); } } void MznProcess::flushOutput() { readStdOut(); readStdErr(); auto stdOut = QString::fromUtf8(p.readAllStandardOutput()); if (!stdOut.isEmpty()) { onStdOutLine(stdOut); } auto stdErr = QString::fromUtf8(p.readAllStandardError()); if (!stdErr.isEmpty()) { emit outputStdError(stdOut); } } void MznProcess::onStdOutLine(const QString& line) { emit outputStdOut(line); if (!parse) { // Not running with --json-stream, so cannot parse output emit unknownOutput(line); return; } QJsonParseError error; auto json = QJsonDocument::fromJson(line.toUtf8(), &error); if (!json.isNull()) { auto msg = json.object(); auto msg_type = msg["type"].toString(); if (msg_type == "solution") { auto sections = msg["output"].toObject().toVariantMap(); QStringList order; if (msg["sections"].isArray()) { for (auto it : msg["sections"].toArray()) { order << it.toString(); } } else { order = sections.keys(); } qint64 time = msg["time"].isDouble() ? static_cast(msg["time"].toDouble()) : -1; emit solutionOutput(sections, order, time); } else if (msg_type == "checker") { auto checkerSol = [&] (QJsonObject& msg) { auto sections = msg["output"].toObject().toVariantMap(); QStringList order; if (msg["sections"].isArray()) { for (auto it : msg["sections"].toArray()) { order << it.toString(); } } else { order = sections.keys(); } qint64 time = msg["time"].isDouble() ? static_cast(msg["time"].toDouble()) : -1; emit checkerOutput(sections, order, time); }; if (msg["messages"].isArray()) { for (auto it : msg["messages"].toArray()) { auto msg = it.toObject(); auto msg_type = msg["type"].toString(); if (msg_type == "solution") { checkerSol(msg); } else if (msg_type == "trace") { auto section = msg["section"].toString(); qint64 time = msg["time"].isDouble() ? static_cast(msg["time"].toDouble()) : -1; emit checkerOutput({{section, msg["message"].toString()}}, {section}, time); } else if (msg_type == "comment") { auto comment = msg["comment"].toString(); emit commentOutput(comment); } else if (msg_type == "warning") { emit warningOutput(msg, true); } else if (msg_type == "error") { emit errorOutput(msg); } } } else { checkerSol(msg); } } else if (msg_type == "status") { qint64 time = msg["time"].isDouble() ? static_cast(msg["time"].toDouble()) : -1; emit finalStatus(msg["status"].toString(), time); } else if (msg_type == "statistics") { emit statisticsOutput(msg["statistics"].toObject().toVariantMap()); } else if (msg_type == "comment") { auto comment = msg["comment"].toString(); emit commentOutput(comment); } else if (msg_type == "time") { qint64 time = msg["time"].isDouble() ? static_cast(msg["time"].toDouble()) : -1; emit timeOutput(time); } else if (msg_type == "error") { emit errorOutput(msg); } else if (msg_type == "warning") { emit warningOutput(msg); } else if (msg_type == "progress") { emit progressOutput(msg["progress"].toDouble()); } else if (msg_type == "paths") { QVector paths; for (auto it : msg["paths"].toArray()) { paths << it.toObject(); } emit pathsOutput(paths); } else if (msg_type == "profiling") { QVector t; for (auto it : msg["entries"].toArray()) { t << it.toObject(); } emit profilingOutput(t); } else if (msg_type == "trace") { emit traceOutput(msg["section"].toString(), msg["message"].toVariant()); } else { emit unknownOutput(line); } return; } // Fall back to just printing everything emit unknownOutput(line); } ================================================ FILE: MiniZincIDE/process.h ================================================ #ifndef PROCESS_H #define PROCESS_H #include #include #include #include #include #ifdef Q_OS_WIN #define NOMINMAX #include #endif #include "solver.h" #include "elapsedtimer.h" #include "profilecompilation.h" /// /// \brief The Process class /// Extends QProcess with the ability to handle child processes. /// class Process : public QProcess { #ifdef Q_OS_WIN Q_OBJECT #endif public: Process(QObject* parent=nullptr) : QProcess(parent) {} void start(const QString& program, const QStringList& arguments, const QString& path); void terminate(void); void sendInterrupt(); protected: #if QT_VERSION >= 0x060000 static void setpgid(); #else virtual void setupChildProcess(); #endif #ifdef Q_OS_WIN private: HANDLE jobObject; private slots: void attachJob(); #endif }; /// /// \brief The MznDriver class /// Central store of minizinc executable information. /// Singleton class instantiated with MznDriver::get(). /// class MznDriver { public: static MznDriver& get() { static MznDriver d; return d; } /// /// \brief Set the location of the MiniZinc executable to the given directory /// \param mznDistribPath The MiniZinc binary directory /// void setLocation(const QString& mznDistribPath); /// /// \brief Returns the validity of the current MiniZinc installation /// \return Whether or not a valid MiniZinc installation was found /// bool isValid(void) const { return !minizincExecutable().isEmpty(); } MznDriver(MznDriver const&) = delete; void operator=(MznDriver const&) = delete; /// /// \brief The name of the minizinc executable /// \return The executable name, or an empty string if not found /// const QStringList& minizincExecutable(void) const { return _minizincExecutable; } /// /// \brief The directory that contains the minizinc executable /// \return The directory as a string /// const QString& mznDistribPath(void) const { return _mznDistribPath; } /// /// \brief The output from running with --version /// \return The full output string including stderr /// const QString& minizincVersionString(void) const { return _versionString; } /// /// \brief The user solver config directory /// \return The directory as a string /// const QString& userSolverConfigDir(void) const { return _userSolverConfigDir; } /// /// \brief The location of the user's config file /// \return The file location as a string /// const QString& userConfigFile(void) const { return _userConfigFile; } /// /// \brief The directory which contains stdlib /// \return The directory as a string /// const QString& mznStdlibDir(void) const { return _mznStdlibDir; } /// /// \brief The built-in solvers as found by running with --solvers-json /// \return The built-in solvers /// QList& solvers(void) { return _solvers; } /// /// \brief Get a pointer to the default Solver /// \return The default solver or nullptr if there is none /// Solver* defaultSolver(void); /// /// \brief Sets the default solver /// \param s The new default solver /// void setDefaultSolver(const Solver& s); /// /// \brief Returns the version number of MiniZinc /// \return The version number /// const QVersionNumber& version(void) { return _version; } private: MznDriver() {} QStringList _minizincExecutable; QString _mznDistribPath; QString _versionString; QString _userSolverConfigDir; QString _userConfigFile; QString _mznStdlibDir; QList _solvers; QVersionNumber _version; void clear() { _minizincExecutable.clear(); _mznDistribPath.clear(); _versionString.clear(); _userSolverConfigDir.clear(); _userConfigFile.clear(); _mznStdlibDir.clear(); for (auto* s : _solvers) { delete s; } _solvers.clear(); _version = QVersionNumber(); } }; /// /// \brief The MznProcess class /// Runs MiniZinc using the current MznDriver /// class MznProcess : public QObject { Q_OBJECT public: struct RunResult { int exitCode; QString stdOut; QString stdErr; RunResult(int _exitCode, const QString& _stdOut, const QString& _stdErr) : exitCode(_exitCode), stdOut(_stdOut), stdErr(_stdErr) {} }; enum FailureType { NonZeroExit, FailedToStart, Crashed, UnknownError }; Q_ENUM(FailureType) MznProcess(QObject* parent = nullptr) : QObject(parent), cancelled(false), p(nullptr), timer(nullptr) {} /// /// \brief Start minizinc. Does not enable --json-stream. /// \param args Command line arguments /// \param cwd Working directory /// void start(const QStringList& args, const QString& cwd = QString()); /// /// \brief Start minizinc and use the given solver configuration. /// \param sc The solver configuration to use /// \param args Command line arguments /// \param cwd Working directory /// \param jsonStream Whether or not to enable --json-stream /// void start(const SolverConfiguration& sc, const QStringList& args, const QString& cwd = QString(), bool jsonStream = true); /// /// \brief Stop minizinc (does not block) /// void stop(); /// /// \brief Force stop minizinc immediately (blocks, does not finishe processing output) /// void terminate(); /// /// \brief Run minizinc in blocking mode (only for fast commands). /// \param args Command line arguments /// \param cwd Working directory /// \return The stdout and stderr output /// RunResult run(const QStringList& args, const QString& cwd = QString()); /// /// \brief Run minizinc in blocking mode (only for fast commands). /// \param sc The solver configuration to use /// \param args Command line arguments /// \param cwd Working directory /// \return The stdout and stderr output /// RunResult run(const SolverConfiguration& sc, const QStringList& args, const QString& cwd = QString()); /// /// \brief Get the time since the process started. /// \return The elapsed time in nanoseconds /// qint64 elapsedTime(); /// /// \brief Write string to process stdin /// \param data String data to write /// void writeStdIn(const QString& data); /// /// \brief Closes process stdin /// void closeStdIn(); QString command() const; signals: /// /// \brief Emitted when the process is started. /// void started(); /// /// \brief Emitted when a line is written to stdout. /// \param output The data in stdout. /// void outputStdOut(const QString& output); /// /// \brief Emitted when a line is written to stderr. /// \param error The data in stderr. /// void outputStdError(const QString& error); /// /// \brief Emitted regularly as time elapses /// \param time The time elapsed in nanoseconds /// void timeUpdated(qint64 time); /// /// \brief Emitted on successful exit (or after stopping). /// void success(bool cancelled); /// /// \brief Emitted when the process encounters an error /// \param exitCode The exit code /// \param e The error that occurred /// void failure(int exitCode, FailureType e); /// /// \brief Emitted when finished regardless of success/failure. /// \param time The runtime in nanoseconds. /// void finished(qint64 time); // Emitted if --json-stream is enabled /// /// \brief Emitted when a solution message is produced. /// void solutionOutput(const QVariantMap& sections, const QStringList& sectionOrder, qint64 time = -1); /// /// \brief Emitted when a checker message is produced. /// void checkerOutput(const QVariantMap& sections, const QStringList& sectionOrder, qint64 time = -1); /// /// \brief Emitted when en error message is produced. /// void errorOutput(const QJsonObject& error); /// /// \brief Emitted when a warning message is produced. /// void warningOutput(const QJsonObject& warning, bool fromChecker = false); /// /// \brief Emitted when a statistics message is produced. /// void statisticsOutput(const QVariantMap& statistics); /// /// \brief Emitted when a progress message is read. /// \param progress The progress value /// void progressOutput(double progress); /// /// \brief Emitted when a final status string is read. /// void finalStatus(const QString& status, qint64 time = -1); /// /// \brief Emitted when a comment is read /// \param data The data that was read /// void commentOutput(const QString& data); /// /// \brief Emitted if a time message is output /// \param time The output time /// void timeOutput(qint64 time = -1); /// /// \brief Emitted when paths are output /// \param paths The paths /// void pathsOutput(const QVector& paths); /// /// \brief Emitted when detailed timing info is produced /// \param timing The timing information /// void profilingOutput(const QVector& timing); /// /// \brief Emitted when a non-stderr trace is produced /// \param section The trace output section /// \param message The trace message (either a string or JSON) /// void traceOutput(const QString& section, const QVariant& message); /// /// \brief Emitted when an unknown fragment is output /// \param data The data that was read /// void unknownOutput(const QString& data); private: bool cancelled; bool parse; Process p; ElapsedTimer timer; void readStdOut(); void readStdErr(); void processOutput(); void onStdOutLine(const QString& line); void flushOutput(); }; #endif // PROCESS_H ================================================ FILE: MiniZincIDE/profilecompilation.cpp ================================================ #include "profilecompilation.h" #include Path::Path(const QString& path) { auto items = path.split(';'); for (auto& it : items) { auto parts = it.split('|'); if (parts.size() < 5) { continue; } Path::Segment segment; QFileInfo fi(parts[0]); segment.filename = fi.canonicalFilePath(); segment.firstLine = parts[1].toInt(); segment.firstColumn = parts[2].toInt(); segment.lastLine = parts[3].toInt(); segment.lastColumn = parts[4].toInt(); QStringList rest; for (auto i = 5; i < parts.size(); i++) { segment.parts << parts[i]; } _segments << segment; } } PathEntry::PathEntry(const QJsonObject& obj) : _path(obj["path"].toString()) { if (obj["constraintIndex"].isUndefined()) { _flatZincName = obj["flatZincName"].toString(); _niceName = obj["niceName"].toString(); } else { _constraintIndex = obj["constraintIndex"].toInt(); } } TimingEntry::TimingEntry(const QJsonObject& obj) { QFileInfo fi(obj["filename"].toString()); _filename = fi.canonicalFilePath(); _line = obj["line"].toInt(); _time = obj["time"].toInt(); } ================================================ FILE: MiniZincIDE/profilecompilation.h ================================================ #ifndef PROFILE_COMPILATION_H #define PROFILE_COMPILATION_H #include #include #include #include #include "highlighter.h" class Path { public: struct Segment { QString filename; int firstLine; int firstColumn; int lastLine; int lastColumn; QStringList parts; }; Path(const QString& path); Path() {} const QVector& segments() const { return _segments; } private: QVector _segments; }; class PathEntry { public: PathEntry(const QJsonObject& obj); PathEntry() {} const QString& flatZincName() const {return _flatZincName; } const QString& niceName() const { return _niceName; } int constraintIndex() const { return _constraintIndex; } const Path& path() const { return _path; } private: QString _flatZincName; QString _niceName; int _constraintIndex = -1; Path _path; }; class TimingEntry { public: TimingEntry(const QJsonObject& obj); TimingEntry() {} const QString& filename() const { return _filename; } int line() const { return _line; } int time() const { return _time; } private: QString _filename; int _line; int _time; }; #endif // PROFILE_COMPILATION_H ================================================ FILE: MiniZincIDE/project.cpp ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "project.h" #include "moocsubmission.h" #include "solver.h" #include "ide.h" #include #include #include #include #include #include #include Project::Project(const QList& configs, QObject* parent) : QObject(parent), solverConfigs(configs) { itemModel = new QStandardItemModel(this); itemModel->setColumnCount(1); connect(itemModel, &QStandardItemModel::itemChanged, this, &Project::on_itemChanged); rootItem = new QStandardItem(QIcon(":/images/mznicon.png"), "Untitled Project"); rootItem->setData(NodeType::ProjectFile, Role::Type); rootItem->setData("Untitled Project", Role::OriginalLabel); rootItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable); auto font = rootItem->font(); font.setBold(true); modelsItem = new QStandardItem("Models"); modelsItem->setData(NodeType::Group, Role::Type); modelsItem->setFlags(Qt::NoItemFlags); modelsItem->setFont(font); dataItem = new QStandardItem("Data (right click to run)"); dataItem->setData(NodeType::Group, Role::Type); dataItem->setFlags(Qt::NoItemFlags); dataItem->setFont(font); checkersItem = new QStandardItem("Checkers (right click to run)"); checkersItem->setData(NodeType::Group, Role::Type); checkersItem->setFlags(Qt::NoItemFlags); checkersItem->setFont(font); configsItem = new QStandardItem("Solver configurations"); configsItem->setData(NodeType::Group, Role::Type); configsItem->setFlags(Qt::NoItemFlags); configsItem->setFont(font); otherItem = new QStandardItem("Other files"); otherItem->setData(NodeType::Group, Role::Type); otherItem->setFlags(Qt::NoItemFlags); otherItem->setFont(font); rootItem->appendRow(modelsItem); rootItem->appendRow(dataItem); rootItem->appendRow(checkersItem); rootItem->appendRow(configsItem); rootItem->appendRow(otherItem); itemModel->appendRow(rootItem); } QStringList Project::loadProject(const QString& file, ConfigWindow* configWindow) { clear(); projectFile(file); QStringList warnings; QFile f(file); QFileInfo fi(f); if (!f.open(QFile::ReadOnly)) { throw FileError("Failed to open project file"); } auto doc = QJsonDocument::fromJson(f.readAll()); if (doc.isObject()) { loadJSON(doc.object(), fi, configWindow, warnings); } else { f.reset(); QDataStream in(&f); loadLegacy(in, fi, configWindow, warnings); } f.close(); // Save these because adding the configs can change them auto projectBuiltinConfigId = selectedBuiltinConfigId; auto projectBuiltinConfigVersion = selectedBuiltinConfigVersion; auto projectselectedSolverConfigFile = selectedSolverConfigFile; for (auto& sc : solverConfigurationFiles()) { configWindow->addConfig(sc); } if (!projectBuiltinConfigId.isEmpty()) { int index = configWindow->findBuiltinConfig(projectBuiltinConfigId, projectBuiltinConfigVersion); if (index == -1) { warnings << "Could not find solver " + projectBuiltinConfigId + "@" + projectBuiltinConfigVersion; } else { configWindow->setCurrentIndex(index); } } else if (!projectselectedSolverConfigFile.isEmpty()) { int index = configWindow->findConfigFile(rootDir().absolutePath() + "/" + projectselectedSolverConfigFile); configWindow->setCurrentIndex(index); } setModified(false); return warnings; } void Project::loadJSON(const QJsonObject& obj, const QFileInfo& fi, ConfigWindow* configWindow, QStringList& warnings) { int version = obj["version"].toInt(); QString basePath = fi.absolutePath() + "/"; auto of = obj["openFiles"].toArray(); for (auto file : of) { auto path = basePath + file.toString(); if (QFileInfo(path).exists()) { openTabs << path; } else { warnings << "The file " + file.toString() + " could not be found"; } } openTabIndex = obj["openTab"].toInt(); QList configs; if (obj["builtinSolverConfigs"].isArray()) { for (auto config : obj["builtinSolverConfigs"].toArray()) { if (!config.isObject()) { warnings << "Failed to read solver builtin solver config"; continue; } try { SolverConfiguration* loaded; if (version >= 106) { loaded = new (SolverConfiguration) (SolverConfiguration::loadJSON(QJsonDocument(config.toObject()), warnings)); } else { loaded = new (SolverConfiguration) (SolverConfiguration::loadLegacy(QJsonDocument(config.toObject()), warnings)); } loaded->isBuiltin = true; configs << loaded; } catch (Exception& e) { warnings << e.message(); } } } if (obj["projectSolverConfigs"].isArray()) { for (auto config : obj["projectSolverConfigs"].toArray()) { if (!config.isObject()) { warnings << "Failed to read solver project solver config"; continue; } try { auto loaded = new (SolverConfiguration) (SolverConfiguration::loadLegacy(QJsonDocument(config.toObject()), warnings)); loaded->modified = true; configs << loaded; } catch (Exception& e) { warnings << e.message(); } } } configWindow->mergeConfigs(configs); if (obj["selectedBuiltinConfigId"].isString()) { selectedBuiltinConfigId = obj["selectedBuiltinConfigId"].toString(); selectedBuiltinConfigVersion = obj["selectedBuiltinConfigVersion"].toString(); selectedSolverConfigFile = ""; } else if (obj["selectedSolverConfigFile"].isString()) { selectedSolverConfigFile = obj["selectedSolverConfigFile"].toString(); selectedBuiltinConfigId = ""; selectedBuiltinConfigVersion = ""; } /*else { warnings << "No selected solver config in project"; }*/ for (auto file : obj["projectFiles"].toArray()) { auto path = basePath + file.toString(); if (QFileInfo(path).exists()) { add(path); } else { warnings << "The file " + file.toString() + " could not be found"; } } } void Project::loadLegacy(QDataStream& in, const QFileInfo& fi, ConfigWindow* configWindow, QStringList& warnings) { // Load old binary format of project throw InternalError("This project format is no longer supported. Please use MiniZinc IDE version 2.4 to upgrade it."); } void Project::saveProject() { QJsonObject confObject; confObject["version"] = 106; // Save the currently open tabs QStringList of; for (auto& f: openTabs) { of << relativeToProject(f); } confObject["openFiles"] = QJsonArray::fromStringList(of); confObject["openTab"] = openTabIndex; // Save paths of all project files QStringList relativeFilePaths; for (auto& file : files()) { relativeFilePaths << relativeToProject(file); } confObject["projectFiles"] = QJsonArray::fromStringList(relativeFilePaths); // Save which config is currently selected if (!selectedBuiltinConfigId.isEmpty() && !selectedBuiltinConfigVersion.isEmpty()) { confObject["selectedBuiltinConfigId"] = selectedBuiltinConfigId; confObject["selectedBuiltinConfigVersion"] = selectedBuiltinConfigVersion; } else if (!selectedSolverConfigFile.isEmpty()){ confObject["selectedSolverConfigFile"] = selectedSolverConfigFile; } // Write project file QJsonDocument doc(confObject); QFile file(projectFile()); if (!file.open(QFile::WriteOnly)) { throw FileError("Failed to write file"); } file.write(doc.toJson()); file.close(); setModified(false); } void Project::add(const QString& fileName) { auto abs = QFileInfo(fileName).absoluteFilePath(); auto path = relativeToProject(fileName); if (contains(abs)) { return; } #if QT_VERSION >= 0x060000 auto parts = path.split("/", Qt::SkipEmptyParts); // Qt always uses / as the path separator #else auto parts = path.split("/", QString::SkipEmptyParts); // Qt always uses / as the path separator #endif auto file = parts.takeLast(); QStandardItem* node = otherItem; QString icon; NodeType type = NodeType::Other; if (file.endsWith(".mzc.mzn") || file.endsWith(".mzc")) { node = checkersItem; icon = ":/images/mznicon.png"; type = NodeType::Checker; } else if (file.endsWith(".mzn")) { node = modelsItem; icon = ":/images/mznicon.png"; type = NodeType::Model; } else if (file.endsWith(".dzn") || file.endsWith(".json")) { node = dataItem; icon = ":/images/mznicon.png"; type = NodeType::Data; } else if (file.endsWith(".mpc")) { node = configsItem; icon = ":/images/mznicon.png"; type = NodeType::SolverConfig; } else if (file == "_mooc" || file == "_coursera") { if (mooc) { delete mooc; } mooc = new MOOCAssignment(fileName); emit moocChanged(mooc); } node->setFlags(Qt::ItemIsEnabled); // Traverse existing path items int i = 0; while (!parts.empty() && i < node->rowCount()) { if (getType(node->child(i)->index()) == NodeType::Dir && node->child(i)->text() == parts.first()) { parts.pop_front(); node = node->child(i); i = 0; } else { i++; } } // Create new path items for (auto& part : parts) { auto dir = new QStandardItem(QIcon(":/icons/images/folder.png"), part); dir->setData(NodeType::Dir, Role::Type); dir->setFlags(Qt::ItemIsEnabled); node->appendRow(dir); node->sortChildren(0); node = dir; } // Add file item auto item = new QStandardItem(QIcon(icon), file); item->setData(type, Role::Type); item->setData(abs, Role::Path); item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable); node->appendRow(item); node->sortChildren(0); entries.insert(abs, item); if (!projectFile().isEmpty()) { setModified(true); } } void Project::add(const QStringList& fileNames) { for (auto& fileName : fileNames) { add(fileName); } } void Project::remove(const QString &fileName) { auto path = QFileInfo(fileName).absoluteFilePath(); if (!contains(path)) { return; } // Make sure we also remove any unused dirs auto node = entries[path]; if (node->text() == "_mooc" || node->text() == "_coursera") { delete mooc; mooc = nullptr; emit moocChanged(mooc); } while (getType(node->parent()->index()) == NodeType::Dir && node->parent()->rowCount() <= 1) { node = node->parent(); } auto parent = node->parent(); parent->removeRow(node->row()); if (parent->data(Role::Type) == "group" && !parent->hasChildren()) { parent->setFlags(Qt::NoItemFlags); } entries.remove(path); if (!projectFile().isEmpty()) { setModified(true); } } void Project::remove(const QStringList &fileNames) { for (auto& fileName : fileNames) { remove(fileName); } } void Project::remove(const QModelIndex& index) { remove(getFileName(index)); } void Project::remove(const QModelIndexList& indexes) { for (auto& index : indexes) { remove(index); } } void Project::clear() { modelsItem->removeRows(0, modelsItem->rowCount()); dataItem->removeRows(0, dataItem->rowCount()); checkersItem->removeRows(0, checkersItem->rowCount()); otherItem->removeRows(0, otherItem->rowCount()); entries.clear(); setModified(true); } QStringList Project::files(void) const { return entries.keys(); } QStringList Project::modelFiles(void) const { return getFiles(NodeType::Model); } QStringList Project::solverConfigurationFiles(void) const { return getFiles(NodeType::SolverConfig); } QStringList Project::dataFiles(void) const { return getFiles(NodeType::Data); } bool Project::contains(const QString &fileName) { QFileInfo fi(fileName); return entries.contains(fi.absoluteFilePath()); } void Project::setModified(bool m) { modified = m; auto label = rootItem->data(Role::OriginalLabel).toString(); if (modified) { rootItem->setText(label + " *"); } else { rootItem->setText(label); } } void Project::projectFile(const QString& fileName) { QStringList files = entries.keys(); clear(); if (fileName.isEmpty()) { rootItem->setText("Untitled Project"); rootItem->setData("Untitled Project", Role::OriginalLabel); projFile = ""; } else { QFileInfo fi(fileName); projFile = fi.absoluteFilePath(); rootItem->setText(fi.fileName()); rootItem->setData(fi.fileName(), Role::OriginalLabel); } add(files); } QDir Project::rootDir() { QFileInfo fi(projectFile()); return QDir(fi.absolutePath()); } bool Project::hasProjectFile() { return !projectFile().isEmpty(); } Project::NodeType Project::getType(const QModelIndex& index) { return static_cast(model()->data(index, Role::Type).toInt()); } QString Project::getFileName(const QModelIndex& index) { return model()->data(index, Role::Path).toString(); } QStringList Project::getFileNames(const QModelIndexList& indices) { QStringList result; for (auto& index : indices) { result << getFileName(index); } return result; } QStringList Project::getFiles(NodeType type) const { QStringList ret; for (auto it = entries.begin(); it != entries.end(); it++) { auto t = static_cast(it.value()->data(Role::Type).toInt()); if (t == type) { ret << it.key(); } } return ret; } QString Project::relativeToProject(const QString& fileName) { QFileInfo fi(fileName); auto abs = fi.absoluteFilePath(); return hasProjectFile() ? rootDir().relativeFilePath(abs) : abs; } void Project::openTabsChanged(const QStringList& files, int currentTab) { openTabs.clear(); for (auto& file : files) { auto abs = QFileInfo(file).absoluteFilePath(); if (contains(abs)) { openTabs << abs; } } if (currentTab >= 0) { openTabIndex = openTabs.indexOf(QFileInfo(files[currentTab]).absoluteFilePath()); } else { openTabIndex = -1; } // setModified(true); } void Project::activeSolverConfigChanged(const SolverConfiguration* sc) { if (!sc) { selectedSolverConfigFile = ""; selectedBuiltinConfigId = ""; selectedBuiltinConfigVersion = ""; } else if (sc->isBuiltin) { selectedSolverConfigFile = ""; selectedBuiltinConfigId = sc->solverDefinition.id; selectedBuiltinConfigVersion = sc->solverDefinition.version; } else { selectedBuiltinConfigId = ""; selectedBuiltinConfigVersion = ""; selectedSolverConfigFile = relativeToProject(sc->paramFile); } // setModified(true); } void Project::on_itemChanged(QStandardItem* item) { auto oldPath = item->data(Qt::UserRole + 1).toString(); auto newName = item->text(); if (oldPath.isEmpty() || oldPath.endsWith(newName)) { return; } QFileInfo fi(oldPath); auto target = fi.path() + "/" + item->text(); if (QFile::rename(oldPath, target)) { remove(oldPath); add(target); emit fileRenamed(oldPath, target); } else { item->setText(fi.fileName()); } } ================================================ FILE: MiniZincIDE/project.h ================================================ /* * Author: * Guido Tack * * Copyright: * NICTA 2013 */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef PROJECT_H #define PROJECT_H #include #include #include #include "solver.h" #include "moocsubmission.h" #include "exception.h" #include "configwindow.h" #include "history.h" class Project : public QObject { Q_OBJECT public: enum NodeType { Model = 1, Data = 2, Checker = 4, SolverConfig = 8, Other = 16, Dir = 32, Group = 64, ProjectFile = 128 }; Q_DECLARE_FLAGS(NodeTypes, NodeType) explicit Project(const QList& configs, QObject* parent = nullptr); ~Project() { delete mooc; emit moocChanged(nullptr); } /// /// \brief Loads a project from an mzp file. /// \param file The mzp file to be loaded /// \param configWindow The solver configuration window /// \return A list of warnings from loading the project /// QStringList loadProject(const QString& file, ConfigWindow* configWindow); /// /// \brief Save this project /// void saveProject(void); /// /// \brief Add a file to the project /// \param fileName The file to be added /// void add(const QString& fileName); /// /// \brief Adds multiple files to the project /// \param fileNames The files to be added /// void add(const QStringList& fileNames); /// /// \brief Removes a files from the project /// \param fileName The file to be removed /// void remove(const QString& fileName); /// /// \brief Removes multiple files from the project /// \param fileNames The files to be removed /// void remove(const QStringList& fileNames); /// /// \brief Removes a file from the project /// \param index The index of the file to be removed /// void remove(const QModelIndex& index); /// /// \brief Removes multiple files from the project /// \param indexes The indices of the files to be removed /// void remove(const QModelIndexList& indexes); /// /// \brief Removes all files from the project /// void clear(void); /// /// \brief Gets the files in this project /// \return The files in this project /// QStringList files(void) const; /// /// \brief Gets the model files in this project (excluding checkers) /// \return The file paths of the model files /// QStringList modelFiles(void) const; /// /// \brief Gets the solver configuration files in this project /// \return The file paths of the solver configurations /// QStringList solverConfigurationFiles(void) const; /// /// \brief Gets the data files in this project /// \return The file paths of the data files /// QStringList dataFiles(void) const; const QStringList& openFiles(void) { return openTabs; } int openTab(void) { return openTabIndex; } /// /// \brief Return whether this project contains the given file /// \param fileName The file to check /// \return True if the given file is in the project, and false otherwise /// bool contains(const QString& fileName); /// /// \brief Return whether this project is modified /// \return True if this project was modified /// bool isModified(void) { return modified; } /// /// \brief Marks this project as modified or unmodified /// \param m True for modified, false for unmodified /// void setModified(bool m); /// /// \brief The path to the project .mzp file /// \return The path to the project file, or empty if there is none /// const QString& projectFile(void) const { return projFile; } /// /// \brief Set the project .mzp file /// \param fileName The new project file path /// void projectFile(const QString& fileName); /// /// \brief The root directory off of which project file paths are based /// \return The root directory /// QDir rootDir(void); /// /// \brief Return whether this project has a .mzp file /// \return True if there is one, and false otherwise /// bool hasProjectFile(void); /// /// \brief Gets the mooc assignment associated with this project /// \return The mooc assignment (throws exception if there is none) /// MOOCAssignment& moocAssignment(void) { if (!mooc) { throw MoocError("No mooc assignment in project"); } return *mooc; } /// /// \brief Get the type of the node at the given index /// \param index The model index /// \return The type of the node /// NodeType getType(const QModelIndex& index); /// /// \brief Gets a file path from a model index /// \param index The model index /// \return The file path as a string /// QString getFileName(const QModelIndex& index); /// /// \brief Gets the file paths of the given model indexes /// \param indices The model indexes /// \return The full file paths /// QStringList getFileNames(const QModelIndexList& indices); /// /// \brief Get the underlying item model associated with this project /// \return The project files as a QAbstractItemModel /// QAbstractItemModel* model(void) { return itemModel; } History* history() const { if (mooc == nullptr) { return nullptr; } return mooc->history; } signals: /// /// \brief Emitted when a file gets renamed /// \param oldName The previous name of the file /// \param newName The new name of the file /// void fileRenamed(const QString& oldName, const QString& newName); void moocChanged(const MOOCAssignment* mooc); public slots: void openTabsChanged(const QStringList& files, int currentTab); void activeSolverConfigChanged(const SolverConfiguration* sc); private slots: void on_itemChanged(QStandardItem* item); private: enum Role { Type = Qt::UserRole, Path = Qt::UserRole + 1, OriginalLabel = Qt::UserRole + 2 }; void loadJSON(const QJsonObject& obj, const QFileInfo& fi, ConfigWindow* configWindow, QStringList& warnings); void loadLegacy(QDataStream& in, const QFileInfo& fi, ConfigWindow* configWindow, QStringList& warnings); QStringList getFiles(NodeType type) const; QString relativeToProject(const QString& fileName); bool modified = false; QStandardItemModel* itemModel; QString projFile; QStandardItem* rootItem; QStandardItem* modelsItem; QStandardItem* checkersItem; QStandardItem* dataItem; QStandardItem* configsItem; QStandardItem* otherItem; MOOCAssignment* mooc = nullptr; QStringList openTabs; int openTabIndex; QString selectedBuiltinConfigId; QString selectedBuiltinConfigVersion; QString selectedSolverConfigFile; const QList& solverConfigs; QMap entries; }; Q_DECLARE_OPERATORS_FOR_FLAGS(Project::NodeTypes) #endif // PROJECT_H ================================================ FILE: MiniZincIDE/projectbrowser.cpp ================================================ #include "projectbrowser.h" #include "ui_projectbrowser.h" #include "ide.h" #include "exception.h" #include #include #include #include #include ProjectBrowser::ProjectBrowser(QWidget *parent) : QWidget(parent), ui(new Ui::ProjectBrowser) { ui->setupUi(this); setupContextMenu(); } ProjectBrowser::~ProjectBrowser() { delete ui; } void ProjectBrowser::project(Project* p) { proj = p; ui->treeView->setModel(proj->model()); ui->treeView->expandToDepth(1); } void ProjectBrowser::setupContextMenu() { auto contextMenu = new QMenu(this); contextMenu->addAction("Open file", [=] () { auto selected = ui->treeView->selectionModel()->selectedIndexes(); emit openRequested(proj->getFileNames(selected)); }); auto runWith = contextMenu->addAction("Run model with this data", [=] () { auto selected = ui->treeView->selectionModel()->selectedIndexes(); emit runRequested(proj->getFileNames(selected)); }); contextMenu->addAction("Add existing file(s)...", [=] () { auto fileNames = QFileDialog::getOpenFileNames(this, tr("Select one or more files to open"), IDE::instance()->getLastPath(), "MiniZinc Files (*.mzn *.dzn *.json)"); proj->add(fileNames); if (fileNames.count()) { IDE::instance()->setLastPath(QFileInfo(fileNames.last()).absolutePath() + "/"); } }); auto rename = contextMenu->addAction("Rename file", [=] () { ui->treeView->edit(ui->treeView->currentIndex()); }); contextMenu->addAction("Remove from project", [=] () { auto selected = ui->treeView->selectionModel()->selectedIndexes(); emit removeRequested(proj->getFileNames(selected)); }); connect(ui->treeView, &QWidget::customContextMenuRequested, [=] (const QPoint& pos) { auto selected = ui->treeView->selectionModel()->selectedIndexes(); if (!proj || selected.empty()) { return; } int numModelsSelected = 0; int numDataSelected = 0; int numCheckersSelected = 0; int numSolverConfigsSelected = 0; int numOthersSelected = 0; for (auto& index : selected) { switch (proj->getType(index)) { case Project::Model: numModelsSelected++; break; case Project::Data: numDataSelected++; break; case Project::Checker: numCheckersSelected++; break; case Project::SolverConfig: numSolverConfigsSelected++; break; case Project::Other: numOthersSelected++; break; default: return; // No applicable actions } } rename->setDisabled(selected.length() > 1); runWith->setDisabled(numOthersSelected > 0 || numCheckersSelected > 1 || numSolverConfigsSelected > 1); if (numDataSelected == 0 && numCheckersSelected == 1) { runWith->setText("Run model with this checker"); } else if (numDataSelected > 0) { runWith->setText("Run model with this data"); } else { runWith->setText("Run model"); } contextMenu->exec(ui->treeView->mapToGlobal(pos)); }); } void ProjectBrowser::on_treeView_activated(const QModelIndex &index) { emit openRequested({proj->getFileName(index)}); } ================================================ FILE: MiniZincIDE/projectbrowser.h ================================================ #ifndef PROJECTBROWSER_H #define PROJECTBROWSER_H #include #include #include #include #include #include "project.h" #include "moocsubmission.h" namespace Ui { class ProjectBrowser; } class ProjectBrowser : public QWidget { Q_OBJECT friend class TestIDE; public: explicit ProjectBrowser(QWidget *parent = nullptr); ~ProjectBrowser(); /// /// \brief Gets the project associated with this project browser /// \return A pointer to the project /// Project* project(void) { return proj; } /// /// \brief Sets the project associated with this project browser /// \param project The new project /// void project(Project* project); signals: void runRequested(const QStringList& files); void openRequested(const QStringList& files); void removeRequested(const QStringList& files); private slots: void on_treeView_activated(const QModelIndex &index); private: Ui::ProjectBrowser *ui; void setupContextMenu(void); Project* proj = nullptr; }; #endif // PROJECTBROWSER_H ================================================ FILE: MiniZincIDE/projectbrowser.ui ================================================ ProjectBrowser 0 0 400 300 Form 0 0 0 0 Qt::CustomContextMenu QAbstractItemView::EditKeyPressed QAbstractItemView::ExtendedSelection 10 false ================================================ FILE: MiniZincIDE/server/connector.js ================================================ const MiniZincIDE = (() => { const callbacks = {}; const responses = []; const freeSlots = []; let userData; window.addEventListener('message', (e) => { switch (e.data.event) { case 'response': { resolveResponse(e.data.id, e.data.payload); break; } case 'error': { rejectResponse(e.data.id, e.data.message); break; } default: if (e.data.event in callbacks) { callbacks[e.data.event].forEach(callback => { callback(e.data.payload); }); } break; } }); function resolveResponse(index, payload) { const { resolve } = responses[index]; resolve(payload); responses[index] = null; freeSlots.push(index); } function rejectResponse(index, message) { const { reject } = responses[index]; reject(message); responses[index] = null; freeSlots.push(index); } function createPromise(message) { return new Promise((resolve, reject) => { const id = freeSlots.length > 0 ? freeSlots.pop() : responses.length; responses[id] = {resolve, reject}; window.parent.postMessage({ ...message, id }, '*'); }); } function on(event, callback) { if (!(event in callbacks)) { callbacks[event] = new Set(); } callbacks[event].add(callback); } function off(event, callback) { if (event in callbacks) { callbacks[event].delete(callback); } } function getUserData() { return new Promise((resolve, reject) => { if (userData === undefined) { on('init', (data) => { userData = data; resolve(userData); }); } else { resolve(userData); } }); } function goToSolution(idx) { window.parent.postMessage({ event: 'rebroadcast', message: { event: 'goToSolution', payload: idx } }, '*'); } function solve(modelFile, dataFiles, options) { window.parent.postMessage({ event: 'solve', modelFile, dataFiles, options }, '*'); } function getNumSolutions() { return createPromise({ event: 'getNumSolutions' }); } function getSolution(index) { return createPromise({ event: 'getSolution', index }); } function getAllSolutions() { return createPromise({ event: 'getAllSolutions' }); } function getStatus() { return createPromise({ event: 'getStatus' }); } function getFinishTime() { return createPromise({ event: 'getFinishTime' }); } return { getUserData, on, off, goToSolution, solve, getNumSolutions, getSolution, getAllSolutions, getStatus, getFinishTime }; })(); ================================================ FILE: MiniZincIDE/server/index.html ================================================ %3 | MiniZincIDE
Connecting...
================================================ FILE: MiniZincIDE/server.cpp ================================================ #include "server.h" #include #include #include #include "ideutils.h" #include "exception.h" VisConnector::~VisConnector() { for (auto* c : _clients) { delete c; } } void VisConnector::newWebSocketClient(QWebSocket* socket) { _clients << socket; connect(socket, &QWebSocket::disconnected, this, &VisConnector::webSocketClientDisconnected); connect(socket, &QWebSocket::textMessageReceived, this, &VisConnector::webSocketMessageReceived); QJsonObject obj({{"event", "init"}, {"windows", _windows}, {"numSolutions", _solutions.isEmpty() ? 0 : _solutions.first().size()}}); auto json = QString::fromUtf8(QJsonDocument(obj).toJson()); socket->sendTextMessage(json); } void VisConnector::webSocketClientDisconnected() { auto* client = qobject_cast(sender()); if (client) { _clients.removeAll(client); client->deleteLater(); } } void VisConnector::addWindow(const QString& key, const QString& url, const QJsonValue& userData) { _windows[key] = QJsonObject({{"url", url}, {"userData", userData}}); _solutions[key] = QJsonArray(); QJsonObject obj({{"event", "window"}, {"windowId", key}, {"url", url}, {"userData", userData}}); broadcastMessage(QJsonDocument(obj)); } void VisConnector::addSolution(const QJsonObject& solution, qint64 time) { auto solIndex = _solutionCount++; for (auto it = solution.begin(); it != solution.end(); it++) { QJsonObject sol({{"time", time}, {"data", it.value()}, {"index", solIndex}}); if (_solutions.contains(it.key())) { _solutions[it.key()].append(sol); } } QJsonObject obj({{"event", "solution"}, {"time", time}, {"solution", solution}, {"index", solIndex}}); broadcastMessage(QJsonDocument(obj)); } void VisConnector::setFinalStatus(const QString& status, qint64 time) { _finalStatus = QJsonObject({{"time", time}, {"status", status}}); QJsonObject obj({{"event", "status"}, {"payload", QJsonObject({ {"time", time}, {"status", status} })}}); broadcastMessage(QJsonDocument(obj)); } void VisConnector::setFinished(qint64 time) { _finishTime = time; QJsonObject obj({{"event", "finish"}, {"payload", time}}); broadcastMessage(QJsonDocument(obj)); } void VisConnector::broadcastMessage(const QJsonDocument& message) { auto json = QString::fromUtf8(message.toJson()); for (auto* client : _clients) { client->sendTextMessage(json); } } void VisConnector::webSocketMessageReceived(const QString& message) { auto* socket = qobject_cast(sender()); QJsonParseError error; auto json = QJsonDocument::fromJson(message.toUtf8(), &error); auto msg = json.object(); auto event = msg["event"].toString(); if (event == "solve") { auto mf = msg["modelFile"].toString(); QStringList dfs; auto dataFiles = msg["dataFiles"]; bool dataFilesGiven = dataFiles.isArray(); for (auto v : dataFiles.toArray()) { dfs << v.toString(); } auto opts = msg["options"].toObject().toVariantMap(); emit solveRequested(mf, dataFilesGiven, dfs, opts); } else if (event == "getNumSolutions") { QJsonObject obj({{"event", "response"}, {"id", msg["id"]}, {"window", msg["window"]}, {"payload", _solutions.isEmpty() ? 0 : _solutions.first().size()}}); socket->sendTextMessage(QString::fromUtf8(QJsonDocument(obj).toJson())); } else if (event == "getSolution") { auto window_id = msg["window"].toString(); if (!_solutions.contains(window_id)) { QJsonObject obj({{"event", "error"}, {"id", msg["id"]}, {"window", msg["window"]}, {"message", "Invalid window."}}); socket->sendTextMessage(QString::fromUtf8(QJsonDocument(obj).toJson())); return; } auto idx = msg["index"].toInt(); if (idx < 0) { idx += _solutions[window_id].count(); } if (idx < 0 || idx >= _solutions[window_id].count()) { QJsonObject obj({{"event", "error"}, {"id", msg["id"]}, {"window", msg["window"]}, {"message", "Index out of range."}}); socket->sendTextMessage(QString::fromUtf8(QJsonDocument(obj).toJson())); return; } QJsonObject obj({{"event", "response"}, {"id", msg["id"]}, {"window", msg["window"]}, {"payload", _solutions[window_id][idx]}}); socket->sendTextMessage(QString::fromUtf8(QJsonDocument(obj).toJson())); } else if (event == "getAllSolutions") { auto window_id = msg["window"].toString(); if (!_solutions.contains(window_id)) { QJsonObject obj({{"event", "error"}, {"id", msg["id"]}, {"window", msg["window"]}, {"message", "Invalid window."}}); socket->sendTextMessage(QString::fromUtf8(QJsonDocument(obj).toJson())); return; } QJsonObject obj({{"event", "response"}, {"id", msg["id"]}, {"window", window_id}, {"payload", _solutions[window_id]}}); socket->sendTextMessage(QString::fromUtf8(QJsonDocument(obj).toJson())); } else if (event == "getStatus") { QJsonObject obj({{"event", "response"}, {"id", msg["id"]}, {"window", msg["window"]}, {"payload", _finalStatus}}); socket->sendTextMessage(QString::fromUtf8(QJsonDocument(obj).toJson())); } else if (event == "getFinishTime") { QJsonObject obj({{"event", "response"}, {"id", msg["id"]}, {"window", msg["window"]}, {"payload", _finishTime == -1 ? QJsonValue() : _finishTime}}); socket->sendTextMessage(QString::fromUtf8(QJsonDocument(obj).toJson())); } else { QJsonObject obj({{"event", "error"}, {"id", msg["id"]}, {"window", msg["window"]}, {"message", QString("Unknown event '%1'.").arg(event)}}); socket->sendTextMessage(QString::fromUtf8(QJsonDocument(obj).toJson())); } } Server::Server(QObject *parent) : QObject(parent), http(new QTcpServer(this)), ws(new QWebSocketServer("MiniZincIDE", QWebSocketServer::NonSecureMode, this)) { connect(http, &QTcpServer::newConnection, this, &Server::newHttpClient); connect(ws, &QWebSocketServer::newConnection, this, &Server::newWebSocketClient); } Server::~Server() { for (auto* c : connectors) { delete c; } } void Server::listen(quint16 httpPort, quint16 wsPort) { if (!http->isListening() || httpPort != initialHttpPort) { initialHttpPort = httpPort; qint16 p = httpPort; http->close(); for (int i = 0; i < 10; i++) { if (http->listen(QHostAddress::LocalHost, p) || p == 0) { break; } p++; } if (!http->isListening()) { throw ServerError("Failed to start HTTP visualisation server"); } } if (!ws->isListening() || wsPort != initialWsPort) { initialWsPort = wsPort; quint16 p = wsPort; ws->close(); for (int i = 0; i < 10; i++) { if (ws->listen(QHostAddress::LocalHost, p) || p == 0) { break; } p++; } if (!ws->isListening()) { throw ServerError("Failed to start WebSocket visualisation server"); } } } VisConnector* Server::addConnector(const QString& label, const QStringList& roots) { auto* c = new VisConnector(this); c->_label = label; c->_roots = roots; c->_url.setScheme("http"); c->_url.setHost(address()); c->_url.setPort(port()); c->_url.setPath(QString("/%1").arg(connectors.size())); connectors << c; return c; } void Server::clear() { for (auto* c : connectors) { delete c; } connectors.clear(); } bool Server::sendToLastClient(const QJsonDocument& message) { auto json = QString::fromUtf8(message.toJson()); for (auto it = clients.rbegin(); it != clients.rend(); it++) { if ((*it)->isValid()) { (*it)->sendTextMessage(json); return true; } } return false; } void Server::newHttpClient() { auto* socket = http->nextPendingConnection(); connect(socket, &QTcpSocket::readyRead, this, &Server::handleHttpRequest); connect(socket, &QTcpSocket::stateChanged, this, [=] (QAbstractSocket::SocketState s) { if (s == QAbstractSocket::UnconnectedState) { socket->deleteLater(); } }); } void Server::handleHttpRequest() { auto* socket = qobject_cast(sender()); if (!socket->canReadLine()) { return; } QTextStream ts(socket); auto parts = ts.readLine().split(QRegularExpression("\\s+")); if (parts.length() < 3 || parts[0] != "GET") { return; } // Connector script if (parts[1] == "/minizinc-ide.js") { QFile f(":/server/server/connector.js"); f.open(QFile::ReadOnly | QFile::Text); QTextStream c(&f); ts << "HTTP/1.1 200 OK\r\n" << "Content-Type: text/javascript\r\n" << "\r\n" << c.readAll(); socket->close(); return; } QRegularExpression re("^/?(\\d+)(/.*)?$"); auto p = QUrl::fromPercentEncoding(parts[1].toUtf8()); auto match = re.match(p); if (!match.hasMatch()) { ts << "HTTP/1.1 404 OK\r\n" << "\r\n" << "File not found."; socket->close(); return; } auto index = match.captured(1).toInt(); if (index >= connectors.size()) { ts << "HTTP/1.1 404 OK\r\n" << "\r\n" << "File not found."; socket->close(); return; } auto path = match.captured(2); // Window management page if (path.isEmpty() || path == "index.html") { QString title = connectors[index]->_label.toHtmlEscaped(); QFile f(":/server/server/index.html"); f.open(QFile::ReadOnly | QFile::Text); QTextStream c(&f); ts << "HTTP/1.1 200 OK\r\n" << "Content-Type: text/html\r\n" << "\r\n" << c.readAll().arg(ws->serverUrl().toString()).arg(index).arg(title); socket->close(); return; } // Static file server QStringList base_dirs({":/server/server"}); base_dirs << connectors[index]->_roots; for (auto& base_dir : base_dirs) { auto full_path = base_dir + "/" + path; if (IDEUtils::isChildPath(base_dir, full_path)) { QFile file(full_path); if (!file.exists()) { continue; } if (!file.open(QFile::ReadOnly)) { ts << "HTTP/1.1 500 OK\r\n" << "\r\n" << "Failed to retrieve file"; socket->close(); return; } QMimeDatabase db; QMimeType mime = db.mimeTypeForFile(QFileInfo(file)); ts << "HTTP/1.1 200 OK\r\n" << "Content-Type: " << mime.name() << "\r\n" << "\r\n"; ts.flush(); socket->write(file.readAll()); socket->close(); return; } } ts << "HTTP/1.1 404 OK\r\n" << "\r\n" << "File not found."; socket->close(); return; } void Server::newWebSocketClient() { auto* socket = ws->nextPendingConnection(); auto path = socket->requestUrl().path(); bool ok = false; auto index = path.right(path.size() - 1).toInt(&ok); if (!ok || index >= connectors.size()) { socket->close(); return; } auto* c = connectors[index]; c->newWebSocketClient(socket); clients << socket; connect(socket, &QWebSocket::disconnected, this, &Server::webSocketClientDisconnected); } void Server::webSocketClientDisconnected() { auto* client = qobject_cast(sender()); if (client) { clients.removeAll(client); } } ================================================ FILE: MiniZincIDE/server.h ================================================ #ifndef SERVER_H #define SERVER_H #include #include #include #include #include class VisConnector : public QObject { Q_OBJECT private: QString _label; QStringList _roots; QList _clients; QJsonObject _windows; QMap _solutions; QJsonValue _finalStatus; int _solutionCount = 0; qint64 _finishTime = -1; QUrl _url; friend class Server; public: explicit VisConnector(QObject *parent = nullptr) : QObject(parent) {} ~VisConnector(); QUrl url() const { return _url; } signals: void solveRequested(const QString& modelFile, bool dataFilesGiven, const QStringList& dataFiles, const QVariantMap& options); public slots: void addWindow(const QString& key, const QString& url, const QJsonValue& userData); void addSolution(const QJsonObject& solution, qint64 time); void setFinalStatus(const QString& status, qint64 time); void setFinished(qint64 time); private slots: void newWebSocketClient(QWebSocket* s); void webSocketClientDisconnected(); void webSocketMessageReceived(const QString& message); void broadcastMessage(const QJsonDocument& message); }; /// /// \brief HTTP and WebSocket server for web visualisation /// class Server : public QObject { Q_OBJECT public: explicit Server(QObject *parent = nullptr); ~Server(); void listen(quint16 httpPort = 3000, quint16 wsPort = 3100); QString address() const { return http->serverAddress().toString(); } quint16 port() const { return http->serverPort(); } quint16 desiredHttpPort() const { return initialHttpPort; } quint16 desiredWsPort() const { return initialWsPort; } VisConnector* addConnector(const QString& label, const QStringList& roots); void clear(); bool sendToLastClient(const QJsonDocument& doc); signals: void solve(const QString& model, const QStringList& data, const QVariantMap& options); private slots: void newHttpClient(); void newWebSocketClient(); void webSocketClientDisconnected(); void handleHttpRequest(); private: QTcpServer* http; QWebSocketServer* ws; QList connectors; QList clients; quint16 initialHttpPort; quint16 initialWsPort; }; #endif // SERVER_H ================================================ FILE: MiniZincIDE/solver.cpp ================================================ #include "solver.h" #include "process.h" #include "exception.h" #include #include #include #include #include namespace { void parseArgList(const QString& s, QVariantMap& map) { QStringList ret; bool hadEscape = false; bool inSingleQuote = false; bool inDoubleQuote = false; QString currentArg; foreach (const QChar c, s) { if (hadEscape) { currentArg += c; hadEscape = false; } else { if (c=='\\') { hadEscape = true; } else if (c=='"') { if (inDoubleQuote) { inDoubleQuote=false; ret.push_back(currentArg); currentArg = ""; } else if (inSingleQuote) { currentArg += c; } else { inDoubleQuote = true; } } else if (c=='\'') { if (inSingleQuote) { inSingleQuote=false; ret.push_back(currentArg); currentArg = ""; } else if (inDoubleQuote) { currentArg += c; } else { inSingleQuote = true; } } else if (!inSingleQuote && !inDoubleQuote && c==' ') { if (currentArg.size() > 0) { ret.push_back(currentArg); currentArg = ""; } } else { currentArg += c; } } } if (currentArg.size() > 0) { ret.push_back(currentArg); } QString flag; for (auto& arg : ret) { if (arg.startsWith("-")) { if (!flag.isEmpty()) { // Must be a boolean switch map[flag] = true; } flag = arg; } else if (!flag.isEmpty()) { // Flag with arg map[flag] = arg; flag.clear(); } else { // Arg with no flag map[arg] = true; } } } } Solver::Solver(const QJsonObject& sj) { json = sj; name = sj["name"].toString(); QJsonObject extraInfo = sj["extraInfo"].toObject(); executable_resolved = extraInfo["executable"].toString(""); mznlib_resolved = extraInfo["mznlib"].toString(""); configFile = extraInfo["configFile"].toString(""); if (extraInfo["defaultFlags"].isArray()) { QJsonArray ei = extraInfo["defaultFlags"].toArray(); for (auto df : ei) { defaultFlags.push_back(df.toString()); } } isDefaultSolver = (extraInfo["isDefault"].isBool() && extraInfo["isDefault"].toBool()); contact = sj["contact"].toString(""); website = sj["website"].toString(""); if (sj["inputType"].isString()) { auto t = sj["inputType"].toString(); if (t == "FZN") { inputType = SolverInputType::I_FZN; } else if (t == "MZN") { inputType = SolverInputType::I_MZN; } else if (t == "NL") { inputType = SolverInputType::I_NL; } else if (t == "JSON") { inputType = SolverInputType::I_JSON; } else { inputType = SolverInputType::I_UNKNOWN; } } else if (sj["supportsMzn"].toBool()) { inputType = SolverInputType::I_MZN; } else if (sj["supportsFzn"].toBool(true)) { inputType = SolverInputType::I_FZN; } else { inputType = SolverInputType::I_UNKNOWN; } if (sj["requiredFlags"].isArray()) { QJsonArray rfs = sj["requiredFlags"].toArray(); for (auto rf : rfs) { requiredFlags.push_back(rf.toString()); } } if (sj["stdFlags"].isArray()) { QJsonArray sfs = sj["stdFlags"].toArray(); for (auto sf : sfs) { stdFlags.push_back(sf.toString()); } } if (sj["extraFlags"].isArray()) { QJsonArray sfs = sj["extraFlags"].toArray(); for (auto sf : sfs) { if (sf.isArray()) { QJsonArray extraFlagA = sf.toArray(); if (extraFlagA.size()==4) { SolverFlag extraFlag; extraFlag.min=1.0; extraFlag.max=0.0; extraFlag.name = extraFlagA[0].toString(); QRegularExpression re_opt("^(int|float|bool)(:([0-9a-zA-Z.-]+):([0-9a-zA-Z.-]+))?"); QRegularExpressionMatch re_opt_match = re_opt.match(extraFlagA[2].toString()); if (re_opt_match.hasMatch()) { if (re_opt_match.captured(1)=="int") { if (re_opt_match.captured(3).isEmpty()) { extraFlag.t = SolverFlag::T_INT; } else { extraFlag.t = SolverFlag::T_INT_RANGE; extraFlag.min_ll = re_opt_match.captured(3).toLongLong(); extraFlag.max_ll = re_opt_match.captured(4).toLongLong(); } } else if (re_opt_match.captured(1)=="float") { if (re_opt_match.captured(3).isEmpty()) { extraFlag.t = SolverFlag::T_FLOAT; } else { extraFlag.t = SolverFlag::T_FLOAT_RANGE; extraFlag.min = re_opt_match.captured(3).toDouble(); extraFlag.max = re_opt_match.captured(4).toDouble(); } } else if (re_opt_match.captured(1)=="bool") { if (re_opt_match.captured(3).isEmpty()) { extraFlag.t = SolverFlag::T_BOOL; } else { extraFlag.t = SolverFlag::T_BOOL_ONOFF; extraFlag.options = QStringList({re_opt_match.captured(3),re_opt_match.captured(4)}); } } } else { if (extraFlagA[2].toString()=="string") { extraFlag.t = SolverFlag::T_STRING; } else if (extraFlagA[2].toString().startsWith("opt:")) { extraFlag.t = SolverFlag::T_OPT; extraFlag.options = extraFlagA[2].toString().mid(4).split(":"); // } else if (extraFlagA[2].toString()=="solver") { // extraFlag.t = SolverFlag::T_SOLVER; } else { continue; } } extraFlag.description = extraFlagA[1].toString(); auto def = extraFlagA[3].toString(); switch (extraFlag.t) { case SolverFlag::T_INT: case SolverFlag::T_INT_RANGE: extraFlag.def = def.toLongLong(); break; case SolverFlag::T_BOOL: extraFlag.def = def == "true"; break; case SolverFlag::T_BOOL_ONOFF: if (def == extraFlag.options[0]) { extraFlag.def = true; } else if (def == extraFlag.options[1]) { extraFlag.def = false; } else { extraFlag.def = def == "true"; } break; case SolverFlag::T_FLOAT: case SolverFlag::T_FLOAT_RANGE: extraFlag.def = def.toDouble(); break; case SolverFlag::T_STRING: case SolverFlag::T_OPT: case SolverFlag::T_SOLVER: extraFlag.def = def; break; } extraFlags.push_back(extraFlag); } } } } isGUIApplication = sj["isGUIApplication"].toBool(false); needsMznExecutable = sj["needsMznExecutable"].toBool(false); needsStdlibDir = sj["needsStdlibDir"].toBool(false); needsPathsFile = sj["needsPathsFile"].toBool(false); needsSolns2Out = sj["needsSolns2Out"].toBool(true); executable = sj["executable"].toString(""); id = sj["id"].toString(); version = sj["version"].toString(); mznlib = sj["mznlib"].toString(); mznLibVersion = sj["mznlibVersion"].toInt(); } Solver& Solver::lookup(const QString& str) { MznProcess p; try { auto result = p.run({ "--solver-json", str }); auto solver_doc = QJsonDocument::fromJson(result.stdOut.toUtf8()); if (!solver_doc.isObject()) { throw ConfigError("Failed to find solver " + str); } Solver solver(solver_doc.object()); auto& solvers = MznDriver::get().solvers(); for (auto* s: solvers) { if (solver == *s) { return *s; } } solvers << new Solver(solver); return *solvers.last(); } catch (Exception&) { throw DriverError("Failed to lookup solver " + str); } } Solver& Solver::lookup(const QString& id, const QString& version, bool strict) { if (strict) { return lookup(id + "@" + version); } try { return lookup(id + "@" + version); } catch (ConfigError&) { return lookup(id); } } bool Solver::operator==(const Solver& s) const { if (configFile.isEmpty() && s.configFile.isEmpty()) { return id == s.id && version == s.version; } return configFile == s.configFile; } bool Solver::hasAllRequiredFlags() { for (auto& rf : requiredFlags) { if (!defaultFlags.contains(rf)) { return false; } } return true; } SolverConfiguration::SolverConfiguration(const Solver& _solver, bool builtin) : solverDefinition(_solver), isBuiltin(builtin), timeLimit(0), printIntermediate(true), numSolutions(1), numOptimal(1), verboseCompilation(false), verboseSolving(false), compilationStats(false), solvingStats(false), outputTiming(false), outputObjective(true), optimizationLevel(1), numThreads(1), freeSearch(false), modified(false) { solver = _solver.id + "@" + _solver.version; } SolverConfiguration SolverConfiguration::loadJSON(const QString& filename, QStringList& warnings) { QFile file(filename); QFileInfo fi(file); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { throw FileError("Failed to open file " + filename); } QJsonParseError error; auto json = QJsonDocument::fromJson(file.readAll(), &error); file.close(); if (json.isNull()) { QString message; QTextStream ts(&message); ts << "Could not parse " << fi.fileName() << ". Error at " << error.offset << ": " << error.errorString() << "."; throw ConfigError(message); } auto sc = loadJSON(json, warnings); sc.paramFile = fi.canonicalFilePath(); return sc; } SolverConfiguration SolverConfiguration::loadJSON(const QJsonDocument& json, QStringList& warnings) { auto configObject = json.object(); QString solverValue = configObject.value("--solver").isUndefined() ? configObject.value("solver").toString() : configObject.value("--solver").toString(); Solver* solver = nullptr; try { solver = &Solver::lookup(solverValue); } catch (Exception& e) { warnings << e.message(); warnings << "Using default solver instead."; solver = MznDriver::get().defaultSolver(); } if (solver == nullptr) { throw ConfigError("Failed to load fallback solver."); } SolverConfiguration sc(*solver); for (auto it = configObject.begin(); it != configObject.end(); it++) { QString key = (it.key().startsWith("-") || it.key().startsWith("_")) ? it.key() : "--" + it.key(); if (key == "--solver") { sc.solver = it.value().toString(); } else if (key == "-t" || key == "--time-limit") { sc.timeLimit = it.value().toInt(); } else if (key == "-a" || key == "--all-solutions") { sc.numSolutions = 0; sc.printIntermediate = true; } else if (key == "-i" || key == "--intermediate" || key == "--intermediate-solutions") { sc.printIntermediate = it.value().toBool(true); } else if (key == "--all-satisfaction") { sc.numSolutions = 0; } else if (key == "-n" || key == "--num-solutions") { sc.numSolutions = it.value().toInt(1); } else if (key == "-a-o" || key == "--all-optimal") { sc.numOptimal = 0; } else if (key == "-n-o" || key == "--num-optimal") { sc.numOptimal = it.value().toInt(1); } else if (key == "--verbose-compilation" || key == "--verbose" || key == "-v") { sc.verboseCompilation = it.value().toBool(); } else if (key == "--verbose-solving" || key == "--verbose" || key == "-v") { sc.verboseSolving = it.value().toBool(); } else if (key == "--compiler-statistics" || key == "--statistics" || key == "-s") { sc.compilationStats = it.value().toBool(); } else if (key == "--solver-statistics" || key == "--statistics" || key == "-s") { sc.solvingStats = it.value().toBool(); } else if (key == "--output-time") { sc.outputTiming = it.value().toBool(); } else if (key == "--output-objective") { sc.outputObjective = it.value().toBool(); } else if (key == "-O0" && it.value().toBool()) { sc.optimizationLevel = 0; } else if (key == "-O1" && it.value().toBool()) { sc.optimizationLevel = 1; } else if (key == "-O2" && it.value().toBool()) { sc.optimizationLevel = 2; } else if (key == "-O3" && it.value().toBool()) { sc.optimizationLevel = 3; } else if (key == "-O4" && it.value().toBool()) { sc.optimizationLevel = 4; } else if (key == "-O5" && it.value().toBool()) { sc.optimizationLevel = 5; } else if (key == "-O") { sc.optimizationLevel = it.value().toInt(); } else if (key == "-D" || key == "--cmdline-data") { if (it.value().isArray()) { for (auto v : it.value().toArray()) { sc.additionalData.push_back(v.toString()); } } else { sc.additionalData.push_back(it.value().toString()); } } else if (key == "-p" || key == "--parallel") { sc.numThreads = it.value().toInt(1); } else if (key == "-r" || key == "--random-seed") { sc.randomSeed = it.value().toVariant(); } else if (key == "-f" || key == "--free-search") { sc.freeSearch = it.value().toBool(); } else if (key == "--backend-flags" || key == "--fzn-flags" || key == "--flatzinc-flags" || key == "--mzn-flags" || key == "--minizinc-flags") { if (it.value().isString()) { parseArgList(it.value().toString(), sc.solverBackendOptions); } else if (it.value().isObject()) { auto object = it.value().toObject(); for (auto it = object.begin(); it != object.end(); it++) { sc.solverBackendOptions.insert(it.key(), it.value().toVariant()); } } } else { sc.extraOptions[key] = it.value().toVariant(); } } for (auto f : solver->extraFlags) { if (f.t == SolverFlag::T_BOOL_ONOFF && sc.extraOptions.contains(f.name)) { // Convert on/off string to bool (TODO: would be nice to handle this in minizinc) sc.extraOptions[f.name] = sc.extraOptions[f.name] == f.options[0]; } } return sc; } SolverConfiguration SolverConfiguration::loadLegacy(const QJsonDocument &json, QStringList& warnings) { auto sco = json.object(); Solver* solver = nullptr; try { solver = &Solver::lookup(sco["id"].toString(), sco["version"].toString(), false); } catch (Exception& e) { warnings << e.message(); warnings << "Using default solver instead."; solver = MznDriver::get().defaultSolver(); } if (solver == nullptr) { throw ConfigError("Failed to load fallback solver."); } SolverConfiguration newSc(*solver); // if (sco["name"].isString()) { // newSc.name = sco["name"].toString(); // } if (sco["timeLimit"].isDouble()) { newSc.timeLimit = sco["timeLimit"].toInt(); } // if (sco["defaultBehavior"].isBool()) { // newSc.defaultBehaviour = sco["defaultBehavior"].toBool(); // } if (sco["printIntermediate"].isBool()) { newSc.printIntermediate = sco["printIntermediate"].toBool(); } if (sco["stopAfter"].isDouble()) { newSc.numSolutions = sco["stopAfter"].toInt(); } if (sco["verboseFlattening"].isBool()) { newSc.verboseCompilation = sco["verboseFlattening"].toBool(); } if (sco["flatteningStats"].isBool()) { newSc.compilationStats = sco["flatteningStats"].toBool(); } if (sco["optimizationLevel"].isDouble()) { newSc.optimizationLevel = sco["optimizationLevel"].toInt(); } if (sco["additionalData"].isString() && !sco["additionalData"].toString().isEmpty()) { newSc.additionalData << sco["additionalData"].toString(); } if (sco["additionalCompilerCommandline"].isString() && !sco["additionalCompilerCommandline"].toString().isEmpty()) { parseArgList(sco["additionalCompilerCommandline"].toString(), newSc.extraOptions); } if (sco["nThreads"].isDouble()) { newSc.numThreads = sco["nThreads"].toInt(); } if (sco["randomSeed"].isDouble()) { newSc.randomSeed = sco["randomSeed"].toDouble(); } if (sco["solverFlags"].isString() && !sco["solverFlags"].toString().isEmpty()) { parseArgList(sco["solverFlags"].toString(), newSc.solverBackendOptions); } if (sco["freeSearch"].isBool()) { newSc.freeSearch = sco["freeSearch"].toBool(); } if (sco["verboseSolving"].isBool()) { newSc.verboseSolving = sco["verboseSolving"].toBool(); } if (sco["outputTiming"].isBool()) { newSc.outputTiming = sco["outputTiming"].toBool(); } if (sco["solvingStats"].isBool()) { newSc.solvingStats = sco["solvingStats"].toBool(); } if (sco["extraOptions"].isObject() && sco["useExtraOptions"].toBool()) { QJsonObject extraOptions = sco["extraOptions"].toObject(); for (auto& f : solver->extraFlags) { if (extraOptions.contains(f.name) && f.def.toString() != extraOptions[f.name].toString()) { newSc.extraOptions[f.name] = extraOptions[f.name].toString(); extraOptions.remove(f.name); } } for (auto it = extraOptions.begin(); it != extraOptions.end(); it++) { // Unrecognised, but we shouldn't ignore them newSc.extraOptions[it.key()] = it.value().toVariant(); } } return newSc; } QString SolverConfiguration::name() const { QStringList parts; if (isBuiltin) { parts << solverDefinition.name << solverDefinition.version; } else { if (paramFile.isEmpty()) { parts << "Unsaved Configuration"; } else { parts << QFileInfo(paramFile).completeBaseName(); } parts << "(" + solverDefinition.name + " " + solverDefinition.version + ")"; } if (modified) { parts << "*"; } return parts.join(" "); } QByteArray SolverConfiguration::toJSON(void) const { QJsonDocument jsonDoc; jsonDoc.setObject(toJSONObject()); return jsonDoc.toJson(); } QJsonObject SolverConfiguration::toJSONObject(void) const { QJsonObject config; config["solver"] = solver; if (timeLimit > 0) { config["time-limit"] = timeLimit; } if ((supports("-a") || supports("-i"))) { config["intermediate-solutions"] = printIntermediate; } if (numSolutions > 1 && supports("-n")) { config["num-solutions"] = numSolutions; } if (numSolutions == 0 && supports("-a")) { config["all-satisfaction"] = true; } if (numOptimal > 1 && supports("-n-o")) { config["num-optimal"] = numOptimal; } if (numOptimal == 0 && supports("-a-o")) { config["all-optimal"] = true; } if (verboseCompilation) { config["verbose-compilation"] = verboseCompilation; } if (verboseSolving && supports("-v")) { config["verbose-solving"] = verboseSolving; } if (compilationStats) { config["compiler-statistics"] = compilationStats; } if (solvingStats && supports("-s")) { config["solver-statistics"] = solvingStats; } if (outputTiming) { config["output-time"] = outputTiming; } if (outputObjective && (solverDefinition.inputType != Solver::I_MZN || supports("--output-objective"))) { config["output-objective"] = true; } if (optimizationLevel != 1) { config["-O"] = optimizationLevel; } QJsonArray arr; for (auto& d : additionalData) { arr.push_back(d); } if (arr.size() > 0) { config["cmdline-data"] = arr; } if (numThreads > 1 && supports("-p")) { config["parallel"] = numThreads; } if (!randomSeed.isNull() && supports("-r")) { config["random-seed"] = QJsonValue::fromVariant(randomSeed); } if (freeSearch && supports("-f")) { config["free-search"] = freeSearch; } for (auto it = extraOptions.begin(); it != extraOptions.end(); it++) { config[it.key()] = QJsonValue::fromVariant(it.value()); } if (!solverBackendOptions.empty()) { config["backend-flags"] = QJsonObject::fromVariantMap(solverBackendOptions); } for (auto f : solverDefinition.extraFlags) { if (f.t == SolverFlag::T_BOOL_ONOFF && extraOptions.contains(f.name)) { // Convert to on/off string instead of bool (TODO: would be nice to handle this in minizinc) config[f.name] = extraOptions[f.name].toBool() ? f.options[0] : f.options[1]; } } return config; } bool SolverConfiguration::syncedOptionsMatch(const SolverConfiguration& sc) const { if (isBuiltin || sc.isBuiltin) { // Built-in configs always can have their options overidden return true; } if (timeLimit != sc.timeLimit) { return false; } if ((supports("-a") || supports("-i")) && (sc.supports("-a") || sc.supports("-i")) && printIntermediate != sc.printIntermediate) { return false; } if (supports("-n") && sc.supports("-n") && (numSolutions > 0 || sc.numSolutions > 0) && numSolutions != sc.numSolutions) { return false; } if (supports("-a") && sc.supports("-a") && (numSolutions == 0 || sc.numSolutions == 0) && numSolutions != sc.numSolutions) { return false; } if (supports("-n-o") && sc.supports("-n-o") && (numOptimal > 0 || sc.numOptimal > 0) && numOptimal != sc.numOptimal) { return false; } if (supports("-a-o") && sc.supports("-a-o") && (numOptimal == 0 || sc.numOptimal == 0) && numOptimal != sc.numOptimal) { return false; } if (verboseCompilation != sc.verboseCompilation) { return false; } if (supports("-v") && sc.supports("-v") && verboseSolving != sc.verboseSolving) { return false; } if (compilationStats != sc.compilationStats) { return false; } if (supports("-s") && sc.supports("-s") && solvingStats != sc.solvingStats) { return false; } if (outputTiming != sc.outputTiming) { return false; } if ((solverDefinition.inputType != Solver::I_MZN || supports("--output-objective")) && (sc.solverDefinition.inputType != Solver::I_MZN || sc.supports("--output-objective")) && outputObjective != sc.outputObjective) { return false; } return true; } bool SolverConfiguration::supports(const QString &flag) const { return solverDefinition.stdFlags.contains(flag); } bool SolverConfiguration::operator==(const SolverConfiguration& sc) const { return solver == sc.solver && solverDefinition.id == sc.solverDefinition.id && solverDefinition.version == sc.solverDefinition.version && isBuiltin == sc.isBuiltin && timeLimit == sc.timeLimit && printIntermediate == sc.printIntermediate && numSolutions == sc.numSolutions && verboseCompilation == sc.verboseCompilation && verboseSolving == sc.verboseSolving && compilationStats == sc.compilationStats && solvingStats == sc.solvingStats && outputTiming == sc.outputTiming && outputObjective == sc.outputObjective && optimizationLevel == sc.optimizationLevel && additionalData == sc.additionalData && numThreads == sc.numThreads && randomSeed == sc.randomSeed && freeSearch == sc.freeSearch && extraOptions == sc.extraOptions; } ================================================ FILE: MiniZincIDE/solver.h ================================================ #ifndef SOLVER_H #define SOLVER_H #include #include #include #include #include struct SolverFlag { QString name; QString description; enum { T_INT, T_INT_RANGE, T_BOOL, T_BOOL_ONOFF, T_FLOAT, T_FLOAT_RANGE, T_STRING, T_OPT, T_SOLVER } t; double min; double max; qlonglong min_ll; qlonglong max_ll; QStringList options; QVariant def; }; Q_DECLARE_METATYPE(SolverFlag) struct Solver { enum SolverInputType { I_FZN, I_MZN, I_NL, I_JSON, I_UNKNOWN }; QString configFile; QString id; QString name; QString executable; QString executable_resolved; QString mznlib; QString mznlib_resolved; QString version; int mznLibVersion; QString description; QString contact; QString website; SolverInputType inputType; bool needsSolns2Out; bool isGUIApplication; bool needsMznExecutable; bool needsStdlibDir; bool needsPathsFile; QStringList stdFlags; QList extraFlags; QStringList requiredFlags; QStringList defaultFlags; QStringList tags; QJsonObject json; bool isDefaultSolver; Solver(void) {} Solver(const QJsonObject& json); static Solver& lookup(const QString& str); static Solver& lookup(const QString& id, const QString& version, bool strict = true); bool hasAllRequiredFlags(void); bool operator==(const Solver&) const; }; class SolverConfiguration { public: SolverConfiguration(const Solver& solver, bool builtin = false); static SolverConfiguration loadJSON(const QString& filename, QStringList& warnings); static SolverConfiguration loadJSON(const QJsonDocument& json, QStringList& warnings); static SolverConfiguration loadLegacy(const QJsonDocument& json, QStringList& warnings); QString solver; const Solver& solverDefinition; QString paramFile; bool isBuiltin; int timeLimit; bool printIntermediate; int numSolutions; int numOptimal; bool verboseCompilation; bool verboseSolving; bool compilationStats; bool solvingStats; bool outputTiming; bool outputObjective; int optimizationLevel; QStringList additionalData; int numThreads; QVariant randomSeed; bool freeSearch; QVariantMap extraOptions; QVariantMap solverBackendOptions; bool modified; /// /// \brief Gets the name of this solver config /// \return The name of the config /// QString name(void) const; /// /// \brief Give the JSON representation of this SolverConfiguration /// \return This solver config in JSON format /// QByteArray toJSON(void) const; /// /// \brief Give the JSON representation of this SolverConfiguration /// \return This solver config as a JSON object /// QJsonObject toJSONObject(void) const; /// /// \brief Determines if these two solver configs have compatible basic options /// \param sc Another solver configuration /// \return Whether the basic options match /// bool syncedOptionsMatch(const SolverConfiguration& sc) const; /// /// \brief Returns whether the given stdFlag is supported by the solver for this config /// \param flag The standard flag /// \return True if the flag is supported and false otherwise /// bool supports(const QString& flag) const; bool operator==(const SolverConfiguration& sc) const; bool operator!=(const SolverConfiguration& sc) const { return !(*this == sc); } }; #endif // SOLVER_H ================================================ FILE: MiniZincIDE/theme.cpp ================================================ #include "theme.h" QString Theme::styleSheet(bool darkMode) const { auto style_sheet = QString("background-color: %1;" "color: %2;") .arg(backgroundColor.get(darkMode).name(QColor::HexArgb)) .arg(textColor.get(darkMode).name(QColor::HexArgb)); if (!isSystemTheme) { // Only change highlight colour for non-system themes // Many platforms have settings/accessibility options for this, so we should probably follow it by default style_sheet += QString( "selection-background-color: %1;" "selection-color: %2;" ) .arg(textHighlightColor.get(darkMode).name(QColor::HexArgb)) .arg(textColor.get(darkMode).name(QColor::HexArgb)); } return style_sheet; } ThemeManager::ThemeManager(QObject* parent) : QObject(parent) { _themes.push_back({"Default", ThemeColor(Qt::black, Qt::white), // text ThemeColor(Qt::darkGreen, QColor(0xbb86fc)), //keywords ThemeColor(Qt::blue, QColor(0x13C4F5)), // function ThemeColor(Qt::darkRed, QColor(0xF29F05)), // string ThemeColor(Qt::red, QColor(0x616161)), //comment ThemeColor(Qt::white,QColor(0x222222)), //background ThemeColor(QColor(0xF0F0F0),QColor(0x292929)), // line highlight ThemeColor(QColor(0xE7E3FF),QColor(0x004161)), // text highlight ThemeColor(Qt::red), // error color ThemeColor(QColor(0xd1ab13), Qt::yellow), // warning color true }); _themes.push_back({"Blueberry", ThemeColor(QColor(0x0B1224), QColor(0x8EBBED)), // text ThemeColor(QColor(0x3354A7), QColor(0x1172A6)), //keywords ThemeColor(QColor(0x9147A6), QColor(0xD676CC)), // function ThemeColor(QColor(0x132F6B), QColor(0xA192E8)), // string ThemeColor(QColor(0x83B9F5), QColor(0x225773)), //comment ThemeColor(QColor(0xE9ECFF),QColor(0x001926)), //background ThemeColor(QColor(0xE7E3FF),QColor(0x001D2B)), // line highlight ThemeColor(QColor(0xBEBDFC),QColor(0x005580)), // text highlight ThemeColor(Qt::red), // error color ThemeColor(Qt::yellow) // warning color }); _themes.push_back({"Mango", ThemeColor(QColor(0x375327), QColor(0xF2DC6D)), // text ThemeColor(QColor(0xF27B35), QColor(0xF2385A)), //keywords ThemeColor(QColor(0xF2385A), QColor(0xF29F05)), // function ThemeColor(QColor(0x51C0BF), QColor(0xF48985)), // string ThemeColor(QColor(0x6A8F2F), QColor(0xF4503F)), //comment ThemeColor(QColor(0xEFDC9B),QColor(0x030500)), //background ThemeColor(QColor(0xD9C78D),QColor(0x070D00)), // line highlight ThemeColor(QColor(0xCFB359),QColor(0x6B6033)), // text highlight ThemeColor(Qt::red, QColor(0x51C0BF)), // error color ThemeColor(Qt::yellow) // warning color }); } ================================================ FILE: MiniZincIDE/theme.h ================================================ #ifndef THEME_H #define THEME_H #include #include #include struct ThemeColor { QColor light; QColor dark; ThemeColor(QColor lightAndDark) { light = lightAndDark; dark = lightAndDark; } ThemeColor(QColor l, QColor d) { light = l; dark = d; } QColor get(bool darkMode) const { return darkMode ? dark : light; } }; struct Theme { QString name; ThemeColor textColor, // Color of normal text keywordColor, // Color of MiniZinc keywords such as "in" "where" "constraints" functionColor, // Color of words that are followed by a function call, such as "forall()" stringColor, // Color of strings commentColor, // Color of comments % backgroundColor, // Color of background errorColor, //Color of error messages warningColor, // Color of warning messages lineHighlightColor, // Color of highlighted line foregroundActiveColor, // color of active line number foregroundInactiveColor, // color of inactive line number textHighlightColor, // color of highlighted text bracketsMatchColor, // Color of matching brackets bracketsNoMatchColor, //Colors of dangling brackets lineNumberbackground // Color of line numbers background ; bool isSystemTheme = false; Theme(const QString& _name, ThemeColor _textColor, ThemeColor _keywordColor, ThemeColor _functionColor, ThemeColor _stringColor, ThemeColor _commentColor, ThemeColor _backgroundColor, ThemeColor _lineHighlightColor, ThemeColor _textHighlightColor, ThemeColor _errorColor, ThemeColor _warningColor, bool systemTheme = false) : name(_name), textColor(_textColor), keywordColor(_keywordColor), functionColor(_functionColor), stringColor( _stringColor), commentColor(_commentColor), backgroundColor(_backgroundColor), errorColor(_errorColor), warningColor(_warningColor), lineHighlightColor(_lineHighlightColor), foregroundActiveColor(textColor), foregroundInactiveColor(textColor.light.lighter(), textColor.dark.darker()), textHighlightColor(_textHighlightColor), bracketsMatchColor(keywordColor.light.lighter(200), keywordColor.dark.darker(150)), bracketsNoMatchColor(errorColor.light, errorColor.dark), lineNumberbackground(backgroundColor.light.darker(110), backgroundColor.dark.lighter(150)), isSystemTheme(systemTheme) {} QString styleSheet(bool darkMode) const; }; class ThemeManager : public QObject { Q_OBJECT public: explicit ThemeManager(QObject *parent = nullptr); const std::vector& themes() const { return _themes; } const Theme& current() const { return _themes[_current]; } void current(size_t index) { if (index < 0 || index >= _themes.size()) { _current = 0; } else { _current = index; } } const Theme& get(size_t index) const { if (index < 0 || index >= _themes.size()) { return _themes[0]; } return _themes[index]; } private: size_t _current = 0; std::vector _themes; }; #endif // THEME_H ================================================ FILE: MiniZincIDE.pro ================================================ TEMPLATE = subdirs SUBDIRS = \ MiniZincIDE \ tests libminizinc { SUBDIRS += libminizinc MiniZincIDE.depends = libminizinc } ================================================ FILE: README.md ================================================

Logo

MiniZinc IDE

Integrated development environment for the high-level constraint modelling language MiniZinc.

MiniZinc Compiler · Documentation · Report Bug

The MiniZinc IDE

## Getting started Packages for Linux, macOS and Windows can be found in the [releases](https://github.com/MiniZinc/MiniZincIDE/releases) or from [the MiniZinc website](http://www.minizinc.org/software.html). These packages contain the MiniZinc IDE, the MiniZinc compiler toolchain, as well as several solvers. For more detailed installation instructions, see the [documentation](https://www.minizinc.org/doc-latest/en/installation.html). ## Building from source The MiniZinc IDE is a [Qt](https://www.qt.io/) project and requires: - A recent C++ compiler - [Qt](https://www.qt.io/) (we target the latest LTS Qt version) - We also require the [Qt WebSockets](https://doc.qt.io/qt-6/qtwebsockets-index.html) module - [Make](https://www.gnu.org/software/make/) Ensure you clone the repository including submodules: ```sh git clone --recurse-submodules https://github.com/MiniZinc/MiniZincIDE cd MiniZincIDE ``` Then either build open the project (`MiniZincIDE.pro`) in Qt Creator and build, or from the command line: ```sh mkdir build cd build qmake -makefile ../MiniZincIDE/MiniZincIDE.pro make -j4 ``` See the [MiniZinc compiler project](https://github.com/MiniZinc/libminizinc) for instructions on how to build the compiler toolchain. ### Running tests The IDE has a test suite which can be compiled and run with: ```sh mkdir test cd test qmake -makefile ../tests/tests.pro make -j4 make check ``` ================================================ FILE: TODO.txt ================================================ Features that would be nice to have: * Show types * Integrated global constraint library browser * Save all data files before running * Auto indent * Column selection Done * Focus * Make dzn selection in Configuration tab work * Clear output window * Font size * Tabs-to-spaces (without messing up the undo buffer) * About dialog * Click on error location to jump to position in editor * Configuration dialog for adding other solvers * Global solver configurations stored somewhere system wide * Find/replace * Font selection * Go to line * Configure paths * Time outs * Mac application icon * Indent/outdent selection * Report solve time * Basic help * Windows application icon * Multiple windows ("projects") * Saving/loading projects (i.e. the contents of the Configuration tab + all open files) * Do not load really large files (show button instead to load on demand) * Save before running * Remove LGPL find/replace dialog * Correct copyright headers * Handle duplicate open files * Save all * Change font also for output * Correct handling of open constraint graph windows * Packaging * Run fzn files * Use mzn2fzn and implement the solutions processing internally * Comment/uncomment selection * Detach solver (to be able to run Gecode w/ Gist) * Dialog for missing model parameters * Detect when files change on disk (auto reload + dialog if changed) * Colour selection ================================================ FILE: cp-profiler/README.md ================================================ # CP-Profiler CP-Profiler provides search tree visualisations for executions of constraint programming solvers. ## Table of Contents - [Building](#building) - [Usage](#usage) ### Building Dependencies: - Qt5.x ``` git submodule update --init mkdir build && cd build qmake .. && make ``` ### Usage 1. Starting CP-Profiler `cp-profiler.app/Contents/MacOS/cp-profiler` (Mac) This starts a local TCP server listening on one of the available ports (6565 by default). 2. Executing a supported solver The solver must implement the profiling protocol (TODO). Integration libraries are available if you wish to extend your solver to work with CP-Profiler. ### The Protocol (high level) The following describes the protocol that a solver must implement to communicate with the profiler. The protocol distinguishes between the following types of messages: **`Start`**, **`Restart`**, **`Done`**, and **`Node`**. The **`Start`** message is sent at the beginning of the execution (**TODO**: multithreaded search). The message has two optional (?) parameters: - `Name`: execution's descriptor to be presented to the user (e.g. the model's name) - `Execution ID`: globally unique identifier used to distinguish between different executions. **TODO**: any other parameters? The **`Restart`** message is sent whenever a solver performs a restart in case of a restart-based search. The **`Done`** message is sent to the profiler at the end of the execution to indicate that no further nodes should be expected. The **`Node`** message is sent whenever a new node is explored by the solver and contains information necessary for reconstructing the search tree. The required parameters are: - `Node ID`: unique node identifier in the execution. - `Parent ID`: the identifier (`Node ID`) of the parent node. A root node can have an identifier of `-1`. (**TODO**: needs checking) - `Alternative`: the node's position relative to its siblings; for the left-most child it is `0`, for the second left-most it is `1` etc. - `Number of Children`: the number of children nodes. If not known, can be set to `0` and the node will be extended with extra children later on if necessary. It is, however, advisable to specify the number of children the profiler should expect (for example, the yet to arrive nodes can be visualised to give a better idea about the search). - `Status`: allows to distinguish between different types of nodes. Supported values are: - *BRANCH*: internal node in the tree; - *SOLUTION*: leaf node representing a solution; - *FAILURE*: leaf node representing a failure; - *SKIPPED*: leaf node representing unexplored search space due to backjumping. **Example**. The following sequence of nodes (excluding the `Start` and `Done` messages) produces the simple tree below: | `Label` | `Node ID` | `Parent ID` | `Alternative` | `Number of Children` | `Status` | |:--------:|:---------:|:-----------:|:-------------:|:--------------------:|:--------:| | Root | 0 | -1 | -1 | 2 | BRANCH | | Failure | 1 | 0 | 0 | 0 | FAILED | | Solution | 2 | 0 | 1 | 0 | SOLVED | ![A simple search tree](todo) ### The Protocol (low level) Each message starts with a four-byte integer encoding the size of the remainder of the message in bytes. This is followed by a single byte encoding the type of the message. The corresponding values are: **`Node`**: `0`, **`Done`**: `1`, **`Start`**: `2`, **`Restart`**: `3`. #### `Node` message In case the message is of the type **`Node`**, the following fields are added in order: `id`, `pid`, `alt`, `children` and `status`. Node identifiers `id` and `pid` are represented using three four-byte integers: first identifies the identifier of the node within a thread, the second — the identifier of the restart (in a restart-based search), and the third — the identifier of the thread. The `alt` and `children` fields are represented by a single four byte integer each. The `status` field is represented by a single byte; its possible values are: *SOLVED*: 0, *FAILED*: 1, *BRANCH*: 2, *SKIPPED*: 3. All multi-byte integer values are encoded using the *two's compliment* notation in the *big-endian order*. Additionally, each node message can contain the following optional fields: - `label`: branching decision (or any arbitrary string to be drawn together with the node); - `nogood`: string representation of a newly generated nogood in a learning solver; - `info`: arbitrary information about the node (*TODO*). Field identifiers and their sizes in bytes: | field name | field id | size (bytes) | |:----------:|:--------:|:------------:| | `id` | n/a | 12 | | `pid` | n/a | 12 | | `alt` | n/a | 4 | | `children` | n/a | 4 | | `status` | n/a | 1 | | `label` | 0 | any | | `nogood` | 1 | any | | `info` | 2 | any | **Example**. The following is a possible correspondence between a solver and the profiler that generates the simple tree above.The order in which different fields arrive is shown from top to bottom (rows are numbered for convenience). *Message 1:* | Row | Bytes | Interpretation | |-----|------------------------------------------------------------------------------------|-------------------------------| | 1 | `00 00 00 21` | message size (33) | | 2 | `02` | message type (*START*) | | 3 | `02` | field (*info*) | | 4 | `00 00 00 1B` | string size (27) | | 5 | `7b 22 6e 61 6d 65 22 3a 20 22 6d 69 6e 69 6d 61 6c 20 65 78 61 6d 70 6c 65 22 7d` | '{"name": "minimal example"}' | *Message 2:* | Row | Bytes | Interpretation | |-----|---------------|-------------------------| | 6 | `00 00 00 2B` | message size (43) | | 7 | `00` | message type (**NODE**) | | 8 | `00 00 00 00` | node id (0) | | 9 | `FF FF FF FF` | node restart id (-1) | | 10 | `FF FF FF FF` | node thread id (-1) | | 11 | `FF FF FF FF` | parent id (-1) | | 12 | `FF FF FF FF` | parent restart id (-1) | | 13 | `FF FF FF FF` | parent thread id (-1) | | 14 | `FF FF FF FF` | alternative (-1) | | 15 | `00 00 00 02` | children (2) | | 16 | `02` | status (*BRANCH*) | | 17 | `00` | field (label) | | 18 | `00 00 00 04` | string size (4) | | 19 | `52 6f 6f 74` | 'Root' | *Message 3:* | Row | Bytes | Interpretation | |-----|---------------|-------------------------| | 20 | `00 00 00 01` | message size (1) | | 21 | `01` | message type (**DONE**) | ### Tree Visualisations #### Traditional Tree Visualisation When a new execution is connected to the profiler it will be added to the list of executions displayed at the top of the main window. For example, in the image below execution *golomb6a.fzn* is shown to be added to the profiler. To display the execution, select its name from the list and click the *Show Tree* button. Note that the solver can still be running the execution, in which case the profiler will draw the search tree in real time. ![Profiler Conductor](https://bitbucket.org/Msgmaxim/cp-profiler2/raw/d2aad1af0805a47f89459f771f70516f29886a09/docs/images/doc_conductor1.png) The image below shows an example of a traditional (node-link) visualisation of the search tree. Different types of nodes are shown differently: branch (internal) nodes are shown as blue circles; nodes representing failures are shown as red squares; solution nodes are shown as green diamonds. Note that the root of the tree is shown in gold colour indicating the currently selected node. Arrow keys on the keyboard allow the user to navigate the tree by changing which node is selected. `Down` navigates to the first child of the current node, `Shift+Down` — to its last child, `Up` — to its the parent, `Left` — to its next sibling on the left, `Right` — to its next sibling on the right. Additionally, pressing `R` will navigate to the root of the tree. The same actions are available under the **`Navigation`** menu. ![Traditional Visualisation Interface](https://bytebucket.org/Msgmaxim/cp-profiler2/raw/d2aad1af0805a47f89459f771f70516f29886a09/docs/images/doc_traditional_interface.png) If a subtree contains no solutions, it can be collapsed into a special single node displayed as a large red triangle. By default, the tree will be collapse failed subtrees automatically during its construction as a way to deal with large trees. The image below shows the same search tree as above, but with all failed subtrees collapsed. ![Collapsed Failed Subtrees](https://bitbucket.org/Msgmaxim/cp-profiler2/raw/76bb21242ad5427b10b84f7c4cb60f9b557490a0/docs/images/doc_traditional_collapsed.png) This view of the tree allows the user to show additional information for every node — its label, which usually represents the branching decision made by the solver to move from the parent node to its child. Pressing `L` on the keyboard will display labels for all descendants of the current node. `Shift+L` will display labels on the path to the current node. For example, the visualisation above shows branching decisions on the path from the first solution (shown as the new current node) to the root of the tree. Status bar at the bottom of the window displays node statistics: the depth of the tree and the counts of different types of nodes. The scroll bar on the right lets the user to zoom in/out on the visualisation; #### Similar Subtree Analysis This analysis allows users to find similarities within a single search tree. It can be initiated by selecting **`Similar Subtrees`** from the menu **`Analyses`** (shortcut: `Shift+S`). The image below shows the result of running the analysis on the search tree above. Horizontal bars on the left lists all similarities (patterns) found in the tree. Here, the lengths of the bars indicate are configured to indicate how many subtrees belong to a particular pattern (*count*). Additionally the bars are sorted so that the patterns with subtrees of larger *size* appear at the top. Another property of a pattern is its *height*, which indicates the height/depth of subtrees that the pattern represent. Note that the second from the top pattern is currently selected (shown with orange outline). The view on the right shows a "preview" (traditional visualisation) of one of the subtrees representing the selected pattern. The two rows below the show the result of computing the difference in labels on the path from the root to two of the subtrees representing the pattern (in this case it is the first two subtrees encountered when the tree is traversed in the depth-first-search order). ![Similar Subtrees Summary](https://bitbucket.org/Msgmaxim/cp-profiler2/raw/dec396e2537294be8cdf18b9594441ac710e937b/docs/images/doc_ss_analysis_hist.png) Changing the configuration menu at the bottom of the window, the user can filter the list of patterns based on their *count* and *height* values. They way the length of horizontal bars is determined and the sorting criteria can also be specified there. Whenever a pattern on the left hand side is selected, the corresponding subtrees will be highlighted on the traditional visualisation by drawing their . Additionally, if the option *Hide not selected* is selected (top of the window), the subtrees of t ![Similar Subtrees Highlighted](https://bitbucket.org/Msgmaxim/cp-profiler2/raw/dec396e2537294be8cdf18b9594441ac710e937b/docs/images/doc_ss_analysis.png) **Elimination of subsumed patterns** A pattern `P` is said to be subsumed by one or more patterns if subtrees of those patterns contain all of the subtrees of `P` as descendants. **Applying filters.** Should filtered out patterns be allowed to subsume? ================================================ FILE: cp-profiler/cp-profiler.pri ================================================ SOURCES += \ $$PWD/src/cpprofiler/core.cpp \ $$PWD/src/cpprofiler/command_line_parser.cpp \ $$PWD/src/cpprofiler/name_map.cpp \ $$PWD/src/cpprofiler/tcp_server.cpp \ $$PWD/src/cpprofiler/receiver_thread.cpp \ $$PWD/src/cpprofiler/receiver_worker.cpp \ $$PWD/src/cpprofiler/conductor.cpp \ $$PWD/src/cpprofiler/execution.cpp \ $$PWD/src/cpprofiler/user_data.cpp \ $$PWD/src/cpprofiler/tree_builder.cpp \ $$PWD/src/cpprofiler/execution_list.cpp \ $$PWD/src/cpprofiler/execution_window.cpp \ $$PWD/src/cpprofiler/utils/utils.cpp \ $$PWD/src/cpprofiler/utils/string_utils.cpp \ $$PWD/src/cpprofiler/utils/path_utils.cpp \ $$PWD/src/cpprofiler/utils/tree_utils.cpp \ $$PWD/src/cpprofiler/utils/perf_helper.cpp \ $$PWD/src/cpprofiler/utils/array.cpp \ $$PWD/src/cpprofiler/utils/std_ext.cpp \ $$PWD/src/cpprofiler/utils/maybe_caller.cpp \ $$PWD/src/cpprofiler/tree/node.cpp \ $$PWD/src/cpprofiler/tree/structure.cpp \ $$PWD/src/cpprofiler/tree/layout.cpp \ $$PWD/src/cpprofiler/tree/layout_computer.cpp \ $$PWD/src/cpprofiler/tree/shape.cpp \ $$PWD/src/cpprofiler/tree/node_tree.cpp \ $$PWD/src/cpprofiler/tree/node_id.cpp \ $$PWD/src/cpprofiler/tree/node_info.cpp \ $$PWD/src/cpprofiler/tree/visual_flags.cpp \ $$PWD/src/cpprofiler/tree/traditional_view.cpp \ $$PWD/src/cpprofiler/pixel_views/pt_canvas.cpp \ $$PWD/src/cpprofiler/pixel_views/icicle_canvas.cpp \ $$PWD/src/cpprofiler/pixel_views/pixel_image.cpp \ $$PWD/src/cpprofiler/pixel_views/pixel_widget.cpp \ $$PWD/src/cpprofiler/tree/tree_scroll_area.cpp \ $$PWD/src/cpprofiler/tree/cursors/node_cursor.cpp \ $$PWD/src/cpprofiler/tree/cursors/drawing_cursor.cpp \ $$PWD/src/cpprofiler/tree/cursors/layout_cursor.cpp \ $$PWD/src/cpprofiler/tree/cursors/hide_failed_cursor.cpp \ $$PWD/src/cpprofiler/tree/cursors/hide_not_highlighted_cursor.cpp \ $$PWD/src/cpprofiler/tree/cursors/nodevisitor.hpp \ $$PWD/src/cpprofiler/analysis/similar_subtree_analysis.cpp \ $$PWD/src/cpprofiler/analysis/similar_subtree_window.cpp \ $$PWD/src/cpprofiler/analysis/path_comp.cpp \ $$PWD/src/cpprofiler/analysis/merge_window.cpp \ $$PWD/src/cpprofiler/analysis/merging/pentagon_rect.cpp \ $$PWD/src/cpprofiler/analysis/tree_merger.cpp \ $$PWD/src/cpprofiler/analysis/histogram_scene.cpp \ $$PWD/src/cpprofiler/analysis/pattern_rect.cpp \ $$PWD/src/cpprofiler/tree/node_drawing.cpp \ $$PWD/src/cpprofiler/db_handler.cpp \ $$PWD/src/cpprofiler/solver_data.cpp \ $$PWD/src/cpprofiler/nogood_dialog.cpp \ HEADERS += \ $$PWD/src/cpprofiler/config.hh \ $$PWD/src/cpprofiler/options.hh \ $$PWD/src/cpprofiler/command_line_parser.hh \ $$PWD/src/cpprofiler/name_map.hh \ $$PWD/src/cpprofiler/settings.hh \ $$PWD/src/cpprofiler/conductor.hh \ $$PWD/src/cpprofiler/tcp_server.hh \ $$PWD/src/cpprofiler/receiver_thread.hh \ $$PWD/src/cpprofiler/receiver_worker.hh \ $$PWD/src/cpprofiler/execution.hh \ $$PWD/src/cpprofiler/user_data.hh \ $$PWD/src/cpprofiler/tree_builder.hh \ $$PWD/src/cpprofiler/execution_list.hh \ $$PWD/src/cpprofiler/execution_window.hh \ $$PWD/src/cpprofiler/utils/utils.hh \ $$PWD/src/cpprofiler/utils/string_utils.hh \ $$PWD/src/cpprofiler/utils/path_utils.hh \ $$PWD/src/cpprofiler/utils/tree_utils.hh \ $$PWD/src/cpprofiler/utils/perf_helper.hh \ $$PWD/src/cpprofiler/utils/array.hh \ $$PWD/src/cpprofiler/utils/debug.hh \ $$PWD/src/cpprofiler/utils/std_ext.hh \ $$PWD/src/cpprofiler/utils/maybe_caller.hh \ $$PWD/src/cpprofiler/tree/node.hh \ $$PWD/src/cpprofiler/tree/structure.hh \ $$PWD/src/cpprofiler/tree/layout.hh \ $$PWD/src/cpprofiler/tree/layout_computer.hh \ $$PWD/src/cpprofiler/tree/shape.hh \ $$PWD/src/cpprofiler/tree/node_tree.hh \ $$PWD/src/cpprofiler/tree/node_id.hh \ $$PWD/src/cpprofiler/tree/node_info.hh \ $$PWD/src/cpprofiler/tree/node_stats.hh \ $$PWD/src/cpprofiler/tree/visual_flags.hh \ $$PWD/src/cpprofiler/tree/traditional_view.hh \ $$PWD/src/cpprofiler/pixel_views/pt_canvas.hh \ $$PWD/src/cpprofiler/pixel_views/icicle_canvas.hh \ $$PWD/src/cpprofiler/pixel_views/pixel_image.hh \ $$PWD/src/cpprofiler/pixel_views/pixel_widget.hh \ $$PWD/src/cpprofiler/tree/tree_scroll_area.hh \ $$PWD/src/cpprofiler/tree/subtree_view.hh \ $$PWD/src/cpprofiler/tree/cursors/node_cursor.hh \ $$PWD/src/cpprofiler/tree/cursors/drawing_cursor.hh \ $$PWD/src/cpprofiler/tree/cursors/layout_cursor.hh \ $$PWD/src/cpprofiler/tree/cursors/hide_failed_cursor.hh \ $$PWD/src/cpprofiler/tree/cursors/hide_not_highlighted_cursor.hh \ $$PWD/src/cpprofiler/tree/cursors/nodevisitor.hh \ $$PWD/src/cpprofiler/core.hh \ $$PWD/src/cpprofiler/solver_id.hh \ $$PWD/src/cpprofiler/utils/debug_mutex.hh \ $$PWD/src/cpprofiler/analysis/similar_subtree_analysis.hh \ $$PWD/src/cpprofiler/analysis/similar_subtree_window.hh \ $$PWD/src/cpprofiler/analysis/merge_window.hh \ $$PWD/src/cpprofiler/analysis/pentagon_counter.hpp \ $$PWD/src/cpprofiler/analysis/tree_merger.hh \ $$PWD/src/cpprofiler/analysis/subtree_pattern.hh \ $$PWD/src/cpprofiler/analysis/path_comp.hh \ $$PWD/src/cpprofiler/analysis/histogram_scene.hh \ $$PWD/src/cpprofiler/analysis/pattern_rect.hh \ $$PWD/src/cpprofiler/analysis/merging/pentagon_list_widget.hh \ $$PWD/src/cpprofiler/analysis/merging/merge_result.hh \ $$PWD/src/cpprofiler/analysis/merging/pentagon_rect.hh \ $$PWD/src/cpprofiler/tree/node_widget.hh \ $$PWD/src/cpprofiler/tree/node_drawing.hh \ $$PWD/src/cpprofiler/db_handler.hh \ $$PWD/src/cpprofiler/solver_data.hh \ $$PWD/src/cpprofiler/nogood_dialog.hh \ $$PWD/src/cpprofiler/analysis/nogood_analysis_dialog.hh \ $$PWD/src/cpprofiler/message_wrapper.hh \ SOURCES += \ $$PWD/src/cpprofiler/tests/tree_test.cpp \ $$PWD/src/cpprofiler/tests/execution_test.cpp \ HEADERS += \ $$PWD/src/cpprofiler/tests/tree_test.hh \ $$PWD/src/cpprofiler/tests/execution_test.hh \ ================================================ FILE: cp-profiler/cp-profiler.pro ================================================ TEMPLATE = app TARGET = cp-profiler QT += widgets network sql CONFIG += c++11 include(cp-profiler.pri) SOURCES += $$PWD/src/main_cpprofiler.cpp ================================================ FILE: cp-profiler/src/cpp-integration/README.md ================================================ #### 1. Create a connector instance ```c++ unsigned int port = 6565; ``` ```c++ Connector c(port); ``` #### 2. Establish a connection and start a new search tree ```c++ /// Establishes a socket connection using the port specified above c.connect(); /// Tells the profiler to start a new tree c.restart("example"); /// Also used in case of a restart with restart id specified c.restart("example", 1); ``` #### 3. Send data every time the solver branches/fails/finds a solution ```c++ /// Create a node on a stack with mandatory fields Node node = c.createNode(node_id, parent_id, alt, kids, status); ``` ```c++ // Specify optional fields (whichever available) node.set_label("b"); ``` ```c++ // Send the node c.sendNode(node); ``` Or all in one line: ```c++ c.createNode(node_id, parent_id, alt, kids, status).set_label("b").send(); ``` The parameters are: field | type | description ------ | ---- | ----------- node_id | int | current node's identifier parent_id | int | identifier of node's parent alt | int | which of its siblings the node is (0 for the left-most) kids | int | number of children status | Profiling::NodeStatus | determines the node's type (solution, failure, branching etc) label | std::string | some text-based information to go along with the node (ie branching decision #### 4. Finish the tree and release the socket ```c++ c.done(); c.disconnect(); ``` ================================================ FILE: cp-profiler/src/cpp-integration/connector.hpp ================================================ #ifndef CONNECTOR #define CONNECTOR #include "message.hpp" #include #include #include #include #ifdef WIN32 #include #include #pragma comment(lib, "Ws2_32.lib") #pragma comment(lib, "Mswsock.lib") #pragma comment(lib, "AdvApi32.lib") #include typedef SSIZE_T ssize_t; #else #include #include #endif namespace cpprofiler { template class Option { T value_; bool present{false}; public: bool valid() const { return present; } void set(const T& t) { present = true; value_ = t; } void unset() { present = false; } const T& value() const { assert(present); return value_; } T& value() { assert(present); return value_; } }; class Connector; class Node; static void sendNode(Connector& c, Node& node); class Node { Connector& _c; NodeUID node_; NodeUID parent_; int alt_; int kids_; NodeStatus status_; Option label_; Option nogood_; Option info_; public: Node(NodeUID node, NodeUID parent, int alt, int kids, NodeStatus status, Connector& c) : _c(c), node_{node}, parent_{parent}, alt_(alt), kids_(kids), status_(status) {} Node& set_node_thread_id(int tid) { node_.tid = tid; return *this; } const Option& label() const { return label_; } Node& set_label(const std::string& label) { label_.set(label); return *this; } const Option& nogood() const { return nogood_; } Node& set_nogood(const std::string& nogood) { nogood_.set(nogood); return *this; } const Option& info() const { return info_; } Node& set_info(const std::string& info) { info_.set(info); return *this; } int alt() const { return alt_; } int kids() const { return kids_; } NodeStatus status() const { return status_; } NodeUID nodeUID() const { return node_; } NodeUID parentUID() const { return parent_; } int node_id() const { return node_.nid; } int parent_id() const { return parent_.nid; } int node_thread_id() const { return node_.tid; } int node_restart_id() const { return node_.rid; } int parent_thread_id() const { return parent_.tid; } int parent_restart_id() const { return parent_.rid; } void send() { sendNode(_c, *this); } }; // From http://beej.us/guide/bgnet/output/html/multipage/advanced.html#sendall static int sendall(int s, const char* buf, int* len) { int total = 0; // how many bytes we've sent int bytesleft = *len; // how many we have left to send ssize_t n; while (total < *len) { n = send(s, buf + total, static_cast(bytesleft), 0); if (n == -1) { break; } total += n; bytesleft -= n; } *len = total; // return number actually sent here return n == -1 ? -1 : 0; // return -1 on failure, 0 on success } class Connector { private: MessageMarshalling marshalling; const unsigned int port; int sockfd; bool _connected; void sendOverSocket() { if (!_connected) return; std::vector buf = marshalling.serialize(); sendRawMsg(buf); } public: void sendRawMsg(const std::vector& buf) { uint32_t bufSize = static_cast(buf.size()); int bufSizeLen = sizeof(uint32_t); sendall(sockfd, reinterpret_cast(&bufSize), &bufSizeLen); int bufSizeInt = static_cast(bufSize); sendall(sockfd, reinterpret_cast(buf.data()), &bufSizeInt); } Connector(unsigned int port) : port(port), _connected(false) {} bool connected() { return _connected; } /// connect to a socket via port specified in the construction (6565 by /// default) void connect() { struct addrinfo hints, *servinfo, *p; int rv; #ifdef WIN32 // Initialise Winsock. WSADATA wsaData; int startupResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (startupResult != 0) { printf("WSAStartup failed with error: %d\n", startupResult); } #endif memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ((rv = getaddrinfo("localhost", std::to_string(port).c_str(), &hints, &servinfo)) != 0) { std::cerr << "getaddrinfo: " << gai_strerror(rv) << "\n"; goto giveup; } // loop through all the results and connect to the first we can for (p = servinfo; p != NULL; p = p->ai_next) { if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) { // errno is set here, but we don't examine it. continue; } if (::connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) { #ifdef WIN32 closesocket(sockfd); #else close(sockfd); #endif // errno is set here, but we don't examine it. continue; } break; } // Connection failed; give up. if (p == NULL) { goto giveup; } freeaddrinfo(servinfo); // all done with this structure _connected = true; return; giveup: _connected = false; return; } // sends START_SENDING message to the Profiler with a model name void start(const std::string& file_path = "", int execution_id = -1, bool has_restarts = false) { /// extract fzn file name std::string base_name(file_path); { size_t pos = base_name.find_last_of('/'); if (pos != static_cast(-1)) { base_name = base_name.substr(pos + 1, base_name.length() - pos - 1); } } std::string info{""}; { std::stringstream ss; ss << "{"; ss << "\"has_restarts\": " << (has_restarts ? "true" : "false") << "\n"; ss << ",\"name\": " << "\"" << base_name << "\"" << "\n"; if (execution_id != -1) { ss << ",\"execution_id\": " << execution_id; } ss << "}"; info = ss.str(); } marshalling.makeStart(info); sendOverSocket(); } void restart(int restart_id = -1) { std::string info{""}; { std::stringstream ss; ss << "{"; ss << "\"restart_id\": " << restart_id << "\n"; ss << "}"; info = ss.str(); } marshalling.makeRestart(info); sendOverSocket(); } void done() { marshalling.makeDone(); sendOverSocket(); } /// disconnect from a socket void disconnect() { #ifdef WIN32 closesocket(sockfd); #else close(sockfd); #endif } void sendNode(const Node& node) { if (!_connected) return; auto& msg = marshalling.makeNode(node.nodeUID(), node.parentUID(), node.alt(), node.kids(), node.status()); if (node.label().valid()) msg.set_label(node.label().value()); if (node.nogood().valid()) msg.set_nogood(node.nogood().value()); if (node.info().valid()) msg.set_info(node.info().value()); sendOverSocket(); } Node createNode(NodeUID node, NodeUID parent, int alt, int kids, NodeStatus status) { return Node(node, parent, alt, kids, status, *this); } }; void sendNode(Connector& c, Node& node) { c.sendNode(node); } } #endif ================================================ FILE: cp-profiler/src/cpp-integration/message.hpp ================================================ #ifndef MESSAGE_HH #define MESSAGE_HH #include #include #include #include namespace cpprofiler { static const int32_t PROFILER_PROTOCOL_VERSION = 3; enum NodeStatus { SOLVED = 0, ///< Node representing a solution FAILED = 1, ///< Node representing failure BRANCH = 2, ///< Node representing a branch SKIPPED = 3, ///< Skipped by backjumping }; enum class MsgType { NODE = 0, DONE = 1, START = 2, RESTART = 3, }; // Unique identifier for a node struct NodeUID { // Node number int32_t nid; // Restart id int32_t rid; // Thread id int32_t tid; }; class Message { MsgType _type; NodeUID _node; NodeUID _parent; int32_t _alt; int32_t _kids; NodeStatus _status; bool _have_label{false}; std::string _label; bool _have_nogood{false}; std::string _nogood; bool _have_info{false}; std::string _info; bool _have_version{false}; int32_t _version; // PROFILER_PROTOCOL_VERSION; public: bool isNode(void) const { return _type == MsgType::NODE; } bool isDone(void) const { return _type == MsgType::DONE; } bool isStart(void) const { return _type == MsgType::START; } bool isRestart(void) const { return _type == MsgType::RESTART; } NodeUID nodeUID(void) const { return _node; } void set_nodeUID(const NodeUID& n) { _node = n; } NodeUID parentUID(void) const { return _parent; } void set_parentUID(const NodeUID& p) { _parent = p; } int32_t alt(void) const { return _alt; } void set_alt(int32_t alt) { _alt = alt; } int32_t kids(void) const { return _kids; } void set_kids(int32_t kids) { _kids = kids; } NodeStatus status(void) const { return _status; } void set_status(NodeStatus status) { _status = status; } void set_label(const std::string& label) { _have_label = true; _label = label; } void set_info(const std::string& info) { _have_info = true; _info = info; } void set_nogood(const std::string& nogood) { _have_nogood = true; _nogood = nogood; } void set_version(int32_t v) { _have_version = true; _version = v; } bool has_version(void) const { return _have_version; } int32_t version(void) const { return _version; } bool has_label(void) const { return _have_label; } const std::string& label() const { return _label; } bool has_nogood(void) const { return _have_nogood; } const std::string& nogood(void) const { return _nogood; } // generic optional fields bool has_info(void) const { return _have_info; } const std::string& info(void) const { return _info; } void set_type(MsgType type) { _type = type; } MsgType type(void) const { return _type; } void reset(void) { _have_label = false; _have_nogood = false; _have_info = false; _have_version = false; } }; class MessageMarshalling { private: /// Only optional fields are listed here, if node (no need for field id) enum Field { LABEL = 0, NOGOOD = 1, INFO = 2, VERSION = 3 }; Message msg; typedef char* iter; static void serializeType(std::vector& data, MsgType f) { data.push_back(static_cast(f)); } static void serializeField(std::vector& data, Field f) { data.push_back(static_cast(f)); } static void serialize(std::vector& data, int32_t i) { data.push_back(static_cast((i & 0xFF000000) >> 24)); data.push_back(static_cast((i & 0xFF0000) >> 16)); data.push_back(static_cast((i & 0xFF00) >> 8)); data.push_back(static_cast((i & 0xFF))); } static void serialize(std::vector& data, NodeStatus s) { data.push_back(static_cast(s)); } static void serialize(std::vector& data, const std::string& s) { serialize(data, static_cast(s.size())); for (char c : s) { data.push_back(c); } } static MsgType deserializeMsgType(iter& it) { auto m = static_cast(*it); ++it; return m; } static Field deserializeField(iter& it) { auto f = static_cast(*it); ++it; return f; } static int32_t deserializeInt(iter& it) { auto b1 = static_cast(reinterpret_cast(*it++)); auto b2 = static_cast(reinterpret_cast(*it++)); auto b3 = static_cast(reinterpret_cast(*it++)); auto b4 = static_cast(reinterpret_cast(*it++)); return static_cast(b1 << 24 | b2 << 16 | b3 << 8 | b4); } static NodeStatus deserializeStatus(iter& it) { auto f = static_cast(*it); ++it; return f; } static std::string deserializeString(iter& it) { std::string result; int32_t size = deserializeInt(it); result.reserve(static_cast(size)); for (int32_t i = 0; i < size; i++) { result += *it; ++it; } return result; } public: Message& makeNode(NodeUID node, NodeUID parent, int32_t alt, int32_t kids, NodeStatus status) { msg.reset(); msg.set_type(MsgType::NODE); msg.set_nodeUID(node); msg.set_parentUID(parent); msg.set_alt(alt); msg.set_kids(kids); msg.set_status(status); return msg; } void makeStart(const std::string& info) { msg.reset(); msg.set_type(MsgType::START); msg.set_version(PROFILER_PROTOCOL_VERSION); msg.set_info(info); /// info containts name, has_restarts, execution id } void makeRestart(const std::string& info) { msg.reset(); msg.set_type(MsgType::RESTART); msg.set_info(info); /// info contains restart_id (-1 default) } void makeDone(void) { msg.reset(); msg.set_type(MsgType::DONE); } const Message& get_msg(void) { return msg; } std::vector serialize(void) const { std::vector data; size_t dataSize = 1 + (msg.isNode() ? 4 * 8 + 1 : 0) + (msg.has_label() ? 1 + 4 + msg.label().size() : 0) + (msg.has_nogood() ? 1 + 4 + msg.nogood().size() : 0) + (msg.has_info() ? 1 + 4 + msg.info().size() : 0); data.reserve(dataSize); serializeType(data, msg.type()); if (msg.isNode()) { // serialize NodeId node auto n_uid = msg.nodeUID(); serialize(data, n_uid.nid); serialize(data, n_uid.rid); serialize(data, n_uid.tid); // serialize NodeId parent auto p_uid = msg.parentUID(); serialize(data, p_uid.nid); serialize(data, p_uid.rid); serialize(data, p_uid.tid); // Other Data serialize(data, msg.alt()); serialize(data, msg.kids()); serialize(data, msg.status()); } if(msg.has_version()) { serializeField(data, VERSION); serialize(data, msg.version()); } if (msg.has_label()) { serializeField(data, LABEL); serialize(data, msg.label()); } if (msg.has_nogood()) { serializeField(data, NOGOOD); serialize(data, msg.nogood()); } if (msg.has_info()) { serializeField(data, INFO); serialize(data, msg.info()); } return data; } void deserialize(char* data, size_t size) { char *end = data + size; msg.set_type(deserializeMsgType(data)); if (msg.isNode()) { int32_t nid = deserializeInt(data); int32_t rid = deserializeInt(data); int32_t tid = deserializeInt(data); msg.set_nodeUID({nid, rid, tid}); nid = deserializeInt(data); rid = deserializeInt(data); tid = deserializeInt(data); msg.set_parentUID({nid, rid, tid}); msg.set_alt(deserializeInt(data)); msg.set_kids(deserializeInt(data)); msg.set_status(deserializeStatus(data)); } msg.reset(); while (data != end) { MessageMarshalling::Field f = deserializeField(data); switch (f) { case VERSION: msg.set_version(deserializeInt(data)); break; case LABEL: msg.set_label(deserializeString(data)); break; case NOGOOD: msg.set_nogood(deserializeString(data)); break; case INFO: msg.set_info(deserializeString(data)); break; default: break; } } } }; } #endif // MESSAGE_HH ================================================ FILE: cp-profiler/src/cpprofiler/analysis/histogram_scene.cpp ================================================ #include "histogram_scene.hh" #include "pattern_rect.hh" #include "../utils/perf_helper.hh" namespace cpprofiler { namespace analysis { static constexpr int SHAPE_RECT_HEIGHT = 16; static constexpr int NUMBER_WIDTH = 50; static constexpr int COLUMN_WIDTH = NUMBER_WIDTH + 10; static constexpr int ROW_HEIGHT = SHAPE_RECT_HEIGHT + V_DISTANCE; enum class Align { CENTER, RIGHT }; QColor PatternRect::normal_outline{252, 209, 22}; QColor PatternRect::highlighted_outline{252, 148, 77}; static void addText(QGraphicsScene &scene, int col, int row, QGraphicsSimpleTextItem *text_item, Align alignment = Align::CENTER) { int item_width = text_item->boundingRect().width(); int item_height = text_item->boundingRect().height(); // center the item vertically at y int y_offset = item_height / 2; int x_offset = 0; switch (alignment) { case Align::CENTER: x_offset = (COLUMN_WIDTH - item_width) / 2; break; case Align::RIGHT: x_offset = COLUMN_WIDTH - item_width; break; } int x = col * COLUMN_WIDTH + x_offset; int y = row * ROW_HEIGHT + y_offset; text_item->setPos(x, y - y_offset); scene.addItem(text_item); } static std::shared_ptr addRect(HistogramScene &hist_scene, int row, int val) { int x = 0; int y = row * ROW_HEIGHT; int width = val * 40; auto item = std::make_shared(hist_scene, x, y, width, SHAPE_RECT_HEIGHT); item->addToScene(); return item; } static void addText(QGraphicsScene &scene, int col, int row, const char *text) { auto str = new QGraphicsSimpleTextItem{text}; addText(scene, col, row, str); } static void addText(QGraphicsScene &scene, int col, int row, int value) { auto int_text_item = new QGraphicsSimpleTextItem{QString::number(value)}; addText(scene, col, row, int_text_item, Align::RIGHT); } void HistogramScene::drawPatterns(PatternProp prop) { auto &scene = *scene_; addText(scene, 0, 0, "hight"); addText(scene, 1, 0, "count"); addText(scene, 2, 0, "size"); int row = 1; for (auto &&p : patterns_) { const auto nid = p->first(); const int count = p->count(); const int height = p->height(); /// number of nodes in the frist subtree represeting a pattern const auto size = p->size(); int val; switch (prop) { case PatternProp::HEIGHT: val = height; break; case PatternProp::COUNT: val = count; break; case PatternProp::SIZE: val = size; break; } auto item = addRect(*this, row, val); rects_.push_back(item); rect2pattern_.insert(std::make_pair(item, p)); addText(scene, 0, row, height); addText(scene, 1, row, count); addText(scene, 2, row, size); ++row; } } void HistogramScene::setPatterns(std::vector &&patterns) { patterns_.reserve(patterns.size()); for (auto &p : patterns) { patterns_.push_back(std::make_shared(std::move(p))); } } int HistogramScene::findPatternIdx(PatternRect *pattern) const { for (auto i = 0; i < rects_.size(); ++i) { if (pattern == rects_[i].get()) { return i; } } return -1; } PatternPtr HistogramScene::rectToPattern(PatternRectPtr prect) const { if (rect2pattern_.find(prect) != rect2pattern_.end()) { return rect2pattern_.at(prect); } return nullptr; } void HistogramScene::reset() { if (selected_rect_) { selected_rect_->setHighlighted(false); selected_rect_ = nullptr; selected_idx_ = -1; } rects_.clear(); rect2pattern_.clear(); scene_->clear(); patterns_.clear(); } void HistogramScene::changeSelectedPattern(int idx) { if (idx < 0 || idx >= rects_.size()) return; auto prect = rects_[idx]; /// Need to find pattern to get the list of nodes auto pattern = rectToPattern(prect); if (!pattern) return; emit pattern_selected(pattern->first()); emit should_be_highlighted(pattern->nodes()); { if (idx == selected_idx_) { selected_rect_->setHighlighted(false); selected_idx_ = -1; selected_rect_ = nullptr; } else { selected_idx_ = idx; /// unselect the old one if (selected_rect_) { selected_rect_->setHighlighted(false); } prect->setHighlighted(true); selected_idx_ = idx; selected_rect_ = prect.get(); } } } void HistogramScene::findAndSelect(PatternRect *prect) { auto idx = findPatternIdx(prect); changeSelectedPattern(idx); } /// TODO: this should change scroll bar value as well void HistogramScene::prevPattern() { if (rects_.size() == 0) return; auto new_idx = selected_idx_ - 1; if (new_idx < 0) return; changeSelectedPattern(new_idx); } void HistogramScene::nextPattern() { if (rects_.size() == 0) return; auto new_idx = selected_idx_ + 1; if (new_idx >= rects_.size()) return; changeSelectedPattern(new_idx); } } // namespace analysis } // namespace cpprofiler ================================================ FILE: cp-profiler/src/cpprofiler/analysis/histogram_scene.hh ================================================ #ifndef CPPROFILER_ANALYSES_HISTOGRAM_SCENE #define CPPROFILER_ANALYSES_HISTOGRAM_SCENE #include #include #include #include #include "../core.hh" #include "subtree_pattern.hh" namespace cpprofiler { namespace analysis { /// vertical distance between two shape rectangles static constexpr int V_DISTANCE = 2; class PatternRect; enum class PatternProp; using PatternRectPtr = std::shared_ptr; using PatternPtr = std::shared_ptr; class HistogramScene : public QObject { Q_OBJECT /// Scene used for drawing std::unique_ptr scene_; /// Selected pattern rectangle (nullptr if no selected) PatternRect *selected_rect_ = nullptr; /// Index of the selected pattern int selected_idx_ = -1; /// A list of visual elements for patterns (used for navigation); std::vector rects_; /// The result of the analysis in a form of a list of patterns std::vector patterns_; /// Mapping from a visual element to the pattern it represents std::unordered_map rect2pattern_; /// Find the possition of the rectangle representing `pattern` int findPatternIdx(PatternRect *pattern) const; /// Select `idx` unselecting the previous pattern void changeSelectedPattern(int idx); PatternPtr rectToPattern(PatternRectPtr prect) const; public: HistogramScene() { scene_.reset(new QGraphicsScene()); } /// Prepare the GUI for new patterns void reset(); /// Expose underlying scene QGraphicsScene *scene() { return scene_.get(); } /// Find the pattern's id and select it void findAndSelect(PatternRect *prect); /// Draw the patterns onto the scene void drawPatterns(PatternProp prop); /// Initialize patterns based on the analysis results void setPatterns(std::vector &&patterns); signals: void pattern_selected(NodeID); void should_be_highlighted(const std::vector &); public slots: void prevPattern(); void nextPattern(); }; } // namespace analysis } // namespace cpprofiler #endif ================================================ FILE: cp-profiler/src/cpprofiler/analysis/merge_window.cpp ================================================ #include "merge_window.hh" #include "../tree/traditional_view.hh" #include "merging/pentagon_list_widget.hh" #include "pentagon_counter.hpp" #include "../user_data.hh" #include "../solver_data.hh" #include "../execution.hh" #include "nogood_analysis_dialog.hh" #include #include #include #include #include #include #include #include namespace cpprofiler { namespace analysis { MergeWindow::MergeWindow(Execution &ex_l, Execution &ex_r, std::shared_ptr nt, std::shared_ptr res, QWidget* parent) : QMainWindow(parent), ex_l_(ex_l), ex_r_(ex_r), nt_(nt), merge_result_(res) { initOrigLocations(); user_data_.reset(new UserData); solver_data_.reset(new SolverData); view_.reset(new tree::TraditionalView(*nt_, *user_data_, *solver_data_)); view_->setScale(50); auto layout = new QGridLayout(); resize(500 + pent_config::VIEW_WIDTH, 700); pentagon_bar = new PentagonCounter(this); statusBar()->addPermanentWidget(pentagon_bar); pent_list = new PentagonListWidget(this, *merge_result_); connect(pent_list, &PentagonListWidget::pentagonClicked, view_.get(), &tree::TraditionalView::setCurrentNode); // connect(pent_list, &PentagonListWidget::pentagonClicked, view_.get(), &tree::TraditionalView::setAndCenterNode); auto sort_cb = new QCheckBox("sorted", this); sort_cb->setChecked(true); connect(sort_cb, &QCheckBox::stateChanged, pent_list, &PentagonListWidget::handleSortCB); layout->addWidget(pent_list, 2, 0, 1, 1, Qt::AlignLeft); layout->addWidget(sort_cb, 1, 0, 1, 1, Qt::AlignLeft); { auto widget = new QWidget(); setCentralWidget(widget); widget->setLayout(layout); layout->addWidget(view_->widget(), 1, 1, 2, 1); } auto menuBar = new QMenuBar(0); // Don't add the menu bar on Mac OS X #ifndef Q_WS_MAC setMenuBar(menuBar); #endif connect(nt_.get(), &tree::NodeTree::structureUpdated, view_.get(), &tree::TraditionalView::setLayoutOutdated); /// This indirection is necessary to immitate the behaviour of ExecutionWindow, /// which selects nodes in all views (traditional, pixel etc.) connect(view_.get(), &tree::TraditionalView::nodeSelected, view_.get(), &tree::TraditionalView::setCurrentNode); { auto widget = new QWidget(); auto button_layout = new QHBoxLayout(); button_layout->setContentsMargins(0, 0, 0, 0); widget->setLayout(button_layout); { auto nodeMenu = new QMenu("&Node"); auto centerNode = new QAction{"Center current node", this}; centerNode->setShortcut(QKeySequence("C")); nodeMenu->addAction(centerNode); connect(centerNode, &QAction::triggered, view_.get(), &tree::TraditionalView::centerCurrentNode); auto navRoot = new QAction{"Go to the root", this}; navRoot->setShortcut(QKeySequence("R")); nodeMenu->addAction(navRoot); connect(navRoot, &QAction::triggered, view_.get(), &tree::TraditionalView::navRoot); auto navDown = new QAction{"Go down the tree", this}; navDown->setShortcut(QKeySequence("Down")); nodeMenu->addAction(navDown); connect(navDown, &QAction::triggered, view_.get(), &tree::TraditionalView::navDown); auto navUp = new QAction{"Go up the tree", this}; navUp->setShortcut(QKeySequence("Up")); nodeMenu->addAction(navUp); connect(navUp, &QAction::triggered, view_.get(), &tree::TraditionalView::navUp); auto navLeft = new QAction{"Go left the tree", this}; navLeft->setShortcut(QKeySequence("Left")); nodeMenu->addAction(navLeft); connect(navLeft, &QAction::triggered, view_.get(), &tree::TraditionalView::navLeft); auto navRight = new QAction{"Go right the tree", this}; navRight->setShortcut(QKeySequence("Right")); nodeMenu->addAction(navRight); connect(navRight, &QAction::triggered, view_.get(), &tree::TraditionalView::navRight); auto toggleShowLabel = new QAction{"Show labels down", this}; toggleShowLabel->setShortcut(QKeySequence("L")); nodeMenu->addAction(toggleShowLabel); connect(toggleShowLabel, &QAction::triggered, view_.get(), &tree::TraditionalView::showLabelsDown); auto toggleShowLabelsUp = new QAction{"Show labels down", this}; toggleShowLabelsUp->setShortcut(QKeySequence("Shift+L")); nodeMenu->addAction(toggleShowLabelsUp); connect(toggleShowLabelsUp, &QAction::triggered, view_.get(), &tree::TraditionalView::showLabelsUp); auto hideFailed = new QAction{"Hide failed", this}; hideFailed->setShortcut(QKeySequence("F")); nodeMenu->addAction(hideFailed); connect(hideFailed, &QAction::triggered, this, &analysis::MergeWindow::hideFailed); auto unhideAll = new QAction{"Unhide all", this}; unhideAll->setShortcut(QKeySequence("U")); nodeMenu->addAction(unhideAll); connect(unhideAll, &QAction::triggered, view_.get(), &tree::TraditionalView::unhideAll); auto toggleHighlighted = new QAction{"Toggle highlight subtree", this}; toggleHighlighted->setShortcut(QKeySequence("H")); nodeMenu->addAction(toggleHighlighted); connect(toggleHighlighted, &QAction::triggered, view_.get(), &tree::TraditionalView::toggleHighlighted); auto button = new QToolButton(widget); button->setStyleSheet("padding: 3px;"); button->setPopupMode(QToolButton::InstantPopup); button->setText("Node"); button->setToolButtonStyle(Qt::ToolButtonTextOnly); button->setMenu(nodeMenu); button_layout->addWidget(button); } { auto debugMenu = new QMenu("&Debug"); auto updateLayoutAction = new QAction{"Update layout", this}; debugMenu->addAction(updateLayoutAction); connect(updateLayoutAction, &QAction::triggered, view_.get(), &tree::TraditionalView::updateLayout); auto updateView = new QAction{"Update view", this}; debugMenu->addAction(updateView); connect(updateView, &QAction::triggered, view_.get(), &tree::TraditionalView::needsRedrawing); auto button = new QToolButton(widget); button->setStyleSheet("padding: 3px;"); button->setPopupMode(QToolButton::InstantPopup); button->setText("Debug"); button->setToolButtonStyle(Qt::ToolButtonTextOnly); button->setMenu(debugMenu); button_layout->addWidget(button); } { auto analysisMenu = new QMenu("&Analysis"); auto ngAnalysisAction = new QAction{"Nogood analysis", this}; analysisMenu->addAction(ngAnalysisAction); connect(ngAnalysisAction, &QAction::triggered, this, &MergeWindow::runNogoodAnalysis); auto button = new QToolButton(widget); button->setStyleSheet("padding: 3px;"); button->setPopupMode(QToolButton::InstantPopup); button->setText("Analysis"); button->setToolButtonStyle(Qt::ToolButtonTextOnly); button->setMenu(analysisMenu); button_layout->addWidget(button); } button_layout->addStretch(); layout->addWidget(widget, 0, 0, 1, 0); } pentagon_bar->update(merge_result_->size()); pent_list->updateScene(); } MergeWindow::~MergeWindow() = default; /// Find original ids for nodes under a pentagon static void linkLocationsPentagon(NodeID n_m, const tree::NodeTree &nt_m, NodeID n, const tree::NodeTree &nt, std::vector &locs) { std::stack stack_m; /// stack for nodes of the merged tree std::stack stack; /// stack for nodes of the original tree if (n_m == NodeID::NoNode && n == NodeID::NoNode) return; stack_m.push(n_m); stack.push(n); while (!stack_m.empty()) { auto node_m = stack_m.top(); stack_m.pop(); auto node = stack.top(); stack.pop(); locs[n_m] = {n}; /// The trees at this point must have the same strucutre for (auto alt = nt_m.childrenCount(node_m) - 1; alt >= 0; --alt) { stack_m.push(nt_m.getChild(node_m, alt)); stack.push(nt.getChild(node, alt)); } } } void MergeWindow::initOrigLocations() { // TODO: Perform DFS traversal of three trees in lockstep assigning ids orig_locations_.resize(nt_->nodeCount()); std::stack stack_m; /// stack for nodes of the merged tree std::stack stack_l; /// stack for nodes of the left tree std::stack stack_r; /// stack for nodes of the right tree auto &nt_l = ex_l_.tree(); /// left tree auto &nt_r = ex_r_.tree(); /// right tree stack_m.push(nt_->getRoot()); stack_l.push(nt_l.getRoot()); stack_r.push(nt_r.getRoot()); while (!stack_m.empty()) { auto n = stack_m.top(); stack_m.pop(); auto n_l = stack_l.top(); stack_l.pop(); auto n_r = stack_r.top(); stack_r.pop(); if (nt_->getStatus(n) == tree::NodeStatus::MERGED) { auto left_child = nt_->getChild(n, 0); auto right_child = nt_->getChild(n, 1); linkLocationsPentagon(left_child, *nt_, n_l, nt_l, orig_locations_); linkLocationsPentagon(right_child, *nt_, n_r, nt_r, orig_locations_); continue; } orig_locations_[n] = {n_l}; /// arbitrarily link to the node on the left tree /// Note: the tree above merged nodes must have the same structure for (auto alt = nt_->childrenCount(n) - 1; alt >= 0; --alt) { stack_m.push(nt_->getChild(n, alt)); stack_l.push(nt_l.getChild(n_l, alt)); stack_r.push(nt_r.getChild(n_r, alt)); } } } tree::NodeTree &MergeWindow::getTree() { return *nt_; } MergeResult &MergeWindow::mergeResult() { return *merge_result_; } namespace ng_analysis { struct ReductionStats { int total_red; /// total reduction by a nogood int count; /// number of times a nogood contributed to a 1-n pentagon }; class ResultBuilder { using ResType = std::unordered_map; /// Accumulate nogood contributions here ResType ng_items; public: ResultBuilder() {} /// Account for search reduction of one 1-n pentagon void addPentagonData( const std::vector &nogoods, // responsible nogoods int red) // node reduction (n-1) { /// reduction attributed to each nogood const auto rel_red = std::ceil((float)red / nogoods.size()); for (auto ng : nogoods) { if (ng_items.find(ng) == ng_items.end()) { /// never seen this nogood before ng_items.insert({ng, {0, 0}}); } auto &ng_stats = ng_items.at(ng); ng_stats.count++; ng_stats.total_red += rel_red; } } const ResType &result() const { return ng_items; } }; } // namespace ng_analysis void MergeWindow::runNogoodAnalysis() const { /// Determine which tree has nogoods const bool l_has_ng = ex_l_.hasNogoods(); const bool r_has_ng = ex_r_.hasNogoods(); /// whether treat the execution on the left as the one with nogoods const bool left = [&]() { /// left execution is the default one, but /// use the one on the right if it is the only /// one with nogoods if (r_has_ng && !l_has_ng) return false; else if (r_has_ng && l_has_ng) { print("NOTE: both LEFT and RIGHT executions have nogoods"); return true; } return true; }(); print("merge result size: {}", merge_result_->size()); ng_analysis::ResultBuilder res_builder; /// The tree with nogoods const auto &ng_tree = left ? ex_l_.tree() : ex_r_.tree(); for (auto &item : *merge_result_) { /// check if the nogood tree contains 1 node subtree under pentagon const auto subtree_size = left ? item.size_l : item.size_r; if (subtree_size != 1) continue; /// See what nogoods contribute to the nogood at item.nid /// get the sole node const auto alt = left ? 0 : 1; const auto kid = nt_->getChild(item.pen_nid, alt); const auto orig_id = orig_locations_[kid].nid; /// get contributing nogoods: const auto *nogoods = ng_tree.solver_data().getContribNogoods(orig_id); if (nogoods) { res_builder.addPentagonData(*nogoods, std::abs(item.size_r - item.size_l)); } else { // print("no contrib nogoods for {}", orig_locations_[kid_l].nid); } } /// construct ng analysis data in the format required by ng dialog std::vector nga_data; nga_data.reserve(res_builder.result().size()); for (auto item : res_builder.result()) { const NogoodID id = item.first; const auto &ng_str = ng_tree.getNogood(id); const auto *reasons_ptr = ng_tree.solver_data().getContribConstraints(id); std::vector reasons = reasons_ptr ? *reasons_ptr : std::vector{}; nga_data.push_back({id, ng_str, item.second.total_red, item.second.count, std::move(reasons)}); } auto ng_window = new NogoodAnalysisDialog(std::move(nga_data)); ng_window->setAttribute(Qt::WA_DeleteOnClose); connect(ng_window, &NogoodAnalysisDialog::nogoodClicked, [this](NodeID nid) { const_cast(view_.get())->setAndCenterNode(nid); }); ng_window->show(); } static bool has_as_ancestor(const tree::NodeTree &nt, NodeID n, NodeID a) { while (n != NodeID::NoNode) { if (n == a) return true; n = nt.getParent(n); } return false; } /// Check if the node is under some pentagon static bool under_pentagon(const tree::NodeTree &nt, NodeID n) { /// Don't check the node itself if (n != NodeID::NoNode) { n = nt.getParent(n); } while (n != NodeID::NoNode) { if (nt.getStatus(n) == tree::NodeStatus::MERGED) { return true; } n = nt.getParent(n); } return false; } /// Hide failed subtrees in a way that does not hide pentagon nodes void MergeWindow::hideFailed() { auto cur_node = view_->node(); /// Hide if the node is under some pentagon /// (meaning there are no other pentagons under the node) if (under_pentagon(*nt_, cur_node)) { view_->hideFailedAt(cur_node); return; } /// Otherwise, see which pentagons are under the /// node and try to hide their children for (auto item : *merge_result_) { /// pentagon node const auto pen = item.pen_nid; /// hide failed children if pen is below cur_node if (has_as_ancestor(*nt_, pen, cur_node)) { for (auto alt = 0; alt < nt_->childrenCount(pen); ++alt) { auto kid = nt_->getChild(pen, alt); view_->hideFailedAt(kid); } } } } // NodeID MergeWindow::findOriginalId(NodeID nid) const // { // auto iter = nid; // /// For now assume the node comes from left tree // bool left_tree = true; // while (iter != NodeID::NoNode) // { // const auto status = nt_.getStatus(iter); // if (status == tree::NodeStatus::MERGED) // { // print("found pentagon node: {}", iter); // for () // } // iter = nt_.getParent(iter); // } // } } // namespace analysis } // namespace cpprofiler ================================================ FILE: cp-profiler/src/cpprofiler/analysis/merge_window.hh ================================================ #pragma once #include #include "../tree/node_tree.hh" #include "merging/merge_result.hh" namespace cpprofiler { namespace tree { class TraditionalView; } class Execution; class UserData; } // namespace cpprofiler namespace cpprofiler { namespace analysis { class PentagonCounter; class PentagonListWidget; /// Original location for a node; struct OriginalLoc { NodeID nid; }; class MergeWindow : public QMainWindow { Q_OBJECT /// The two executions merged Execution &ex_l_; Execution &ex_r_; std::shared_ptr nt_; std::shared_ptr merge_result_; /// Dummy user data (required for traditional view) std::unique_ptr user_data_; /// Dummy solver data (required for traditional view) std::unique_ptr solver_data_; std::unique_ptr view_; PentagonCounter *pentagon_bar; PentagonListWidget *pent_list; /// Original locations for node `i` (used for nogoods/labels etc) std::vector orig_locations_; private: /// find the right data for a node Nogood getNogood(); /// traverse the merged tree to find origins for all nodes void initOrigLocations(); /// Find id for a node `nid` of a merged tree // NodeID findOriginalId(NodeID nid) const; /// Hide all failed subtrees under pentagons void hideFailed(); public: MergeWindow(Execution &ex_l, Execution &ex_r, std::shared_ptr nt, std::shared_ptr res, QWidget* parent = nullptr); ~MergeWindow(); tree::NodeTree &getTree(); MergeResult &mergeResult(); public slots: void runNogoodAnalysis() const; }; } // namespace analysis } // namespace cpprofiler ================================================ FILE: cp-profiler/src/cpprofiler/analysis/merging/merge_result.hh ================================================ #pragma once #include "../../core.hh" #include namespace cpprofiler { namespace analysis { struct PentagonItem { /// pentagon node NodeID pen_nid; /// left subtree size int size_l; /// right subtree size int size_r; }; using MergeResult = std::vector; } // namespace analysis } // namespace cpprofiler ================================================ FILE: cp-profiler/src/cpprofiler/analysis/merging/pentagon_list_widget.hh ================================================ #pragma once #include #include #include #include "merge_result.hh" #include "pentagon_rect.hh" namespace cpprofiler { namespace analysis { class PentagonListWidget : public QWidget { Q_OBJECT /// Elements for drawing primitives QGraphicsView *view_; QGraphicsScene *scene_; /// Data to be visualised const MergeResult &merge_res_; /// Whether pentagon list should be sorted (by right/left difference) bool needs_sorting = true; /// Which pentagon from the list is selected NodeID selected_ = NodeID::NoNode; public: /// Create specifying parent widget and the result of merging PentagonListWidget(QWidget *parent, const MergeResult &res); /// Clear the view and draw horizontal bars indicating pentagons void updateScene(); /// Width available for drawing int viewWidth() { return view_->viewport()->width(); } /// Handle pentagon click void handleClick(NodeID node) { selected_ = node; emit pentagonClicked(node); updateScene(); } signals: /// Indicate that a pentagon (associated with some NodeID) was clicked void pentagonClicked(NodeID); public slots: /// Handle checkbox click from Merge Window void handleSortCB(int state) { needs_sorting = (state == Qt::Checked); updateScene(); } }; inline PentagonListWidget::PentagonListWidget(QWidget *w, const MergeResult &res) : QWidget(w), merge_res_(res) { auto layout = new QVBoxLayout(this); view_ = new QGraphicsView(this); view_->setAlignment(Qt::AlignLeft | Qt::AlignTop); view_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); view_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); view_->setMaximumWidth(pent_config::VIEW_WIDTH); view_->setMinimumWidth(pent_config::VIEW_WIDTH); scene_ = new QGraphicsScene(this); view_->setScene(scene_); layout->addWidget(view_); } inline void PentagonListWidget::updateScene() { using namespace pent_config; scene_->clear(); /// This is used to scale the horizontal bars int max_value = 0; for (const auto &pen : merge_res_) { max_value = std::max(max_value, std::max(pen.size_l, pen.size_r)); } auto displayed_items = merge_res_; /// make copy const auto sort_function = [](const PentagonItem &p1, const PentagonItem &p2) { const auto diff1 = std::abs(p1.size_r - p1.size_l); const auto diff2 = std::abs(p2.size_r - p2.size_l); if (diff1 < diff2) { return false; } else if (diff2 > diff1) { return true; } else { const auto sum1 = p1.size_r + p1.size_l; const auto sum2 = p2.size_r + p2.size_l; return sum2 < sum1; } }; if (needs_sorting) { std::sort(displayed_items.begin(), displayed_items.end(), sort_function); } for (auto i = 0; i < displayed_items.size(); ++i) { const auto ypos = i * (HEIGHT + PADDING) + PADDING; const bool sel = (displayed_items[i].pen_nid == selected_); new PentagonRect(scene_, *this, displayed_items[i], ypos, max_value, sel); } } } // namespace analysis } // namespace cpprofiler ================================================ FILE: cp-profiler/src/cpprofiler/analysis/merging/pentagon_rect.cpp ================================================ #include "pentagon_rect.hh" #include "merge_result.hh" #include "pentagon_list_widget.hh" #include #include namespace cpprofiler { namespace analysis { PentagonRect::PentagonRect(QGraphicsScene *scene, PentagonListWidget &listw, const PentagonItem &pen, int y, int max_val, bool selected) : QGraphicsRectItem(pent_config::PADDING, y, listw.viewWidth() - 1, pent_config::HEIGHT), m_pen_list_widget(listw), m_node(pen.pen_nid) { using namespace pent_config; const int PENT_WIDTH = listw.viewWidth() - 1; const int HALF_WIDTH = PENT_WIDTH / 2; const float scale_x = (float)HALF_WIDTH / max_val; const int value_l = pen.size_l; const int value_r = pen.size_r; const int width_l = value_l * scale_x; const int width_r = value_r * scale_x; const int cx = PADDING + listw.viewWidth() / 2; if (selected) { setBrush(sel_color); } auto left = new QGraphicsRectItem(cx - width_l, y, width_l, HEIGHT); left->setBrush(left_color); // left->setPen(Qt::NoPen); auto right = new QGraphicsRectItem(cx, y, width_r, HEIGHT); right->setBrush(right_color); // right->setPen(Qt::NoPen); scene->addItem(this); scene->addItem(left); scene->addItem(right); { auto text_item = new QGraphicsSimpleTextItem{QString::number(value_l)}; text_item->setPos(TEXT_PAD, y); scene->addItem(text_item); } { auto text_item = new QGraphicsSimpleTextItem{QString::number(value_r)}; const int item_width = text_item->boundingRect().width(); const int x = PENT_WIDTH - TEXT_PAD - item_width; text_item->setPos(x, y); scene->addItem(text_item); } } void PentagonRect::mousePressEvent(QGraphicsSceneMouseEvent *) { m_pen_list_widget.handleClick(m_node); } } // namespace analysis } // namespace cpprofiler ================================================ FILE: cp-profiler/src/cpprofiler/analysis/merging/pentagon_rect.hh ================================================ #pragma once #include #include "../../core.hh" namespace cpprofiler { namespace analysis { class PentagonListWidget; struct PentagonItem; namespace pent_config { constexpr int HEIGHT = 16; constexpr int PADDING = 0; constexpr int VIEW_WIDTH = 150; // constexpr int PENT_WIDTH = VIEW_WIDTH - PADDING * 2 - 3; // constexpr int HALF_WIDTH = PENT_WIDTH / 2; constexpr int TEXT_PAD = 10; static QColor left_color{153, 204, 255}; static QColor right_color{255, 153, 204}; /// color for selected pentagon item static QColor sel_color{150, 150, 150}; } // namespace pent_config class PentagonRect : public QGraphicsRectItem { private: PentagonListWidget &m_pen_list_widget; /// Pentagon node associated with this item NodeID m_node; void mousePressEvent(QGraphicsSceneMouseEvent *) override; public: PentagonRect(QGraphicsScene *scene, PentagonListWidget &listw, const PentagonItem &pen, int y, int max_val, bool selected); }; } // namespace analysis } // namespace cpprofiler ================================================ FILE: cp-profiler/src/cpprofiler/analysis/nogood_analysis_dialog.hh ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include "../core.hh" namespace cpprofiler { namespace analysis { struct NgAnalysisItem { NogoodID nid; /// node id of the nogood const Nogood &ng; /// textual representation of the nogood int total_red; /// total reduction by this nogood int count; /// number of times the nogood found in a 1-n pentagon std::vector constraint_ids; /// reasons for the nogood }; using NgAnalysisData = std::vector; class NogoodProxyModel : public QSortFilterProxyModel { bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { /// TODO: extend this to work for clauses too (although this doesn't crash) int lhs = sourceModel()->data(left).toInt(); int rhs = sourceModel()->data(right).toInt(); return lhs < rhs; } }; class NogoodAnalysisDialog : public QDialog { Q_OBJECT private: std::unique_ptr ng_model_; QTableView *ng_table_; NgAnalysisData ng_data_; void init() { static constexpr int DEFAULT_WIDTH = 1000; static constexpr int DEFAULT_HEIGHT = 600; resize(DEFAULT_WIDTH, DEFAULT_HEIGHT); auto layout = new QVBoxLayout(this); ng_table_ = new QTableView(); ng_table_->setSortingEnabled(true); layout->addWidget(ng_table_); ng_model_.reset(new QStandardItemModel(0, 4)); auto proxy_model = new NogoodProxyModel(); proxy_model->setSourceModel(ng_model_.get()); const QStringList headers{"NodeID", "Total Reduction", "Count", "Clause"}; ng_model_->setHorizontalHeaderLabels(headers); ng_table_->horizontalHeader()->setStretchLastSection(true); ng_table_->setSelectionBehavior(QAbstractItemView::SelectRows); ng_table_->setEditTriggers(QAbstractItemView::NoEditTriggers); ng_table_->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); ng_table_->setModel(proxy_model); connect(ng_table_, &QTableView::doubleClicked, [this](const QModelIndex &idx) { const auto nid = ng_model_->item(idx.row())->text().toInt(); emit nogoodClicked(NodeID(nid)); }); auto save_ng_btn = new QPushButton("Save Nogoods"); layout->addWidget(save_ng_btn, 0, Qt::AlignLeft); connect(save_ng_btn, &QPushButton::clicked, this, &NogoodAnalysisDialog::saveNogoods); } void saveNogoods() { QString file_name = QFileDialog::getSaveFileName(this, "Save nogoods to"); /// No file was selected if (file_name.isEmpty()) return; QFile file(file_name); if (!file.open(QIODevice::WriteOnly)) { print("Error: could not open for writing: {}", file_name); return; } QTextStream nogood_stream(&file); const char sep = '\t'; nogood_stream << "nid" << sep << "count" << sep << "reduction" << sep << "nogood" << sep << "reasons" << '\n'; for (auto &ng_item : ng_data_) { nogood_stream << ng_item.nid << sep; nogood_stream << ng_item.count << sep; nogood_stream << ng_item.total_red << sep; nogood_stream << ng_item.ng.get().c_str() << sep; for (auto id : ng_item.constraint_ids) { nogood_stream << id << ' '; } nogood_stream << '\n'; } } public: NogoodAnalysisDialog(NgAnalysisData nga_data) : QDialog(), ng_data_(std::move(nga_data)) { init(); populate(ng_data_); ng_table_->sortByColumn(1, Qt::SortOrder::DescendingOrder); } void populate(const NgAnalysisData &nga_data) { for (auto &item : nga_data) { const auto nid_i = new QStandardItem(QString::number(item.nid)); const auto left_i = new QStandardItem(QString::number(item.total_red)); const auto right_i = new QStandardItem(QString::number(item.count)); const auto ng_i = new QStandardItem(item.ng.get().c_str()); ng_model_->appendRow({nid_i, left_i, right_i, ng_i}); } } signals: void nogoodClicked(NodeID nid); }; } // namespace analysis } // namespace cpprofiler ================================================ FILE: cp-profiler/src/cpprofiler/analysis/path_comp.cpp ================================================ #include "path_comp.hh" #include using std::vector; namespace cpprofiler { namespace analysis { /// A wrapper around std::set_intersection; copying is intended static vector