Add project files.
This commit is contained in:
48
.editorconfig
Normal file
48
.editorconfig
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
[*.{c,c++,cc,cginc,compute,cp,cpp,cu,cuh,cxx,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,mpp,mq4,mq5,mqh,tpp,usf,ush}]
|
||||
indent_style = tab
|
||||
indent_size = tab
|
||||
tab_width = 4
|
||||
|
||||
[*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
|
||||
[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
|
||||
[*]
|
||||
|
||||
# Microsoft .NET properties
|
||||
csharp_new_line_before_members_in_object_initializers = false
|
||||
csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
|
||||
csharp_style_var_for_built_in_types = false:suggestion
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
|
||||
dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none
|
||||
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||
|
||||
# ReSharper properties
|
||||
resharper_for_built_in_types = use_var_when_evident
|
||||
resharper_for_simple_types = use_var_when_evident
|
||||
|
||||
# ReSharper inspection severities
|
||||
resharper_arrange_object_creation_when_type_evident_highlighting = none
|
||||
resharper_arrange_redundant_parentheses_highlighting = hint
|
||||
resharper_arrange_this_qualifier_highlighting = hint
|
||||
resharper_arrange_type_member_modifiers_highlighting = hint
|
||||
resharper_arrange_type_modifiers_highlighting = hint
|
||||
resharper_built_in_type_reference_style_for_member_access_highlighting = hint
|
||||
resharper_built_in_type_reference_style_highlighting = hint
|
||||
resharper_merge_sequential_patterns_highlighting = none
|
||||
resharper_redundant_base_qualifier_highlighting = warning
|
||||
resharper_suggest_var_or_type_built_in_types_highlighting = hint
|
||||
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
||||
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: miroiu
|
||||
custom: ["https://www.buymeacoffee.com/miroiu", "https://paypal.me/miroiuemanuel"]
|
||||
17
.github/ISSUE_TEMPLATE/add_example_app.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/add_example_app.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: "\U0001F680 Example application"
|
||||
about: Suggest an example application that others can benefit from
|
||||
title: "[Application]"
|
||||
labels: application
|
||||
assignees: miroiu
|
||||
|
||||
---
|
||||
|
||||
**Describe the application**
|
||||
A clear and concise description of what the application is doing.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots.
|
||||
|
||||
**Additional context**
|
||||
Add any other context here.
|
||||
10
.github/ISSUE_TEMPLATE/ask-a-question.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/ask-a-question.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: "❓ Ask a question"
|
||||
about: Need help or have a question?
|
||||
title: "[Question]"
|
||||
labels: question
|
||||
assignees: miroiu
|
||||
|
||||
---
|
||||
|
||||
|
||||
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: "\U0001F41B Bug report"
|
||||
about: Create a bug report to help this project improve
|
||||
title: "[Bug]"
|
||||
labels: bug
|
||||
assignees: miroiu
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi!
|
||||
|
||||
All fields are optional and are present only to guide you.
|
||||
Thanks for contributing!
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 📝 Read the docs
|
||||
url: https://github.com/miroiu/nodify/wiki
|
||||
about: Be sure you've read the docs!
|
||||
17
.github/ISSUE_TEMPLATE/documentation.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/documentation.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: 📖 Documentation
|
||||
about: Report an issue related to documentation
|
||||
title: "[Docs]"
|
||||
labels: documentation
|
||||
assignees: miroiu
|
||||
|
||||
---
|
||||
|
||||
**Describe the issue**
|
||||
A clear and concise description of what the issue is.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots.
|
||||
|
||||
**Additional context**
|
||||
Add any other context here.
|
||||
27
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "⭐️ Feature request"
|
||||
about: Submit a request or a proposal for this project
|
||||
title: "[Feature]"
|
||||
labels: enhancement
|
||||
assignees: miroiu
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi!
|
||||
|
||||
All fields are optional and are present only to guide you.
|
||||
Thanks for contributing!
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
22
.github/workflows/build.yml
vendored
Normal file
22
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master", "release-v*"]
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "9.0.x"
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release
|
||||
49
.github/workflows/codeql-analysis.yml
vendored
Normal file
49
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master", "release-v*"]
|
||||
paths:
|
||||
- Nodify/**
|
||||
pull_request:
|
||||
branches: ["master", "release-v*"]
|
||||
schedule:
|
||||
- cron: "27 6 * * 2"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: csharp
|
||||
build-mode: none
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
29
.github/workflows/create-release-branch.yml
vendored
Normal file
29
.github/workflows/create-release-branch.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Create release branch
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.0.0"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Save tag version
|
||||
uses: little-core-labs/get-git-tag@v3.0.1
|
||||
id: tagName
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "9.0.x"
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
- name: Create release branch
|
||||
uses: peterjgrainger/action-create-branch@v2.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
branch: release-${{ steps.tagName.outputs.tag }}
|
||||
24
.github/workflows/publish-package.yml
vendored
Normal file
24
.github/workflows/publish-package.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Publish package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "9.0.x"
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
- name: Publish the package
|
||||
run: dotnet nuget push "*/bin/Release/*.nupkg" -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json
|
||||
22
.github/workflows/sync-docs.yml
vendored
Normal file
22
.github/workflows/sync-docs.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
job-sync-docs-to-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Sync docs to wiki
|
||||
uses: newrelic/wiki-sync-action@main
|
||||
with:
|
||||
source: docs
|
||||
destination: wiki
|
||||
token: ${{ secrets.DOCS_TOKEN }}
|
||||
350
.gitignore
vendored
Normal file
350
.gitignore
vendored
Normal file
@@ -0,0 +1,350 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
BIN
APIVisualExecutor-master.7z
Normal file
BIN
APIVisualExecutor-master.7z
Normal file
Binary file not shown.
353
CHANGELOG.md
Normal file
353
CHANGELOG.md
Normal file
@@ -0,0 +1,353 @@
|
||||
## Changelog
|
||||
|
||||
#### **In development**
|
||||
|
||||
> - Breaking Changes:
|
||||
> - Features:
|
||||
> - Bugfixes:
|
||||
|
||||
#### **Version 7.1.0**
|
||||
|
||||
> - Breaking Changes:
|
||||
> - Added ProcessHandledEvents to IInputHandler and removed it from InputProcessor
|
||||
> - Renamed EditorGestures.Editor.ResetViewportLocation to EditorGestures.Editor.ResetViewport
|
||||
> - Features:
|
||||
> - Introduced a new BringIntoView method overload in NodifyEditor that accepts an offset from the viewport edges
|
||||
> - Added BringIntoViewEdgeOffset to NodifyEditor to control the viewport edge offset when bringing the focused element into view
|
||||
> - Added ResetViewport to NodifyEditor to reset the viewport's location and zoom
|
||||
> - Improved tab and directional navigation, ensuring that focused elements are automatically brought into view
|
||||
> - Added keyboard navigation layers for nodes, connections and decorators; restricting keyboard navigation to the active layer
|
||||
> - Added ActiveNavigationLayer, ActivateNextNavigationLayer, ActivatePreviousNavigationLayer, RegisterNavigationLayer, RemoveNavigationLayer and ActivateNavigationLayer to NodifyEditor for keyboard layers management
|
||||
> - Added KeyboardNavigationLayer property to NodifyEditor that allows navigating through the ItemContainers
|
||||
> - Added AutoRegisterConnectionsLayer, AutoRegisterDecoratorsLayer, AutoFocusFirstElement, AutoPanOnNodeFocus, PanViewportOnKeyboardDrag and MinimumNavigationStepSize to NodifyEditor
|
||||
> - Added EditorGestures.Editor.Keyboard for keyboard navigation gestures
|
||||
> - Added FindNextFocusTarget, OnElementFocused and OnKeyboardNavigationLayerActivated virtual methods to NodifyEditor
|
||||
> - Added new gestures for keyboard navigation available in EditorGestures.Editor.Keyboard
|
||||
> - Added ToggleContentSelection to GroupingNode and its corresponding gesture to toggle the selection of nodes inside the group
|
||||
> - Added ZoomIn, ZoomOut and ResetViewport methods to the Minimap control
|
||||
> - Added ZoomIn, ZoomOut, ResetViewport and Pan gestures to EditorGestures.Minimap
|
||||
> - Added NavigationStepSize static property to Minimap
|
||||
> - Added Unbind to all gestures inside EditorGestures
|
||||
> - Added the KeyComboGesture that requires a trigger key to be held down before pressing a combo key
|
||||
> - Added FocusVisualPen and FocusVisualPadding dependency properties to BaseConnection
|
||||
> - Added default focus visuals for base editor controls that can be included by referencing the FocusVisual.xaml file
|
||||
> - Added MaxHotKeys and HotKeysDisplayMode static configuration fields to PendingConnection
|
||||
> - Added HotKeyControl with its corresponding theme resources to display the hotkeys for a pending connection
|
||||
|
||||
#### **Version 7.0.4**
|
||||
|
||||
> - Features:
|
||||
> - Added AsRef extension method to InputGesture to convert it to an InputGestureRef
|
||||
> - Bugfixes:
|
||||
> - Fixed an issue where the gesture used for EditorGestures.Editor.SelectAll extracted from the ApplicationCommands was assumed to be a KeyGesture
|
||||
> - Fixed overrides of DrawDirectionalArrowheadGeometry virtual method not working in subclasses of the built in connections
|
||||
> - Fixed a memory leak caused by the auto panning timer
|
||||
|
||||
#### **Version 7.0.3**
|
||||
|
||||
> - Bugfixes:
|
||||
> - Fixed an issue where the SelectedEvent and UnselectedEvent events on the ItemContainer were not raised when the selection was completed
|
||||
|
||||
#### **Version 7.0.2**
|
||||
|
||||
> - Features:
|
||||
> - Added EditorGestures.Editor.SelectAll
|
||||
> - Bugfixes:
|
||||
> - Fixed an issue where the EditorCommands.SelectAll gesture could not be customized
|
||||
|
||||
#### **Version 7.0.1**
|
||||
|
||||
> - Bugfixes:
|
||||
> - Fixed an issue where connections would not gain focus when selected, which could prevent editor keybindings from functioning in certain scenarios
|
||||
> - Resolved an issue where selecting a node did not deselect connections and vice versa
|
||||
> - Fixed a bug preventing ItemContainers from being selected when the mouse could not be captured
|
||||
> - Fixed an issue with key detection in Japanese IME environments, causing issues with the MouseGesture
|
||||
|
||||
#### **Version 7.0.0**
|
||||
|
||||
> - Breaking Changes:
|
||||
> - Made the setter of NodifyEditor.IsPanning private
|
||||
> - Made SelectionHelper internal
|
||||
> - Renamed HandleRightClickAfterPanningThreshold to MouseActionSuppressionThreshold in NodifyEditor
|
||||
> - Renamed StartCutting to BeginCutting in NodifyEditor
|
||||
> - Renamed Connector.EnableStickyConnections to ConnectorState.EnabledToggledConnectingMode
|
||||
> - Renamed PushItems to UpdatePushedArea and StartPushingItems to BeginPushingItems in NodifyEditor
|
||||
> - Renamed UnselectAllConnection to UnselectAllConnections in NodifyEditor
|
||||
> - Removed DragStarted, DragDelta and DragCompleted routed events from ItemContainer
|
||||
> - Replaced the System.Windows.Input.MouseGesture with Nodify.Interactivity.MouseGesture for default EditorGesture mappings
|
||||
> - Removed State, GetInitialState, PushState, PopState and PopAllStates from NodifyEditor and ItemContainer
|
||||
> - Replaced EditorState and ContainerState with InputElementState
|
||||
> - Moved AllowCuttingCancellation from CuttingLine to NodifyEditor
|
||||
> - Moved AllowDraggingCancellation from ItemContainer to NodifyEditor
|
||||
> - Moved EditorGestures under the Nodify.Interactivity namespace
|
||||
> - Moved editor events under the Nodify.Events namespace
|
||||
> - Features:
|
||||
> - Added BeginPanning, UpdatePanning, EndPanning, CancelPanning and AllowPanningCancellation to NodifyEditor and Minimap
|
||||
> - Added MouseLocation, ZoomAtPosition and GetLocationInsideMinimap to Minimap
|
||||
> - Added UpdateCuttingLine to NodifyEditor
|
||||
> - Added Select, BeginSelecting, UpdateSelection, EndSelecting, CancelSelecting and AllowSelectionCancellation to NodifyEditor
|
||||
> - Added IsDragging, BeginDragging, UpdateDragging, EndDragging and CancelDragging to NodifyEditor
|
||||
> - Added AlignSelection and AlignContainers methods to NodifyEditor
|
||||
> - Added LockSelection and UnlockSelection methods to NodifyEditor and EditorCommands
|
||||
> - Added ItemsMoved routed event to NodifyEditor
|
||||
> - Added HasCustomContextMenu dependency property to NodifyEditor, ItemContainer, Connector and BaseConnection
|
||||
> - Added Select, BeginDragging, UpdateDragging, EndDragging and CancelDragging to ItemContainer
|
||||
> - Added PreserveSelectionOnRightClick configuration field to ItemContainer
|
||||
> - Added BeginConnecting, UpdatePendingConnection, EndConnecting, CancelConnecting and RemoveConnections methods to Connector
|
||||
> - Added FindTargetConnector and FindConnectionTarget methods to Connector
|
||||
> - Added a custom MouseGesture with support for key combinations
|
||||
> - Added InputProcessor to NodifyEditor, ItemContainer, Connector, BaseConnection and Minimap, enabling the extension of controls with custom states
|
||||
> - Added DragState to simplify creating click-and-drag interactions, with support for initiating and completing them using the keyboard
|
||||
> - Added InputElementStateStack, InputElementStateStack.DragState and InputElementStateStack.InputElementState to manage transitions between states in UI elements
|
||||
> - Added InputProcessor.Shared to enable the addition of global input handlers
|
||||
> - Move the viewport to the mouse position when zooming on the Minimap if ResizeToViewport is false
|
||||
> - Added SplitAtLocation and Remove methods to BaseConnection
|
||||
> - Added AllowPanningWhileSelecting, AllowPanningWhileCutting and AllowPanningWhilePushingItems to EditorState
|
||||
> - Added AllowZoomingWhilePanning, AllowZoomingWhileSelecting, AllowZoomingWhileCutting and AllowZoomingWhilePushingItems to EditorState
|
||||
> - Added EnableToggledSelectingMode, EnableToggledPanningMode, EnableToggledPushingItemsMode and EnableToggledCuttingMode to EditorState
|
||||
> - Added MinimapState.EnableToggledPanningMode
|
||||
> - Added ContainerState.EnableToggledDraggingMode
|
||||
> - Added Unbind to InputGestureRef and EditorGestures.SelectionGestures
|
||||
> - Added EnableHitTesting to PendingConnection
|
||||
> - Bugfixes:
|
||||
> - Fixed an issue where the ItemContainer was selected by releasing the mouse button on it, even when the mouse was not captured
|
||||
> - Fixed an issue where the ItemContainer could open its context menu even when it was not selected
|
||||
> - Fixed an issue where the Home button caused the editor to fail to display items when contained within a ScrollViewer
|
||||
> - Fixed an issue where connector optimization did not work when SelectedItems was not data-bound
|
||||
> - Fixed EditorCommands.Align to perform a single arrange invalidation instead of one for each aligned container
|
||||
> - Fixed an issue where controls would capture the mouse unnecessarily; they now capture it only in response to a defined gesture
|
||||
> - Fixed an issue where the minimap could update the viewport without having the mouse captured
|
||||
> - Fixed ItemContainer.Select and NodifyEditor.SelectArea to clear the existing selection and select the containers within the same transaction
|
||||
> - Fixed an issue where editor interactions failed to cancel upon losing mouse capture
|
||||
> - Fixed an issue where selecting a new connection would not clear the previous selection within the same transaction
|
||||
|
||||
#### **Version 6.6.0**
|
||||
|
||||
> - Features:
|
||||
> - Added InputGroupStyle and OutputGroupStyle to Node
|
||||
> - Added PanWithMouseWheel, PanHorizontalModifierKey and PanVerticalModifierKey to EditorGestures.Editor
|
||||
> - Added CornerRadius dependency property to LineConnection, CircuitConnection and StepConnection
|
||||
> - Added EditorGestures.Editor.PushItems gesture used to start pushing ItemContainers vertically or horizontally
|
||||
> - Added PushedAreaStyle, PushedAreaOrientation and IsPushingItems dependency properties to NodifyEditor
|
||||
> - Added NodifyEditor.SnapToGrid utility function
|
||||
> - Bugfixes:
|
||||
> - Fixed ItemContainer.BorderBrush and ItemContainer.SelectedBrush not reacting to theme changes
|
||||
|
||||
#### **Version 6.5.0**
|
||||
|
||||
> - Features:
|
||||
> - Added SelectedConnection, SelectedConnections, CanSelectMultipleConnections and CanSelectMultipleItems dependency properties to NodifyEditor
|
||||
> - Added IsSelected and IsSelectable attached dependency properties to BaseConnection
|
||||
> - Added PrioritizeBaseConnectionForSelection static field to BaseConnection
|
||||
> - Added EditorGestures.Connection.Selection
|
||||
> - Added support for ScrollViewer in NodifyEditor (implements IScrollInfo)
|
||||
> - Added NodifyEditor.ScrollIncrement dependency property
|
||||
|
||||
#### **Version 6.4.0**
|
||||
|
||||
> - Features:
|
||||
> - Added OutlineBrush and OutlineThickness dependency properties to BaseConnection to support increasing the selection area without increasing the stroke thickness
|
||||
> - Added IsAnimatingDirectionalArrows and DirectionalArrowsAnimationDuration dependency properties to BaseConnection to support controlling the animation from XAML
|
||||
|
||||
#### **Version 6.3.0**
|
||||
|
||||
> - Features:
|
||||
> - Added a CuttingLine control that removes intersecting connections
|
||||
> - Added CuttingLineStyle, CuttingStartedCommand, CuttingCompletedCommand, IsCutting, EnableCuttingLinePreview and CuttingConnectionTypes to NodifyEditor
|
||||
> - Added EditorGestures.Editor.Cutting and EditorGestures.Editor.CancelAction
|
||||
> - Bugfixes:
|
||||
> - Fixed connection styles not inheriting from the BaseConnection style
|
||||
|
||||
#### **Version 6.2.0**
|
||||
|
||||
> - Features:
|
||||
> - Added a Minimap control and EditorGestures.Minimap
|
||||
> - Added ContentContainerStyle, HeaderContainerStyle and FooterContainerStyle dependency properties to Node
|
||||
> - Added BringIntoView that takes a Rect parameter to NodifyEditor
|
||||
> - Added the NodifyEditor's DataContext as the parameter of the ItemsSelectStartedCommand, ItemsSelectCompletedCommand, ItemsDragStartedCommand and ItemsDragCompletedCommand commands
|
||||
> - Bugfixes:
|
||||
> - Fixed hover effect and padding of NodeInput and NodeOutput for vertical orientation
|
||||
> - Fixed ItemContainers being selected sometimes when double clicking the canvas
|
||||
|
||||
#### **Version 6.1.0**
|
||||
|
||||
> - Features:
|
||||
> - Added new built-in connection type: StepConnection
|
||||
> - Bugfixes:
|
||||
> - Fixed CircuitConnection directional arrows not interpolating correctly
|
||||
> - Fixed BaseConnection SplitEvent and DisconnectEvent not being raised if the corresponding command is null
|
||||
> - Fixed DecoratorContainer scaling with zoom when not referencing a theme in App.xaml
|
||||
> - Fixed style not applying to the default Connection template outside App.xaml
|
||||
|
||||
#### **Version 6.0.0**
|
||||
|
||||
> - Breaking Changes:
|
||||
> - Added a parameter for the orientation to DrawArrowGeometry, DrawDefaultArrowhead, DrawRectangleArrowhead and DrawEllipseArrowhead in BaseConnection
|
||||
> - Added source and target parameters to GetTextPosition in BaseConnection
|
||||
> - EditorGestures is now a singleton instead of a static class (can be inherited to create custom mappings)
|
||||
> - Selection gestures for ItemContainer and GroupingNode are now separated from the NodifyEditor selection gestures
|
||||
> - Renamed EditorGestures.Editor.Zoom to ZoomModifierKey
|
||||
> - Features:
|
||||
> - Added SourceOrientation and TargetOrientation to BaseConnection to support vertical connectors (vertical/mixed connection orientation)
|
||||
> - Added DirectionalArrowsCount to BaseConnection to allow drawing multipe arrows on a connection flowing in the connection direction
|
||||
> - Added DrawDirectionalArrowsGeometry and DrawDirectionalArrowheadGeometry to BaseConnection to allow customizing the directional arrows
|
||||
> - Improved EditorGestures to allow changing input gestures at runtime
|
||||
> - Added new gesture types: AnyGesture, AllGestures, and InputGestureRef
|
||||
> - Added Orientation dependency property to NodeInput and NodeOutput
|
||||
> - Added DirectionalArrowsOffset dependency property to BaseConnection
|
||||
> - Added StartAnimation and StopAnimation methods to BaseConnection
|
||||
> - Bugfixes:
|
||||
> - Fixed BaseConnection.Text not always displaying in the center of the connection
|
||||
> - Fixed a bug where the item container would incorrectly transition to the dragging state on mouse over
|
||||
|
||||
#### **Version 5.2.0**
|
||||
|
||||
> - Features:
|
||||
> - Added Text to BaseConnection, allowing displaying of text on connections
|
||||
> - Added Foreground, FontSize, FontWeight, FontStyle, FontStretch and FontFamily to BaseConnection, allowing styling the displaying text
|
||||
> - Bugfixes:
|
||||
> - Fixed MouseCapture not being released when EnableStickyConnections is enabled and the PendingConnection is canceled by a key gesture
|
||||
|
||||
#### **Version 5.1.0**
|
||||
|
||||
> - Features:
|
||||
> - Added ItemContainer.SelectedBorderThickness dependency property
|
||||
> - Added NodifyEditor.GetLocationInsideEditor
|
||||
> - Bugfixes:
|
||||
> - Fixed PendingConnection.PreviewTarget not being set to null when there is no actual target
|
||||
> - Fixed PendingConnection.PreviewTarget not being set on Connector.PendingConnectionStartedEvent
|
||||
> - Fixed PendingConnection.PreviewTarget not being set to null on Connector.PendingConnectionCompletedEvent
|
||||
> - Fixed connectors panel not being affected by Node.VerticalAlignment
|
||||
> - Changing BorderThickness causes layout shift when selecting an item container
|
||||
> - Fixed the unintentional movement caused by snapping correction
|
||||
|
||||
#### **Version 5.0.2**
|
||||
|
||||
> - Bugfixes:
|
||||
> - Fixed NodeOutput content horizontal alignment
|
||||
> - Fixed Connector not opening Context Menu
|
||||
|
||||
#### **Version 5.0.1**
|
||||
|
||||
> - Bugfixes:
|
||||
> - Returning false from PendingConnection.StartedCommand.CanExecute does not stop the creation of a pending connection
|
||||
> - BaseConnection.ArrowEnds does not display correctly when BaseConnection.Direction is ConnectionDirection.Backward
|
||||
|
||||
#### **Version 5.0.0**
|
||||
|
||||
> - Breaking Changes:
|
||||
> - Removed BaseConnection.GetArrowHeadPoints
|
||||
> - Removed BaseConnection.OffsetMode
|
||||
> - Changed return type of BaseConnection.DrawLineGeometry to support both arrowheads no matter the number of points on the line
|
||||
> - Changed the default for BaseConnection.SourceOffset and BaseConnection.TargetOffset from Size(0, 0) to Size(14, 0)
|
||||
> - Changed the default for BaseConnection.ArrowSize from Size(7, 6) to Size(8, 8)
|
||||
> - Features:
|
||||
> - Added BaseConnection.SourceOffsetMode and BaseConnection.TargetOffsetMode
|
||||
> - Added BaseConnection.ArrowEnds dependency property to allow configurable arrowhead ends
|
||||
> - Added BaseConnection.ArrowShape dependency property to allow configurable arrowhead shape
|
||||
> - Added NodifyEditor.EnableDraggingContainersOptimizations to allow receiving ItemContainer.Location updates in realtime
|
||||
> - Added ConnectionOffsetMode.Static to allow offsetting the source and target points of the connection on the X and the Y axis without revolving around the source or target points
|
||||
|
||||
#### **Version 4.1.0**
|
||||
|
||||
> - Features:
|
||||
> - Added EditorGestures.Selection.DefaultMouseAction to make it easier to change between mouse buttons for selection
|
||||
> - Added EditorGestures.Selection.Cancel gesture to cancel the selection operation reverting to the previous selection
|
||||
> - Added ItemsSelectStartedCommand and ItemsSelectCompletedCommand dependency properties to NodifyEditor for better undo/redo support
|
||||
> - Bugfixes:
|
||||
> - Fixed NodifyEditor.SelectedItems being empty after selection is completed
|
||||
> - Fixed drag canceling when Drag and CancelAction are bound to the same gesture
|
||||
|
||||
#### **Version 4.0.1**
|
||||
|
||||
> - Bugfixes:
|
||||
> - Fixed DisablePanning not working anymore
|
||||
|
||||
#### **Version 4.0.0**
|
||||
|
||||
> - Breaking Changes:
|
||||
> - Removed Selection field from NodifyEditor
|
||||
> - Removed InitialMousePosition, CurrentMousePosition, PreviousMousePosition fields from NodifyEditor
|
||||
> - Removed ItemContainer.DraggableHost (use Editor.ItemsHost instead)
|
||||
> - Made SelectionType required in SelectionHelper
|
||||
> - Moved GroupingNode.SwitchMovementModeModifierKey to EditorGestures.GroupingNode
|
||||
> - Pending connections are now restricted to connect only to Connectors or to NodifyEditors and ItemContainers if PendingConnection.AllowOnlyConnectors is false
|
||||
> - Features:
|
||||
> - Added Connector.EnableStickyConnections to allow completing pending connections in two steps
|
||||
> - Added editor states which can be overriden by inheriting from NodifyEditor and implementing NodifyEditor.GetInitialState()
|
||||
> - EditorState - base class for all editor states
|
||||
> - EditorDefaultState
|
||||
> - EditorSelectingState
|
||||
> - EditorPanningState
|
||||
> - Added container states which can be overriden by inheriting from ItemContainer and implementing ItemContainer.GetInitialState()
|
||||
> - ContainerState - base class for all container states
|
||||
> - ContainerDefaultState
|
||||
> - ContainerDraggingState
|
||||
> - Added MultiGesture utility that can combine multiple input gestures into one gesture
|
||||
> - Added configurable input gestures for NodifyEditor, ItemContainer, Connector, BaseConnection and GroupingNode to EditorGestures
|
||||
> - Added State, PushState, PopState and PopAllStates to NodifyEditor and ItemContainer
|
||||
> - Changed the default AutoPanSpeed to 15 from 10 pixels per tick
|
||||
> - Allow setting ItemContainer.IsPreviewingLocation from derived classes
|
||||
> - Bugfixes:
|
||||
> - Fixed HandleRightClickAfterPanningThreshold not working as expected
|
||||
> - Fixed DisablePanning not disabling auto panning in certain situations
|
||||
> - Fixed GroupingNode selection not working with multiple selection modes
|
||||
> - Fixed PendingConnection connecting cross editors
|
||||
|
||||
#### **Version 3.0.0**
|
||||
|
||||
> - Breaking Changes:
|
||||
> - Changed Decorators from UIElement collection to IEnumerable
|
||||
> - Features:
|
||||
> - Added ItemsExtent and DecoratorsExtent dependency properties to NodifyEditor
|
||||
> - Added DecoratorTemplate dependency property to NodifyEditor
|
||||
> - Added FitToScreenExtentMargin static field to NodifyEditor
|
||||
> - Added Extent dependency property to NodifyCanvas
|
||||
> - Bugfixes:
|
||||
> - Selection rectangle and Decorators are no longer scaled with the viewport zoom
|
||||
> - Fixed connector anchor not updating when container size changed
|
||||
|
||||
#### **Version 2.0.1**
|
||||
|
||||
> - Bugfixes:
|
||||
> - Fixed pending connection default style
|
||||
|
||||
#### **Version 2.0.0**
|
||||
|
||||
> - Breaking Changes:
|
||||
> - Renamed Offset to ViewportLocation in NodifyEditor
|
||||
> - Renamed Scale to ViewportZoom in NodifyEditor
|
||||
> - Renamed MinScale to MinViewportZoom in NodifyEditor
|
||||
> - Renamed MaxScale to MaxViewportZoom in NodifyEditor
|
||||
> - Renamed AppliedTransform to ViewportTransform in NodifyEditor
|
||||
> - Renamed DirectionalConnection to LineConnection
|
||||
> - Removed BringIntoViewAnimationDuration from NodifyEditor
|
||||
> - Removed Viewport dependency property from NodifyEditor
|
||||
> - Removed ActualSize dependency property from StateNode
|
||||
> - Removed Icon dependency property from Node as the icon can _(and should)_ be added in the HeaderTemplate if necessary
|
||||
> - PART_ItemsHost is now required for NodifyEditor to work
|
||||
> - ItemContainers cannot be used outside a NodifyEditor anymore
|
||||
> - ZoomAtPosition now requires graph space coordinates instead of screen space coordinates
|
||||
> - Removed custom value converters
|
||||
> - Made DependencyObjectExtensions internal
|
||||
> - Removed the <http://miroiu.github.io/winfx/xaml/nodify> xaml prefix
|
||||
> - Features:
|
||||
> - Added ResizeStartedEvent routed event to GroupingNode
|
||||
> - Added ViewportSize - **OneWayToSource** dependency property to NodifyEditor
|
||||
> - Added ActualSize - **OneWayToSource** dependency property to ItemContainer
|
||||
> - Added DecoratorContainer and DecoratorContainerStyle dependency properties to NodifyEditor
|
||||
> - Added RemoveConnectionCommand command to NodifyEditor
|
||||
> - Added DisconnectCommand and SplitCommand commands to BaseConnection
|
||||
> - Added ContentBrush dependency property to NodifyEditor
|
||||
> - Added HasFooter dependency property to Node
|
||||
> - Added FitToScreen command to NodifyEditor and EditorCommands
|
||||
> - Added onFinish callback to BringIntoView in NodifyEditor
|
||||
> - Added ArrowSize and Spacing dependency properties to all connections inheriting from BaseConnection
|
||||
> - Added BringIntoViewMaxDuration dependency property to NodifyEditor
|
||||
> - Added BringIntoViewSpeed dependency property to NodifyEditor
|
||||
> - Auto panning speed now scales with the zoom factor
|
||||
> - Bugfixes:
|
||||
> - Every public property or method should work with graph space coordinates
|
||||
> - Disable auto panning when panning is disabled
|
||||
> - Min zoom could be set to a very small value
|
||||
> - Bring into view was not disabling all interfering operations
|
||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at miroiu.emanuel@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
26
CONTRIBUTING.md
Normal file
26
CONTRIBUTING.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 👋 **Welcome to Nodify!** 👋
|
||||
|
||||
👍🎉 First off, thanks for taking the time to contribute! Your contributions help make Nodify better for everyone. 👍🎉
|
||||
|
||||
If you find Nodify useful, please consider giving us a ⭐ **star** ⭐ on our GitHub repository!
|
||||
|
||||
Code of Conduct: By contributing to Nodify, you agree to uphold our [Code of Conduct](CODE_OF_CONDUCT.md). We expect all contributors to be respectful and inclusive. (Don't worry, it's all common sense 😎)
|
||||
|
||||
## How you can contribute
|
||||
|
||||
- ❓ [Ask a question](https://github.com/miroiu/nodify/issues/new?assignees=miroiu&labels=question&template=ask-a-question.md&title=%5BQuestion%5D) - If you're unsure about anything related to Nodify, feel free to ask! No question is too small.
|
||||
- 🐛 [Create a bug report](https://github.com/miroiu/nodify/issues/new?assignees=miroiu&labels=bug&template=bug_report.md&title=%5BBug%5D) - Noticed something not working as expected? Let us know by creating a bug report. Please provide as much detail as possible to help us address the issue.
|
||||
- 🌺 [Suggest an enhancement](https://github.com/miroiu/nodify/issues/new?assignees=miroiu&labels=enhancement&template=feature_request.md&title=%5BFeature%5D) - Have an idea to make Nodify even better? We'd love to hear it! Share your suggestions for new features or improvements.
|
||||
- ✨ [Explore example applications](https://github.com/miroiu/nodify/tree/master/Examples) - Check out the example applications provided with Nodify. They're great for learning how to use the library in different scenarios.
|
||||
- 🎉 [Showcase your application](https://github.com/miroiu/nodify/issues/56) - Built something cool with Nodify? Share it with the community! We'd love to see what you've created.
|
||||
- 📝 [Help with the documentation](https://github.com/miroiu/nodify/wiki) - Documentation is crucial for making Nodify accessible to everyone. If you spot errors or have suggestions for improvement, please let us know or update the docs yourself!
|
||||
- 🔧 [Fix a bug](https://github.com/miroiu/nodify/labels/bug) - If you're a developer, you can contribute by fixing bugs in Nodify. Simply locate an open issue tagged as a bug and submit a pull request with your fix.
|
||||
- 🔗 [Create a pull request linking to a feature](https://github.com/miroiu/nodify/labels/enhancement) - Implemented a new feature or enhancement? Fantastic! Submit a pull request linking to the relevant feature or enhancement issue.
|
||||
|
||||
## Some tips
|
||||
|
||||
- Write clear and descriptive issues and try to avoid duplication
|
||||
- If you find a **Closed** issue that relates to yours, open a new issue and include a link to the original issue in the body of your new one.
|
||||
- The easiest way to update documentation is to navigate [to the docs website](https://github.com/miroiu/nodify/wiki) and click 'Edit this page' which is found at the top right of any page.
|
||||
- If you want to create an example application that others can use to learn from, then [create an issue](https://github.com/miroiu/nodify/issues/new?assignees=miroiu&labels=application&template=add_example_app.md&title=%5BApplication%5D) describing what your application is doing and if you need help with anything.
|
||||
- The application you showcase can use any license.
|
||||
BIN
Examples/Nodify.Calculator.7z
Normal file
BIN
Examples/Nodify.Calculator.7z
Normal file
Binary file not shown.
24
Examples/Nodify.Calculator/APIOperationViewModel.cs
Normal file
24
Examples/Nodify.Calculator/APIOperationViewModel.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class APIOperationViewModel : OperationViewModel
|
||||
{
|
||||
public APIOperationViewModel()
|
||||
{
|
||||
_operationType = "GET";
|
||||
}
|
||||
|
||||
private string _operationType;
|
||||
|
||||
public string OperationType
|
||||
{
|
||||
get => _operationType;
|
||||
set => SetProperty(ref _operationType, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Examples/Nodify.Calculator/AddVariableDialog.xaml
Normal file
52
Examples/Nodify.Calculator/AddVariableDialog.xaml
Normal file
@@ -0,0 +1,52 @@
|
||||
<Window x:Class="Nodify.Calculator.AddVariableDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Add New Variable"
|
||||
Width="350"
|
||||
Height="280"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="NoResize"
|
||||
Background="#2D2D30"
|
||||
Foreground="White">
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="Variable Name:" Grid.Row="0" Margin="0 0 0 4" />
|
||||
<TextBox x:Name="VariableNameBox" Grid.Row="1" Margin="0 0 0 10"
|
||||
Background="#3E3E42" Foreground="White" Padding="4" />
|
||||
|
||||
<TextBlock Text="Variable Type:" Grid.Row="2" Margin="0 0 0 4" />
|
||||
<ComboBox x:Name="VariableTypeBox" Grid.Row="3" Margin="0 0 0 10"
|
||||
Background="#3E3E42" Foreground="White" SelectedIndex="0">
|
||||
<ComboBoxItem Content="string" />
|
||||
<ComboBoxItem Content="int" />
|
||||
<ComboBoxItem Content="double" />
|
||||
<ComboBoxItem Content="bool" />
|
||||
<ComboBoxItem Content="float" />
|
||||
<ComboBoxItem Content="decimal" />
|
||||
<ComboBoxItem Content="long" />
|
||||
<ComboBoxItem Content="DateTime" />
|
||||
<ComboBoxItem Content="object" />
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="Default Value (optional):" Grid.Row="4" Margin="0 0 0 4" />
|
||||
<TextBox x:Name="DefaultValueBox" Grid.Row="5" Margin="0 0 0 10"
|
||||
Background="#3E3E42" Foreground="White" Padding="4" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="7">
|
||||
<Button Content="Add" Width="80" Margin="0 0 10 0" Padding="4"
|
||||
Click="OnAddClick" IsDefault="True" />
|
||||
<Button Content="Cancel" Width="80" Padding="4"
|
||||
Click="OnCancelClick" IsCancel="True" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
36
Examples/Nodify.Calculator/AddVariableDialog.xaml.cs
Normal file
36
Examples/Nodify.Calculator/AddVariableDialog.xaml.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public partial class AddVariableDialog : Window
|
||||
{
|
||||
public string VariableName { get; private set; } = string.Empty;
|
||||
public string VariableType { get; private set; } = "string";
|
||||
public string DefaultValue { get; private set; } = string.Empty;
|
||||
|
||||
public AddVariableDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnAddClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(VariableNameBox.Text))
|
||||
{
|
||||
MessageBox.Show("Please enter a variable name.", "Validation", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
VariableName = VariableNameBox.Text.Trim();
|
||||
VariableType = ((ComboBoxItem)VariableTypeBox.SelectedItem).Content.ToString()!;
|
||||
DefaultValue = DefaultValueBox.Text.Trim();
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
private void OnCancelClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Examples/Nodify.Calculator/App.xaml
Normal file
44
Examples/Nodify.Calculator/App.xaml
Normal file
@@ -0,0 +1,44 @@
|
||||
<Application x:Class="Nodify.Calculator.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:nodify="https://miroiu.github.io/nodify"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/Dark.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/FocusVisual.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Icons.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Dark.xaml" />
|
||||
|
||||
<ResourceDictionary>
|
||||
|
||||
<Style x:Key="{x:Static SystemParameters.FocusVisualStyleKey}">
|
||||
<Setter Property="Control.Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate>
|
||||
<Rectangle StrokeThickness="1"
|
||||
StrokeDashArray="2"
|
||||
Margin="-2"
|
||||
RadiusX="3"
|
||||
RadiusY="3"
|
||||
Stroke="DodgerBlue" />
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!--WPF is wonderful-->
|
||||
<Style x:Key="OriginalNodeInputStyle"
|
||||
TargetType="{x:Type nodify:NodeInput}"
|
||||
BasedOn="{StaticResource {x:Type nodify:NodeInput}}" />
|
||||
|
||||
<Style x:Key="OriginalNodeOutputStyle"
|
||||
TargetType="{x:Type nodify:NodeOutput}"
|
||||
BasedOn="{StaticResource {x:Type nodify:NodeOutput}}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
17
Examples/Nodify.Calculator/App.xaml.cs
Normal file
17
Examples/Nodify.Calculator/App.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
}
|
||||
142
Examples/Nodify.Calculator/ApplicationViewModel.cs
Normal file
142
Examples/Nodify.Calculator/ApplicationViewModel.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using Newtonsoft.Json;
|
||||
using Nodify.Calculator.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class ApplicationViewModel : ObservableObject
|
||||
{
|
||||
public NodifyObservableCollection<EditorViewModel> Editors { get; } = new NodifyObservableCollection<EditorViewModel>();
|
||||
|
||||
public ApplicationViewModel()
|
||||
{
|
||||
AddEditorCommand = new DelegateCommand(() => Editors.Add(new EditorViewModel
|
||||
{
|
||||
Name = $"Editor {Editors.Count + 1}"
|
||||
}));
|
||||
CloseEditorCommand = new DelegateCommand<Guid>(
|
||||
id => Editors.RemoveOne(editor => editor.Id == id),
|
||||
_ => Editors.Count > 0 && SelectedEditor != null);
|
||||
RunFlowCommand = new DelegateCommand(() =>
|
||||
{
|
||||
Executor ex = new Executor(Editors.First());
|
||||
var result = ex.PerformPreCheck();
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
MessageBox.Show("Error occured while executing the request : " + result);
|
||||
}
|
||||
else
|
||||
{
|
||||
FlowRunner runner = new FlowRunner(ex);
|
||||
runner.ShowDialog();
|
||||
}
|
||||
});
|
||||
SaveFileCommand = new DelegateCommand(() =>
|
||||
{
|
||||
var firstEditor = Editors.First();
|
||||
var allNodes = firstEditor.Calculator.Operations;
|
||||
|
||||
SaveGraphModel svm = new SaveGraphModel();
|
||||
foreach (var item in allNodes)
|
||||
{
|
||||
SaveNodes svn = new SaveNodes()
|
||||
{
|
||||
Location = item.Location,
|
||||
};
|
||||
svm.Nodes.Add(svn);
|
||||
}
|
||||
|
||||
|
||||
var jsonEditors = JsonConvert.SerializeObject(svm, new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||||
});
|
||||
//var jsonEditors = JsonConvert.SerializeObject(Editors);
|
||||
File.WriteAllText("SaveFile.AEXN", jsonEditors);
|
||||
});
|
||||
OpenFileCommand = new DelegateCommand(() =>
|
||||
{
|
||||
if (File.Exists("SaveFile.AEXN"))
|
||||
{
|
||||
var allText = File.ReadAllText("SaveFile.AEXN");
|
||||
var edts = JsonConvert.DeserializeObject<SaveGraphModel>(allText);
|
||||
var firstEditor = Editors.First();
|
||||
var nodesToAdd = edts.Nodes;
|
||||
foreach (var item in nodesToAdd)
|
||||
{
|
||||
firstEditor.Calculator.Operations.Add(new()
|
||||
{
|
||||
Location = item.Location,
|
||||
NodeId = "H",
|
||||
Title = "HHA"
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
Editors.WhenAdded((editor) =>
|
||||
{
|
||||
if (AutoSelectNewEditor || Editors.Count == 1)
|
||||
{
|
||||
SelectedEditor = editor;
|
||||
}
|
||||
editor.OnOpenInnerCalculator += OnOpenInnerCalculator;
|
||||
})
|
||||
.WhenRemoved((editor) =>
|
||||
{
|
||||
editor.OnOpenInnerCalculator -= OnOpenInnerCalculator;
|
||||
var childEditors = Editors.Where(ed => ed.Parent == editor).ToList();
|
||||
childEditors.ForEach(ed => Editors.Remove(ed));
|
||||
});
|
||||
Editors.Add(new EditorViewModel
|
||||
{
|
||||
Name = $"Editor {Editors.Count + 1}"
|
||||
});
|
||||
}
|
||||
|
||||
private void OnOpenInnerCalculator(EditorViewModel parentEditor, CalculatorViewModel calculator)
|
||||
{
|
||||
var editor = Editors.FirstOrDefault(e => e.Calculator == calculator);
|
||||
if (editor != null)
|
||||
{
|
||||
SelectedEditor = editor;
|
||||
}
|
||||
else
|
||||
{
|
||||
var childEditor = new EditorViewModel
|
||||
{
|
||||
Parent = parentEditor,
|
||||
Calculator = calculator,
|
||||
Name = $"[Inner] Editor {Editors.Count + 1}"
|
||||
};
|
||||
Editors.Add(childEditor);
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand AddEditorCommand { get; }
|
||||
public ICommand CloseEditorCommand { get; }
|
||||
public ICommand RunFlowCommand { get; }
|
||||
public ICommand SaveFileCommand { get; }
|
||||
public ICommand OpenFileCommand { get; }
|
||||
|
||||
private EditorViewModel? _selectedEditor;
|
||||
public EditorViewModel? SelectedEditor
|
||||
{
|
||||
get => _selectedEditor;
|
||||
set => SetProperty(ref _selectedEditor, value);
|
||||
}
|
||||
|
||||
private bool _autoSelectNewEditor = true;
|
||||
public bool AutoSelectNewEditor
|
||||
{
|
||||
get => _autoSelectNewEditor;
|
||||
set => SetProperty(ref _autoSelectNewEditor, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Examples/Nodify.Calculator/AssemblyInfo.cs
Normal file
10
Examples/Nodify.Calculator/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
25
Examples/Nodify.Calculator/AuthOperationViewModel.cs
Normal file
25
Examples/Nodify.Calculator/AuthOperationViewModel.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class AuthOperationViewModel : SystemOperationViewModel
|
||||
{
|
||||
public AuthOperationViewModel()
|
||||
{
|
||||
Title = "Auth";
|
||||
SystemOperationType = SystemOperations.AUTH;
|
||||
}
|
||||
|
||||
private string _baseUrl = string.Empty;
|
||||
public string BaseUrl
|
||||
{
|
||||
get => _baseUrl;
|
||||
set => SetProperty(ref _baseUrl, value);
|
||||
}
|
||||
|
||||
private string _authType = "Bearer Token";
|
||||
public string AuthType
|
||||
{
|
||||
get => _authType;
|
||||
set => SetProperty(ref _authType, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
178
Examples/Nodify.Calculator/BinarySerializationHelper.cs
Normal file
178
Examples/Nodify.Calculator/BinarySerializationHelper.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
public static class BinarySerializationHelper
|
||||
{
|
||||
public static void SerializeObject<T>(T obj, BinaryWriter writer)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
writer.Write(false); // Null flag
|
||||
return;
|
||||
}
|
||||
|
||||
writer.Write(true); // Not null
|
||||
|
||||
Type type = typeof(T);
|
||||
|
||||
// Serialize all non-indexed properties
|
||||
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (property.CanRead && property.GetIndexParameters().Length == 0) // Skip indexers
|
||||
{
|
||||
var value = property.GetValue(obj);
|
||||
SerializeValue(value, writer);
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize all public fields
|
||||
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
var value = field.GetValue(obj);
|
||||
SerializeValue(value, writer);
|
||||
}
|
||||
}
|
||||
|
||||
public static T DeserializeObject<T>(BinaryReader reader) where T : new()
|
||||
{
|
||||
if (!reader.ReadBoolean()) // Null flag
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
T obj = new T();
|
||||
Type type = typeof(T);
|
||||
|
||||
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (property.CanWrite && property.GetIndexParameters().Length == 0) // Skip indexers
|
||||
{
|
||||
var value = DeserializeValue(property.PropertyType, reader);
|
||||
property.SetValue(obj, value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
var value = DeserializeValue(field.FieldType, reader);
|
||||
field.SetValue(obj, value);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static void SerializeValue(object value, BinaryWriter writer)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.Write(false); // Null flag for the value
|
||||
return;
|
||||
}
|
||||
|
||||
writer.Write(true); // Not null!
|
||||
|
||||
Type type = value.GetType();
|
||||
|
||||
if (type == typeof(int))
|
||||
{
|
||||
writer.Write((int)value);
|
||||
}
|
||||
else if (type == typeof(string))
|
||||
{
|
||||
writer.Write((string)value);
|
||||
}
|
||||
else if (type == typeof(bool))
|
||||
{
|
||||
writer.Write((bool)value);
|
||||
}
|
||||
else if (type == typeof(double))
|
||||
{
|
||||
writer.Write((double)value);
|
||||
}
|
||||
else if (type == typeof(float))
|
||||
{
|
||||
writer.Write((float)value);
|
||||
}
|
||||
else if (type == typeof(long))
|
||||
{
|
||||
writer.Write((long)value);
|
||||
}
|
||||
else if (type.IsArray)
|
||||
{
|
||||
var array = (Array)value;
|
||||
writer.Write(array.Length); // Write array length
|
||||
foreach (var item in array)
|
||||
{
|
||||
SerializeValue(item, writer);
|
||||
}
|
||||
}
|
||||
else if (typeof(System.Collections.IList).IsAssignableFrom(type))
|
||||
{
|
||||
var list = (System.Collections.IList)value;
|
||||
writer.Write(list.Count); // Write list count
|
||||
foreach (var item in list)
|
||||
{
|
||||
SerializeValue(item, writer); // Serialize each item
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SerializeObject(value, writer); // Serialize nested objects
|
||||
}
|
||||
}
|
||||
|
||||
private static object DeserializeValue(Type type, BinaryReader reader)
|
||||
{
|
||||
if (!reader.ReadBoolean()) // Null flag
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type == typeof(int))
|
||||
{
|
||||
return reader.ReadInt32();
|
||||
}
|
||||
else if (type == typeof(string))
|
||||
{
|
||||
return reader.ReadString();
|
||||
}
|
||||
else if (type == typeof(bool))
|
||||
{
|
||||
return reader.ReadBoolean();
|
||||
}
|
||||
else if (type == typeof(double))
|
||||
{
|
||||
return reader.ReadDouble();
|
||||
}
|
||||
else if (type.IsArray)
|
||||
{
|
||||
int length = reader.ReadInt32();
|
||||
var array = Array.CreateInstance(type.GetElementType(), length);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
array.SetValue(DeserializeValue(type.GetElementType(), reader), i);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
else if (typeof(System.Collections.IList).IsAssignableFrom(type))
|
||||
{
|
||||
int count = reader.ReadInt32();
|
||||
var list = (System.Collections.IList)Activator.CreateInstance(type);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
list.Add(DeserializeValue(type.GenericTypeArguments[0], reader));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
else
|
||||
{
|
||||
var method = typeof(BinarySerializationHelper)
|
||||
.GetMethod("DeserializeObject")
|
||||
.MakeGenericMethod(type);
|
||||
|
||||
return method.Invoke(null, new object[] { reader });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class CalculatorInputOperationViewModel : OperationViewModel
|
||||
{
|
||||
public CalculatorInputOperationViewModel()
|
||||
{
|
||||
AddOutputCommand = new RequeryCommand(
|
||||
() => Output.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = $"In {Output.Count}"
|
||||
}),
|
||||
() => Output.Count < 10);
|
||||
|
||||
RemoveOutputCommand = new RequeryCommand(
|
||||
() => Output.RemoveAt(Output.Count - 1),
|
||||
() => Output.Count > 1);
|
||||
|
||||
Output.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = $"In {Output.Count}"
|
||||
});
|
||||
}
|
||||
|
||||
public new NodifyObservableCollection<ConnectorViewModel> Output { get; set; } =
|
||||
new NodifyObservableCollection<ConnectorViewModel>();
|
||||
|
||||
public INodifyCommand AddOutputCommand { get; }
|
||||
public INodifyCommand RemoveOutputCommand { get; }
|
||||
}
|
||||
}
|
||||
51
Examples/Nodify.Calculator/CalculatorOperationViewModel.cs
Normal file
51
Examples/Nodify.Calculator/CalculatorOperationViewModel.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class CalculatorOperationViewModel : OperationViewModel
|
||||
{
|
||||
public CalculatorViewModel InnerCalculator { get; } = new CalculatorViewModel();
|
||||
|
||||
private OperationViewModel InnerOutput { get; } = new OperationViewModel
|
||||
{
|
||||
Title = "Output Parameters",
|
||||
Input = { new ConnectorViewModel() },
|
||||
Location = new Point(500, 300),
|
||||
IsReadOnly = true
|
||||
};
|
||||
|
||||
private CalculatorInputOperationViewModel InnerInput { get; } = new CalculatorInputOperationViewModel
|
||||
{
|
||||
Title = "Input Parameters",
|
||||
Location = new Point(300, 300),
|
||||
IsReadOnly = true
|
||||
};
|
||||
|
||||
public CalculatorOperationViewModel()
|
||||
{
|
||||
InnerCalculator.Operations.Add(InnerInput);
|
||||
InnerCalculator.Operations.Add(InnerOutput);
|
||||
|
||||
|
||||
InnerInput.Output.ForEach(x => Input.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = x.Title
|
||||
}));
|
||||
|
||||
InnerInput.Output
|
||||
.WhenAdded(x => Input.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = x.Title
|
||||
}))
|
||||
.WhenRemoved(x => Input.RemoveOne(i => i.Title == x.Title));
|
||||
}
|
||||
|
||||
protected override void OnInputValueChanged()
|
||||
{
|
||||
for (var i = 0; i < Input.Count; i++)
|
||||
{
|
||||
InnerInput.Output[i].Value = Input[i].Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
184
Examples/Nodify.Calculator/CalculatorViewModel.cs
Normal file
184
Examples/Nodify.Calculator/CalculatorViewModel.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class CalculatorViewModel : ObservableObject
|
||||
{
|
||||
public CalculatorViewModel()
|
||||
{
|
||||
CreateConnectionCommand = new DelegateCommand<ConnectorViewModel>(
|
||||
_ => CreateConnection(PendingConnection.Source, PendingConnection.Target),
|
||||
_ => CanCreateConnection(PendingConnection.Source, PendingConnection.Target));
|
||||
StartConnectionCommand = new DelegateCommand<ConnectorViewModel>(_ => PendingConnection.IsVisible = true, (c) => c != null && !(c.IsConnected && c.IsInput));
|
||||
DisconnectConnectorCommand = new DelegateCommand<ConnectorViewModel>(DisconnectConnector);
|
||||
DeleteSelectionCommand = new DelegateCommand(DeleteSelection);
|
||||
GroupSelectionCommand = new DelegateCommand(GroupSelectedOperations, () => SelectedOperations.Count > 0);
|
||||
|
||||
Connections.WhenAdded(c =>
|
||||
{
|
||||
c.Input.IsConnected = true;
|
||||
c.Output.IsConnected = true;
|
||||
|
||||
c.Input.Value = c.Output.Value;
|
||||
|
||||
c.Output.ValueObservers.Add(c.Input);
|
||||
})
|
||||
.WhenRemoved(c =>
|
||||
{
|
||||
var ic = Connections.Count(con => con.Input == c.Input || con.Output == c.Input);
|
||||
var oc = Connections.Count(con => con.Input == c.Output || con.Output == c.Output);
|
||||
|
||||
if (ic == 0)
|
||||
{
|
||||
c.Input.IsConnected = false;
|
||||
}
|
||||
|
||||
if (oc == 0)
|
||||
{
|
||||
c.Output.IsConnected = false;
|
||||
}
|
||||
|
||||
c.Output.ValueObservers.Remove(c.Input);
|
||||
});
|
||||
|
||||
Operations.WhenAdded(x =>
|
||||
{
|
||||
x.Input.WhenRemoved(RemoveConnection);
|
||||
x.NodeId = (Operations.Count + 1).ToString();
|
||||
Debug.WriteLine($"Currently adding the node with node id : {x.NodeId} , Title : {x.Title}");
|
||||
if (x is CalculatorInputOperationViewModel ci)
|
||||
{
|
||||
ci.Output.WhenRemoved(RemoveConnection);
|
||||
}
|
||||
|
||||
void RemoveConnection(ConnectorViewModel i)
|
||||
{
|
||||
var c = Connections.Where(con => con.Input == i || con.Output == i).ToArray();
|
||||
c.ForEach(con => Connections.Remove(con));
|
||||
}
|
||||
})
|
||||
.WhenRemoved(x =>
|
||||
{
|
||||
foreach (var input in x.Input)
|
||||
{
|
||||
DisconnectConnector(input);
|
||||
}
|
||||
|
||||
foreach (var item in x.Output)
|
||||
{
|
||||
DisconnectConnector(item);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
OperationsMenu = new OperationsMenuViewModel(this);
|
||||
}
|
||||
|
||||
private NodifyObservableCollection<OperationViewModel> _operations = new NodifyObservableCollection<OperationViewModel>();
|
||||
public NodifyObservableCollection<OperationViewModel> Operations
|
||||
{
|
||||
get => _operations;
|
||||
set => SetProperty(ref _operations, value);
|
||||
}
|
||||
|
||||
private NodifyObservableCollection<OperationViewModel> _selectedOperations = new NodifyObservableCollection<OperationViewModel>();
|
||||
public NodifyObservableCollection<OperationViewModel> SelectedOperations
|
||||
{
|
||||
get => _selectedOperations;
|
||||
set => SetProperty(ref _selectedOperations, value);
|
||||
}
|
||||
|
||||
public NodifyObservableCollection<ConnectionViewModel> Connections { get; } = new NodifyObservableCollection<ConnectionViewModel>();
|
||||
public PendingConnectionViewModel PendingConnection { get; set; } = new PendingConnectionViewModel();
|
||||
public OperationsMenuViewModel OperationsMenu { get; set; }
|
||||
|
||||
public INodifyCommand StartConnectionCommand { get; }
|
||||
public INodifyCommand CreateConnectionCommand { get; }
|
||||
public INodifyCommand DisconnectConnectorCommand { get; }
|
||||
public INodifyCommand DeleteSelectionCommand { get; }
|
||||
public INodifyCommand GroupSelectionCommand { get; }
|
||||
|
||||
private void DisconnectConnector(ConnectorViewModel connector)
|
||||
{
|
||||
var connections = Connections.Where(c => c.Input == connector || c.Output == connector).ToList();
|
||||
connections.ForEach(c => Connections.Remove(c));
|
||||
}
|
||||
|
||||
internal bool CanCreateConnection(ConnectorViewModel source, ConnectorViewModel? target)
|
||||
=> target == null || (source != target &&
|
||||
source.Shape == target.Shape &&
|
||||
!source.IsConnected &&
|
||||
!target.IsConnected &&
|
||||
source.IsInput != target.IsInput);
|
||||
|
||||
internal void OpenGetSetVariable(Point TargetLocation, string className)
|
||||
{
|
||||
OperationsMenu.OpenOnlyGetSetVariable(TargetLocation, className);
|
||||
OperationsMenu.Closed += OnOperationsMenuClosed;
|
||||
}
|
||||
|
||||
internal void OpenGetSetForVariable(Point targetLocation, OperationInfoViewModel variableInfo)
|
||||
{
|
||||
OperationsMenu.OpenGetSetForVariable(targetLocation, variableInfo);
|
||||
OperationsMenu.Closed += OnOperationsMenuClosed;
|
||||
}
|
||||
|
||||
internal void CreateConnection(ConnectorViewModel source, ConnectorViewModel? target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
PendingConnection.IsVisible = true;
|
||||
OperationsMenu.OpenAt(PendingConnection.TargetLocation);
|
||||
OperationsMenu.Closed += OnOperationsMenuClosed;
|
||||
return;
|
||||
}
|
||||
|
||||
var input = source.IsInput ? source : target;
|
||||
var output = target.IsInput ? source : target;
|
||||
|
||||
PendingConnection.IsVisible = false;
|
||||
|
||||
DisconnectConnector(input);
|
||||
|
||||
Connections.Add(new ConnectionViewModel
|
||||
{
|
||||
Input = input,
|
||||
Output = output,
|
||||
InputNodeId = source.Operation.NodeId,
|
||||
OutputNodeId = target.Operation.NodeId
|
||||
|
||||
});
|
||||
|
||||
Debug.WriteLine($"Creating a connection between input node id: {source.Operation.NodeId} and :{target.Operation.NodeId}");
|
||||
}
|
||||
|
||||
private void OnOperationsMenuClosed()
|
||||
{
|
||||
PendingConnection.IsVisible = false;
|
||||
OperationsMenu.Closed -= OnOperationsMenuClosed;
|
||||
}
|
||||
|
||||
private void DeleteSelection()
|
||||
{
|
||||
var selected = SelectedOperations.ToList();
|
||||
selected.ForEach(o => Operations.Remove(o));
|
||||
}
|
||||
|
||||
private void GroupSelectedOperations()
|
||||
{
|
||||
var selected = SelectedOperations.ToList();
|
||||
var bounding = selected.GetBoundingBox(50);
|
||||
|
||||
Operations.Add(new OperationGroupViewModel
|
||||
{
|
||||
Title = "Operations",
|
||||
Location = bounding.Location,
|
||||
GroupSize = new Size(bounding.Width, bounding.Height)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
Examples/Nodify.Calculator/ColorToSolidBrushConverter.cs
Normal file
27
Examples/Nodify.Calculator/ColorToSolidBrushConverter.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
internal static class ColorToSolidBrushConverter
|
||||
{
|
||||
public static Brush Convert(Color value)
|
||||
{
|
||||
var brush = new SolidColorBrush(value);
|
||||
return brush;
|
||||
}
|
||||
|
||||
public static Color ConvertBack(Brush value)
|
||||
{
|
||||
if (value is SolidColorBrush brush)
|
||||
return brush.Color;
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Examples/Nodify.Calculator/ConnectionViewModel.cs
Normal file
36
Examples/Nodify.Calculator/ConnectionViewModel.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class ConnectionViewModel : ObservableObject
|
||||
{
|
||||
private ConnectorViewModel _input = default!;
|
||||
public ConnectorViewModel Input
|
||||
{
|
||||
get => _input;
|
||||
set => SetProperty(ref _input, value);
|
||||
}
|
||||
|
||||
private ConnectorViewModel _output = default!;
|
||||
public ConnectorViewModel Output
|
||||
{
|
||||
get => _output;
|
||||
set => SetProperty(ref _output, value);
|
||||
}
|
||||
|
||||
private string _inputNodeId;
|
||||
|
||||
public string InputNodeId
|
||||
{
|
||||
get { return _inputNodeId; }
|
||||
set { _inputNodeId = value; }
|
||||
}
|
||||
|
||||
private string _outputNodeId;
|
||||
|
||||
public string OutputNodeId
|
||||
{
|
||||
get { return _outputNodeId; }
|
||||
set { _outputNodeId = value; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
100
Examples/Nodify.Calculator/ConnectorViewModel.cs
Normal file
100
Examples/Nodify.Calculator/ConnectorViewModel.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using LiteDB;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public enum ConnectorShape
|
||||
{
|
||||
Circle,
|
||||
Triangle,
|
||||
Square,
|
||||
}
|
||||
|
||||
public class ConnectorViewModel : ObservableObject
|
||||
{
|
||||
private string? _title;
|
||||
public string? Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
|
||||
private double _value;
|
||||
public double Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value)
|
||||
.Then(() => ValueObservers.ForEach(o => o.Value = value));
|
||||
}
|
||||
|
||||
private ConnectorShape _shape;
|
||||
public ConnectorShape Shape
|
||||
{
|
||||
get => _shape;
|
||||
set => SetProperty(ref _shape, value);
|
||||
}
|
||||
|
||||
private bool _isConnected;
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set => SetProperty(ref _isConnected, value);
|
||||
}
|
||||
|
||||
private bool _isInput;
|
||||
public bool IsInput
|
||||
{
|
||||
get => _isInput;
|
||||
set => SetProperty(ref _isInput, value);
|
||||
}
|
||||
|
||||
private Point _anchor;
|
||||
public Point Anchor
|
||||
{
|
||||
get => _anchor;
|
||||
set => SetProperty(ref _anchor, value);
|
||||
}
|
||||
|
||||
private System.Drawing.Color _color = System.Drawing.Color.DodgerBlue;
|
||||
public System.Drawing.Color ConnectorColor
|
||||
{
|
||||
set
|
||||
{
|
||||
_color = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Brush Color
|
||||
{
|
||||
get
|
||||
{
|
||||
var mediacolor = System.Windows.Media.Color.FromArgb
|
||||
(
|
||||
_color.A,
|
||||
_color.R,
|
||||
_color.G,
|
||||
_color.B
|
||||
);
|
||||
return ColorToSolidBrushConverter.Convert(mediacolor);
|
||||
}
|
||||
}
|
||||
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[BsonIgnore]
|
||||
private OperationViewModel _operation = default!;
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[BsonIgnore]
|
||||
public OperationViewModel Operation
|
||||
{
|
||||
get => _operation;
|
||||
set => SetProperty(ref _operation, value);
|
||||
}
|
||||
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[BsonIgnore]
|
||||
public List<ConnectorViewModel> ValueObservers { get; } = new List<ConnectorViewModel>();
|
||||
}
|
||||
}
|
||||
31
Examples/Nodify.Calculator/Converters/ItemToListConverter.cs
Normal file
31
Examples/Nodify.Calculator/Converters/ItemToListConverter.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class ItemToListConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
var argType = value.GetType();
|
||||
var listType = typeof(List<>).MakeGenericType(argType);
|
||||
var list = Activator.CreateInstance(listType) as IList;
|
||||
list?.Add(value);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Examples/Nodify.Calculator/CreateOperationInfoViewModel.cs
Normal file
16
Examples/Nodify.Calculator/CreateOperationInfoViewModel.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class CreateOperationInfoViewModel
|
||||
{
|
||||
public CreateOperationInfoViewModel(OperationInfoViewModel info, Point location)
|
||||
{
|
||||
Info = info;
|
||||
Location = location;
|
||||
}
|
||||
|
||||
public OperationInfoViewModel Info { get; }
|
||||
public Point Location { get; }
|
||||
}
|
||||
}
|
||||
699
Examples/Nodify.Calculator/EditorView.xaml
Normal file
699
Examples/Nodify.Calculator/EditorView.xaml
Normal file
@@ -0,0 +1,699 @@
|
||||
<UserControl x:Class="Nodify.Calculator.EditorView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Nodify.Calculator"
|
||||
xmlns:nodify="https://miroiu.github.io/nodify"
|
||||
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
|
||||
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
|
||||
d:DataContext="{d:DesignInstance Type=local:EditorViewModel}"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<GeometryDrawing x:Key="SmallGridGeometry"
|
||||
Geometry="M0,0 L0,1 0.03,1 0.03,0.03 1,0.03 1,0 Z"
|
||||
Brush="{DynamicResource GridLinesBrush}" />
|
||||
|
||||
<GeometryDrawing x:Key="LargeGridGeometry"
|
||||
Geometry="M0,0 L0,1 0.015,1 0.015,0.015 1,0.015 1,0 Z"
|
||||
Brush="{DynamicResource GridLinesBrush}" />
|
||||
|
||||
<DrawingBrush x:Key="SmallGridLinesDrawingBrush"
|
||||
TileMode="Tile"
|
||||
ViewportUnits="Absolute"
|
||||
Viewport="0 0 15 15"
|
||||
Transform="{Binding ViewportTransform, ElementName=Editor}"
|
||||
Drawing="{StaticResource SmallGridGeometry}" />
|
||||
|
||||
<DrawingBrush x:Key="LargeGridLinesDrawingBrush"
|
||||
TileMode="Tile"
|
||||
ViewportUnits="Absolute"
|
||||
Opacity="0.5"
|
||||
Viewport="0 0 150 150"
|
||||
Transform="{Binding ViewportTransform, ElementName=Editor}"
|
||||
Drawing="{StaticResource LargeGridGeometry}" />
|
||||
|
||||
<LinearGradientBrush x:Key="AnimatedBrush"
|
||||
StartPoint="0 0"
|
||||
EndPoint="1 0">
|
||||
<GradientStop Color="#6366f1"
|
||||
Offset="0" />
|
||||
<GradientStop Color="#a855f7"
|
||||
Offset="0.5" />
|
||||
<GradientStop Color="#ec4899"
|
||||
Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
<Border x:Key="AnimatedBorderPlaceholder"
|
||||
BorderBrush="{StaticResource AnimatedBrush}" />
|
||||
|
||||
<Storyboard x:Key="AnimateBorder"
|
||||
RepeatBehavior="Forever">
|
||||
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.StartPoint)"
|
||||
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
|
||||
Duration="0:0:2"
|
||||
To="1 0" />
|
||||
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.StartPoint)"
|
||||
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
|
||||
Duration="0:0:2"
|
||||
To="1 1"
|
||||
BeginTime="0:0:2" />
|
||||
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.StartPoint)"
|
||||
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
|
||||
Duration="0:0:2"
|
||||
To="0 1"
|
||||
BeginTime="0:0:4" />
|
||||
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.StartPoint)"
|
||||
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
|
||||
Duration="0:0:2"
|
||||
To="0 0"
|
||||
BeginTime="0:0:6" />
|
||||
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.EndPoint)"
|
||||
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
|
||||
Duration="0:0:2"
|
||||
To="1 1" />
|
||||
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.EndPoint)"
|
||||
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
|
||||
Duration="0:0:2"
|
||||
To="0 1"
|
||||
BeginTime="0:0:2" />
|
||||
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.EndPoint)"
|
||||
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
|
||||
Duration="0:0:2"
|
||||
To="0 0"
|
||||
BeginTime="0:0:4" />
|
||||
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.EndPoint)"
|
||||
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
|
||||
Duration="0:0:2"
|
||||
To="1 0"
|
||||
BeginTime="0:0:6" />
|
||||
</Storyboard>
|
||||
|
||||
<local:ItemToListConverter x:Key="ItemToListConverter" />
|
||||
|
||||
<DataTemplate x:Key="ConnectionTemplate"
|
||||
DataType="{x:Type local:ConnectionViewModel}">
|
||||
<nodify:CircuitConnection Source="{Binding Output.Anchor}"
|
||||
Target="{Binding Input.Anchor}"
|
||||
Foreground="{Binding Input.Color}"
|
||||
Stroke="{Binding Input.Color}"
|
||||
StrokeThickness="2"
|
||||
/>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="PendingConnectionTemplate"
|
||||
DataType="{x:Type local:PendingConnectionViewModel}">
|
||||
<nodify:PendingConnection IsVisible="{Binding IsVisible}"
|
||||
Source="{Binding Source, Mode=OneWayToSource}"
|
||||
Target="{Binding Target, Mode=OneWayToSource}"
|
||||
TargetAnchor="{Binding TargetLocation, Mode=OneWayToSource}"
|
||||
StartedCommand="{Binding DataContext.StartConnectionCommand, RelativeSource={RelativeSource AncestorType={x:Type nodify:NodifyEditor}}}"
|
||||
CompletedCommand="{Binding DataContext.CreateConnectionCommand, RelativeSource={RelativeSource AncestorType={x:Type nodify:NodifyEditor}}}" />
|
||||
</DataTemplate>
|
||||
|
||||
<Style x:Key="ItemContainerStyle"
|
||||
TargetType="{x:Type nodify:ItemContainer}"
|
||||
BasedOn="{StaticResource {x:Type nodify:ItemContainer}}">
|
||||
<Setter Property="Location"
|
||||
Value="{Binding Location}" />
|
||||
<Setter Property="IsSelected"
|
||||
Value="{Binding IsSelected}" />
|
||||
<Setter Property="ActualSize"
|
||||
Value="{Binding Size, Mode=OneWayToSource}" />
|
||||
<Setter Property="BorderBrush"
|
||||
Value="{Binding BorderBrush, Source={StaticResource AnimatedBorderPlaceholder}}" />
|
||||
<Setter Property="BorderThickness"
|
||||
Value="2" />
|
||||
</Style>
|
||||
<SolidColorBrush x:Key="SquareConnectorColor" Color="MediumSlateBlue"></SolidColorBrush>
|
||||
<SolidColorBrush x:Key="TriangleConnectorColor" Color="White"></SolidColorBrush>
|
||||
<Style x:Key="ConnectionStyle" TargetType="{x:Type nodify:BaseConnection}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Input.Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Square}">
|
||||
<Setter Property="Stroke" Value="{StaticResource SquareConnectorColor}"></Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Input.Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Triangle}">
|
||||
<Setter Property="Stroke" Value="{StaticResource TriangleConnectorColor}"></Setter>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
<Setter Property="Stroke" Value="{DynamicResource Connection.StrokeBrush}"></Setter>
|
||||
<Setter Property="Cursor" Value="Hand"></Setter>
|
||||
<Setter Property="ToolTip" Value="Double click to split"></Setter>
|
||||
</Style>
|
||||
<ControlTemplate x:Key="SquareConnector" TargetType="Control">
|
||||
<Rectangle Width="14"
|
||||
Height="14"
|
||||
StrokeDashCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
Stroke="{TemplateBinding BorderBrush}"
|
||||
Fill="{TemplateBinding Background}"
|
||||
StrokeThickness="2" />
|
||||
</ControlTemplate>
|
||||
|
||||
<ControlTemplate x:Key="TriangleConnector" TargetType="Control">
|
||||
<Polygon Width="14"
|
||||
Height="14"
|
||||
Points="2,2 4,2 12,7 4,12 2,12"
|
||||
StrokeDashCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
Stroke="{TemplateBinding BorderBrush}"
|
||||
Fill="{TemplateBinding Background}"
|
||||
|
||||
/>
|
||||
</ControlTemplate>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
|
||||
<nodify:NodifyEditor DataContext="{Binding Calculator}"
|
||||
ItemsSource="{Binding Operations}"
|
||||
Connections="{Binding Connections}"
|
||||
SelectedItems="{Binding SelectedOperations}"
|
||||
DisconnectConnectorCommand="{Binding DisconnectConnectorCommand}"
|
||||
PendingConnection="{Binding PendingConnection}"
|
||||
PendingConnectionTemplate="{StaticResource PendingConnectionTemplate}"
|
||||
ConnectionTemplate="{StaticResource ConnectionTemplate}"
|
||||
Background="{StaticResource SmallGridLinesDrawingBrush}"
|
||||
ItemContainerStyle="{StaticResource ItemContainerStyle}"
|
||||
HasCustomContextMenu="True"
|
||||
GridCellSize="15"
|
||||
AllowDrop="True"
|
||||
Drop="OnDropNode"
|
||||
x:Name="Editor">
|
||||
<nodify:NodifyEditor.Resources>
|
||||
<Style TargetType="{x:Type nodify:NodeInput}"
|
||||
BasedOn="{StaticResource OriginalNodeInputStyle}">
|
||||
<Setter Property="Header"
|
||||
Value="{Binding}" />
|
||||
<Setter Property="IsConnected"
|
||||
Value="{Binding IsConnected}" />
|
||||
<Setter Property="Anchor"
|
||||
Value="{Binding Anchor, Mode=OneWayToSource}" />
|
||||
<Setter Property="ToolTip"
|
||||
Value="{Binding Value}" />
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Title}"
|
||||
Margin="0 0 5 0" />
|
||||
<TextBox Text="{Binding Value}"
|
||||
Visibility="{Binding IsConnected, Converter={shared:BooleanToVisibilityConverter Negate=True}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Title}" Value="">
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Title}" Value="{x:Null}">
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Square}">
|
||||
<Setter Property="ConnectorTemplate" Value="{StaticResource SquareConnector}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource SquareConnectorColor}"></Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Triangle}">
|
||||
<Setter Property="ConnectorTemplate" Value="{StaticResource TriangleConnector}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TriangleConnectorColor}"></Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Circle}">
|
||||
<Setter Property="BorderBrush" Value="{Binding Color}"></Setter>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type nodify:NodeOutput}"
|
||||
BasedOn="{StaticResource OriginalNodeOutputStyle}">
|
||||
<Setter Property="Header"
|
||||
Value="{Binding}" />
|
||||
<Setter Property="IsConnected"
|
||||
Value="{Binding IsConnected}" />
|
||||
<Setter Property="Anchor"
|
||||
Value="{Binding Anchor, Mode=OneWayToSource}" />
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
|
||||
<TextBox Text="{Binding Title}"
|
||||
IsEnabled="False" />
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Title}" Value="">
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Title}" Value="{x:Null}">
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Square}">
|
||||
<Setter Property="ConnectorTemplate" Value="{StaticResource SquareConnector}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource SquareConnectorColor}"></Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Triangle}">
|
||||
<Setter Property="ConnectorTemplate" Value="{StaticResource TriangleConnector}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TriangleConnectorColor}"></Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Circle}">
|
||||
<Setter Property="BorderBrush" Value="{Binding Color}"></Setter>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:OperationGraphViewModel}">
|
||||
<nodify:GroupingNode Header="{Binding}"
|
||||
CanResize="{Binding IsExpanded}"
|
||||
ActualSize="{Binding DesiredSize, Mode=TwoWay}"
|
||||
MovementMode="Self">
|
||||
<nodify:GroupingNode.HeaderTemplate>
|
||||
<DataTemplate DataType="{x:Type local:OperationGraphViewModel}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Text="{Binding Title}" />
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Margin="5 0 0 0"
|
||||
Grid.Column="1">
|
||||
<TextBlock Text="Expand?"
|
||||
Visibility="{Binding IsExpanded, Converter={shared:BooleanToVisibilityConverter}}"
|
||||
Margin="0 0 5 0" />
|
||||
<CheckBox IsChecked="{Binding IsExpanded}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</nodify:GroupingNode.HeaderTemplate>
|
||||
<Grid>
|
||||
<ScrollViewer CanContentScroll="True"
|
||||
Visibility="{Binding IsExpanded, Converter={shared:BooleanToVisibilityConverter}}">
|
||||
<nodify:NodifyEditor Tag="{Binding DataContext, RelativeSource={RelativeSource Self}}"
|
||||
DataContext="{Binding InnerCalculator}"
|
||||
ItemsSource="{Binding Operations}"
|
||||
Connections="{Binding Connections}"
|
||||
SelectedItems="{Binding SelectedOperations}"
|
||||
DisconnectConnectorCommand="{Binding DisconnectConnectorCommand}"
|
||||
PendingConnection="{Binding PendingConnection}"
|
||||
PendingConnectionTemplate="{StaticResource PendingConnectionTemplate}"
|
||||
ConnectionTemplate="{StaticResource ConnectionTemplate}"
|
||||
ItemContainerStyle="{StaticResource ItemContainerStyle}"
|
||||
HasCustomContextMenu="True"
|
||||
Background="Transparent"
|
||||
GridCellSize="15"
|
||||
AllowDrop="True"
|
||||
Drop="OnDropNode"
|
||||
Visibility="{Binding DataContext.IsExpanded, RelativeSource={RelativeSource AncestorType=nodify:GroupingNode}, Converter={shared:BooleanToVisibilityConverter}}">
|
||||
|
||||
<nodify:NodifyEditor.InputBindings>
|
||||
<KeyBinding Key="Delete"
|
||||
Command="{Binding DeleteSelectionCommand}" />
|
||||
<KeyBinding Key="G"
|
||||
Modifiers="Ctrl"
|
||||
Command="{Binding GroupSelectionCommand}" />
|
||||
</nodify:NodifyEditor.InputBindings>
|
||||
|
||||
<nodify:NodifyEditor.CommandBindings>
|
||||
<CommandBinding Command="{x:Static ApplicationCommands.ContextMenu}"
|
||||
Executed="OpenContextMenu_Executed" />
|
||||
</nodify:NodifyEditor.CommandBindings>
|
||||
|
||||
<CompositeCollection>
|
||||
<nodify:DecoratorContainer DataContext="{Binding OperationsMenu}"
|
||||
Location="{Binding Location}">
|
||||
<local:OperationsMenuView />
|
||||
</nodify:DecoratorContainer>
|
||||
</CompositeCollection>
|
||||
</nodify:NodifyEditor>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Input}"
|
||||
Focusable="False">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<nodify:NodeInput />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<nodify:NodeOutput DataContext="{Binding Output}"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</nodify:GroupingNode>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:ExpandoOperationViewModel}">
|
||||
<nodify:Node Header="{Binding Title}"
|
||||
Content="{Binding}"
|
||||
Input="{Binding Input}"
|
||||
Output="{Binding Output}">
|
||||
<nodify:Node.ContentTemplate>
|
||||
<DataTemplate DataType="{x:Type local:ExpandoOperationViewModel}">
|
||||
<StackPanel>
|
||||
<Button Style="{StaticResource IconButton}"
|
||||
Content="{StaticResource PlusIcon}"
|
||||
FocusVisualStyle="{StaticResource {x:Static SystemParameters.FocusVisualStyleKey}}"
|
||||
Command="{Binding AddInputCommand}" />
|
||||
<Button Style="{StaticResource IconButton}"
|
||||
Content="{StaticResource RemoveKeyIcon}"
|
||||
FocusVisualStyle="{StaticResource {x:Static SystemParameters.FocusVisualStyleKey}}"
|
||||
Command="{Binding RemoveInputCommand}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</nodify:Node.ContentTemplate>
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:APIOperationViewModel}">
|
||||
<nodify:Node Header="{Binding Title}"
|
||||
Content="{Binding}"
|
||||
Input="{Binding Input}"
|
||||
Output="{Binding Output}">
|
||||
<nodify:Node.ContentTemplate>
|
||||
<DataTemplate DataType="{x:Type local:APIOperationViewModel}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding OperationType}"></TextBlock>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</nodify:Node.ContentTemplate>
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:ExpressionOperationViewModel}">
|
||||
<nodify:Node Content="{Binding}"
|
||||
Input="{Binding Input}"
|
||||
Output="{Binding Output}">
|
||||
<nodify:Node.ContentTemplate>
|
||||
<DataTemplate DataType="{x:Type local:ExpressionOperationViewModel}">
|
||||
<TextBox Text="{Binding Expression}"
|
||||
MinWidth="100"
|
||||
Margin="5 0 0 0" />
|
||||
</DataTemplate>
|
||||
</nodify:Node.ContentTemplate>
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:CalculatorOperationViewModel}">
|
||||
<nodify:Node Header="{Binding Title}"
|
||||
Input="{Binding Input}"
|
||||
Output="{Binding Output}"
|
||||
ToolTip="Double click to expand">
|
||||
<nodify:Node.InputBindings>
|
||||
<MouseBinding Gesture="LeftDoubleClick"
|
||||
Command="{Binding DataContext.OpenCalculatorCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
CommandParameter="{Binding InnerCalculator}" />
|
||||
</nodify:Node.InputBindings>
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:CalculatorInputOperationViewModel}">
|
||||
<DataTemplate.Resources>
|
||||
<Style TargetType="{x:Type nodify:NodeOutput}"
|
||||
BasedOn="{StaticResource {x:Type nodify:NodeOutput}}">
|
||||
<Setter Property="Header"
|
||||
Value="{Binding}" />
|
||||
<Setter Property="IsConnected"
|
||||
Value="{Binding IsConnected}" />
|
||||
<Setter Property="Anchor"
|
||||
Value="{Binding Anchor, Mode=OneWayToSource}" />
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBox Text="{Binding Value}"
|
||||
IsEnabled="False" />
|
||||
<TextBlock Text="{Binding Title}"
|
||||
Margin="5 0 0 0" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</DataTemplate.Resources>
|
||||
<nodify:Node Header="{Binding Title}"
|
||||
Output="{Binding Output}">
|
||||
<StackPanel>
|
||||
<Button Style="{StaticResource IconButton}"
|
||||
Content="{StaticResource PlusIcon}"
|
||||
Command="{Binding AddOutputCommand}" />
|
||||
<Button Style="{StaticResource IconButton}"
|
||||
Content="{StaticResource RemoveKeyIcon}"
|
||||
Command="{Binding RemoveOutputCommand}" />
|
||||
</StackPanel>
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:OperationGroupViewModel}">
|
||||
<nodify:GroupingNode ActualSize="{Binding GroupSize, Mode=TwoWay}">
|
||||
<nodify:GroupingNode.Header>
|
||||
<shared:EditableTextBlock Text="{Binding Title}"
|
||||
FontWeight="SemiBold"
|
||||
IsEditing="True" />
|
||||
</nodify:GroupingNode.Header>
|
||||
</nodify:GroupingNode>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:AuthOperationViewModel}">
|
||||
<nodify:Node Header="🔐 Auth Configuration"
|
||||
Content="{Binding}"
|
||||
Input="{Binding Input}"
|
||||
Output="{Binding Output}">
|
||||
<nodify:Node.ContentTemplate>
|
||||
<DataTemplate DataType="{x:Type local:AuthOperationViewModel}">
|
||||
<StackPanel Margin="4">
|
||||
<TextBlock Text="Base URL" FontWeight="SemiBold" Margin="0 0 0 2" />
|
||||
<TextBox Text="{Binding BaseUrl, UpdateSourceTrigger=PropertyChanged}"
|
||||
MinWidth="180" Margin="0 0 0 6" />
|
||||
<TextBlock Text="Auth Type" FontWeight="SemiBold" Margin="0 0 0 2" />
|
||||
<ComboBox SelectedItem="{Binding AuthType}"
|
||||
MinWidth="180" Margin="0 0 0 6">
|
||||
<sys:String>Bearer Token</sys:String>
|
||||
<sys:String>Basic Auth</sys:String>
|
||||
<sys:String>API Key</sys:String>
|
||||
<sys:String>None</sys:String>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</nodify:Node.ContentTemplate>
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:OperationViewModel}">
|
||||
<nodify:Node Content="{Binding Title}"
|
||||
Input="{Binding Input}"
|
||||
Output="{Binding Output}" />
|
||||
</DataTemplate>
|
||||
</nodify:NodifyEditor.Resources>
|
||||
|
||||
<nodify:NodifyEditor.InputBindings>
|
||||
<KeyBinding Key="Delete"
|
||||
Command="{Binding DeleteSelectionCommand}" />
|
||||
<KeyBinding Key="G"
|
||||
Modifiers="Ctrl"
|
||||
Command="{Binding GroupSelectionCommand}" />
|
||||
</nodify:NodifyEditor.InputBindings>
|
||||
|
||||
<nodify:NodifyEditor.CommandBindings>
|
||||
<CommandBinding Command="{x:Static ApplicationCommands.ContextMenu}"
|
||||
Executed="OpenContextMenu_Executed" />
|
||||
</nodify:NodifyEditor.CommandBindings>
|
||||
|
||||
<nodify:NodifyEditor.Triggers>
|
||||
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
|
||||
<BeginStoryboard Name="AnimateBorder"
|
||||
Storyboard="{StaticResource AnimateBorder}" />
|
||||
</EventTrigger>
|
||||
</nodify:NodifyEditor.Triggers>
|
||||
|
||||
<CompositeCollection>
|
||||
<nodify:DecoratorContainer DataContext="{Binding OperationsMenu}"
|
||||
Location="{Binding Location}">
|
||||
<local:OperationsMenuView />
|
||||
</nodify:DecoratorContainer>
|
||||
</CompositeCollection>
|
||||
</nodify:NodifyEditor>
|
||||
|
||||
<Grid Background="{StaticResource LargeGridLinesDrawingBrush}"
|
||||
Panel.ZIndex="-2" />
|
||||
|
||||
<Border HorizontalAlignment="Right"
|
||||
MinWidth="200"
|
||||
MaxWidth="300"
|
||||
MaxHeight="500"
|
||||
Padding="7"
|
||||
Margin="10"
|
||||
CornerRadius="3"
|
||||
BorderThickness="2">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{DynamicResource BackgroundColor}"
|
||||
Opacity="0.7" />
|
||||
</Border.Background>
|
||||
<DockPanel>
|
||||
<Button Content="📥 Import Swagger"
|
||||
Command="{Binding Calculator.OperationsMenu.ImportSwaggerCommand}"
|
||||
Margin="5"
|
||||
Padding="8 4"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource NodeInput.BorderBrush}"
|
||||
Foreground="{DynamicResource ForegroundBrush}"
|
||||
FontWeight="Bold"
|
||||
DockPanel.Dock="Top" />
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableOperations}"
|
||||
Focusable="False">
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style>
|
||||
<Setter Property="FrameworkElement.Margin"
|
||||
Value="5" />
|
||||
<Setter Property="FrameworkElement.HorizontalAlignment"
|
||||
Value="Left" />
|
||||
<Setter Property="FrameworkElement.Cursor"
|
||||
Value="Hand" />
|
||||
<Setter Property="FrameworkElement.ToolTip"
|
||||
Value="Drag and drop into the editor" />
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:OperationViewModel}">
|
||||
<nodify:Node Content="{Binding Title}"
|
||||
Input="{Binding Input}"
|
||||
Output="{Binding Output}"
|
||||
BorderBrush="{StaticResource AnimatedBrush}"
|
||||
BorderThickness="2"
|
||||
MouseMove="OnNodeDrag"
|
||||
Focusable="True"
|
||||
KeyboardNavigation.TabNavigation="None">
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Border HorizontalAlignment="Left"
|
||||
MinWidth="200"
|
||||
MaxWidth="250"
|
||||
MaxHeight="500"
|
||||
MinHeight="200"
|
||||
Padding="7"
|
||||
Margin="10"
|
||||
CornerRadius="3"
|
||||
BorderThickness="2">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{DynamicResource BackgroundColor}"
|
||||
Opacity="0.7" />
|
||||
</Border.Background>
|
||||
<DockPanel>
|
||||
<TextBlock Text="Variables" FontWeight="Bold" Margin="0 0 0 4" DockPanel.Dock="Top" />
|
||||
<Button Content="➕ Add New Variable"
|
||||
Command="{Binding Calculator.OperationsMenu.AddVariableCommand}"
|
||||
Margin="0 0 0 6"
|
||||
Padding="6 3"
|
||||
Cursor="Hand"
|
||||
DockPanel.Dock="Top"
|
||||
Background="{DynamicResource NodeInput.BorderBrush}"
|
||||
Foreground="{DynamicResource ForegroundBrush}" />
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- Simple Variables -->
|
||||
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableVariables}"
|
||||
Focusable="False">
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style>
|
||||
<Setter Property="FrameworkElement.Margin" Value="3" />
|
||||
<Setter Property="FrameworkElement.HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="FrameworkElement.Cursor" Value="Hand" />
|
||||
<Setter Property="FrameworkElement.ToolTip" Value="Drag and drop to GET or SET this variable" />
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:OperationInfoViewModel}">
|
||||
<Border Background="#3E3E42" CornerRadius="3" Padding="6 4"
|
||||
MouseMove="OnNodeDrag" Cursor="Hand">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Title}" Foreground="LightGreen" FontWeight="SemiBold" />
|
||||
<TextBlock Text=" : " Foreground="Gray" />
|
||||
<TextBlock Text="{Binding VariableType}" Foreground="CornflowerBlue" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- Separator -->
|
||||
<TextBlock Text="Models" FontWeight="Bold" Margin="0 8 0 4"
|
||||
Visibility="{Binding Calculator.OperationsMenu.AvailableModels.Count, Converter={shared:BooleanToVisibilityConverter}}" />
|
||||
|
||||
<!-- Class Model Variables -->
|
||||
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableModels}"
|
||||
Focusable="False">
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style>
|
||||
<Setter Property="FrameworkElement.Margin" Value="5" />
|
||||
<Setter Property="FrameworkElement.HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="FrameworkElement.Cursor" Value="Hand" />
|
||||
<Setter Property="FrameworkElement.ToolTip" Value="Drag and drop into the editor" />
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:OperationViewModel}">
|
||||
<nodify:Node Content="{Binding Title}"
|
||||
Input="{Binding Input}"
|
||||
Output="{Binding Output}"
|
||||
BorderBrush="{StaticResource AnimatedBrush}"
|
||||
BorderThickness="2"
|
||||
MouseMove="OnNodeDrag"
|
||||
Focusable="True"
|
||||
KeyboardNavigation.TabNavigation="None">
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
101
Examples/Nodify.Calculator/EditorView.xaml.cs
Normal file
101
Examples/Nodify.Calculator/EditorView.xaml.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Nodify.Interactivity;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class OperationsMenuHandler : InputElementState<NodifyEditor>
|
||||
{
|
||||
private static InputGesture OpenGesture { get; } = new Interactivity.MouseGesture(MouseAction.RightClick);
|
||||
private static InputGesture CloseGesture { get; } = new Interactivity.MouseGesture(MouseAction.LeftClick);
|
||||
|
||||
private OperationsMenuViewModel ViewModel => ((CalculatorViewModel)Element.DataContext).OperationsMenu;
|
||||
|
||||
public OperationsMenuHandler(NodifyEditor element) : base(element)
|
||||
{
|
||||
ProcessHandledEvents = true;
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseButtonEventArgs e)
|
||||
{
|
||||
if (!e.Handled && OpenGesture.Matches(e.Source, e))
|
||||
{
|
||||
ViewModel.OpenAt(Element.MouseLocation);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseDown(MouseButtonEventArgs e)
|
||||
{
|
||||
if (CloseGesture.Matches(e.Source, e))
|
||||
{
|
||||
ViewModel.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class EditorView : UserControl
|
||||
{
|
||||
public EditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
static EditorView()
|
||||
{
|
||||
InputProcessor.Shared<NodifyEditor>.RegisterHandlerFactory(editor => new OperationsMenuHandler(editor));
|
||||
}
|
||||
|
||||
private void OnDropNode(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Source is NodifyEditor editor && editor.DataContext is CalculatorViewModel calculator
|
||||
&& e.Data.GetData(typeof(OperationInfoViewModel)) is OperationInfoViewModel operation)
|
||||
{
|
||||
if (operation.IsModelNode)
|
||||
{
|
||||
var dc = editor.DataContext as CalculatorViewModel;
|
||||
var lc = editor.GetLocationInsideEditor(e);
|
||||
var orTitle = operation.Title;
|
||||
dc.OpenGetSetVariable(lc, orTitle);
|
||||
}
|
||||
else if (operation.IsSimpleVariable)
|
||||
{
|
||||
var lc = editor.GetLocationInsideEditor(e);
|
||||
calculator.OpenGetSetForVariable(lc, operation);
|
||||
}
|
||||
else
|
||||
{
|
||||
OperationViewModel op = OperationFactory.GetOperation(operation);
|
||||
op.Location = editor.GetLocationInsideEditor(e);
|
||||
calculator.Operations.Add(op);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNodeDrag(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed && ((FrameworkElement)sender).DataContext is OperationInfoViewModel operation)
|
||||
{
|
||||
var data = new DataObject(typeof(OperationInfoViewModel), operation);
|
||||
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenContextMenu_Executed(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
if (e.Source is NodifyEditor editor && editor.DataContext is CalculatorViewModel calculator)
|
||||
{
|
||||
if (calculator.OperationsMenu.IsVisible)
|
||||
{
|
||||
calculator.OperationsMenu.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
calculator.OperationsMenu.OpenAt(editor.ViewportLocation + new Vector(editor.ViewportSize.Width / 3, editor.ViewportSize.Height / 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Examples/Nodify.Calculator/EditorViewModel.cs
Normal file
40
Examples/Nodify.Calculator/EditorViewModel.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class EditorViewModel : ObservableObject
|
||||
{
|
||||
public event Action<EditorViewModel, CalculatorViewModel>? OnOpenInnerCalculator;
|
||||
|
||||
public EditorViewModel? Parent { get; set; }
|
||||
|
||||
public EditorViewModel()
|
||||
{
|
||||
Calculator = new CalculatorViewModel();
|
||||
OpenCalculatorCommand = new DelegateCommand<CalculatorViewModel>(calculator =>
|
||||
{
|
||||
OnOpenInnerCalculator?.Invoke(this, calculator);
|
||||
});
|
||||
}
|
||||
|
||||
public INodifyCommand OpenCalculatorCommand { get; }
|
||||
|
||||
public Guid Id { get; } = Guid.NewGuid();
|
||||
|
||||
private CalculatorViewModel _calculator = default!;
|
||||
public CalculatorViewModel Calculator
|
||||
{
|
||||
get => _calculator;
|
||||
set => SetProperty(ref _calculator, value);
|
||||
}
|
||||
|
||||
private string? _name;
|
||||
public string? Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
677
Examples/Nodify.Calculator/Executor.cs
Normal file
677
Examples/Nodify.Calculator/Executor.cs
Normal file
@@ -0,0 +1,677 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
#if NET8_0_OR_GREATER
|
||||
using System.Net.Http;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
#endif
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public enum logType
|
||||
{
|
||||
Information,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
public delegate void LogMe(string message, logType logtype = logType.Information);
|
||||
|
||||
public class Executor
|
||||
{
|
||||
private readonly EditorViewModel editorViewModel;
|
||||
public event LogMe OnLogMe;
|
||||
|
||||
static bool isAlreadyLogged = false;
|
||||
static Dictionary<string, string> outputs = new Dictionary<string, string>();
|
||||
static Dictionary<string, dynamic> variables = new Dictionary<string, dynamic>();
|
||||
|
||||
// Auth configuration populated from the Auth node
|
||||
private string _authBaseUrl = string.Empty;
|
||||
private string _authType = string.Empty;
|
||||
private string _authToken = string.Empty;
|
||||
private string _authApiKey = string.Empty;
|
||||
private string _authUsername = string.Empty;
|
||||
private string _authPassword = string.Empty;
|
||||
|
||||
public Executor(EditorViewModel editorViewModel)
|
||||
{
|
||||
this.editorViewModel = editorViewModel;
|
||||
}
|
||||
|
||||
public string PerformPreCheck()
|
||||
{
|
||||
string errorString = string.Empty;
|
||||
var calcModel = editorViewModel.Calculator;
|
||||
var allnodes = calcModel.Operations;
|
||||
errorString = ValidateRequiredNodes("end", allnodes);
|
||||
if (!string.IsNullOrEmpty(errorString))
|
||||
{
|
||||
OnLogMe?.Invoke(errorString, logType.Error);
|
||||
return errorString;
|
||||
}
|
||||
errorString = ValidateRequiredNodes("begin", allnodes);
|
||||
OnLogMe?.Invoke(errorString, logType.Error);
|
||||
return errorString;
|
||||
}
|
||||
|
||||
public string Execute()
|
||||
{
|
||||
string errorString = string.Empty;
|
||||
try
|
||||
{
|
||||
OnLogMe?.Invoke("Starting executing the flow.");
|
||||
OnLogMe?.Invoke("Wish all the best to us :)");
|
||||
OnLogMe?.Invoke("Perorming chain check, it may take some time depending upon the number of nodes.");
|
||||
outputs.Clear();
|
||||
var calcModel = editorViewModel.Calculator;
|
||||
var allnodes = calcModel.Operations;
|
||||
var allConnections = calcModel.Connections;
|
||||
|
||||
// Resolve Auth node configuration before execution
|
||||
ResolveAuthNode(allnodes);
|
||||
|
||||
// Resolve GET variable nodes (they are not in the flow chain)
|
||||
ResolveGetVariableNodes(allnodes, allConnections);
|
||||
|
||||
PerformChainCheck(allnodes, allConnections, false);
|
||||
PerformChainCheck(allnodes, allConnections, true); //Execute operations
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
OnLogMe?.Invoke("either your or our bad luck :(", logType.Error);
|
||||
OnLogMe?.Invoke("Error Details : ", logType.Error);
|
||||
OnLogMe?.Invoke(e.Message, logType.Error);
|
||||
OnLogMe?.Invoke(e.StackTrace, logType.Error);
|
||||
errorString = "Error occured dueing execution of flow";
|
||||
}
|
||||
|
||||
return errorString;
|
||||
}
|
||||
|
||||
public static string GenerateClassFromJson(string json, string className)
|
||||
{
|
||||
// Parse the JSON to understand its structure
|
||||
JToken token = JToken.Parse(json);
|
||||
|
||||
if (token.Type == JTokenType.Array)
|
||||
{
|
||||
// If the JSON is an array, get the first object in the array for structure
|
||||
JArray array = (JArray)token;
|
||||
if (array.Count > 0 && array[0].Type == JTokenType.Object)
|
||||
{
|
||||
JObject firstObject = (JObject)array[0];
|
||||
return $"{GenerateClassFromJObject(firstObject, className)}";
|
||||
}
|
||||
|
||||
return $"The JSON array does not contain valid objects.";
|
||||
}
|
||||
else if (token.Type == JTokenType.Object)
|
||||
{
|
||||
// If the JSON is an object
|
||||
JObject obj = (JObject)token;
|
||||
return GenerateClassFromJObject(obj, className);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "Unsupported JSON structure.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a class definition string from a JObject.
|
||||
/// </summary>
|
||||
/// <param name="jObject">The JObject representing the JSON object.</param>
|
||||
/// <param name="className">The name of the class to generate.</param>
|
||||
/// <returns>The class definition as a string.</returns>
|
||||
public static string GenerateClassFromJObject(JObject jObject, string className)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Start class definition
|
||||
sb.AppendLine($"public class {className}");
|
||||
sb.AppendLine("{");
|
||||
|
||||
// Loop through properties in the JObject and generate class fields
|
||||
foreach (var property in jObject.Properties())
|
||||
{
|
||||
string propName = property.Name;
|
||||
string propType = GetCSharpType(property.Value.Type);
|
||||
|
||||
// Generate the property definition
|
||||
sb.AppendLine($"\tpublic {propType} {ToPascalCase(propName)} {{ get; set; }}");
|
||||
}
|
||||
|
||||
// End class definition
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a JTokenType to a C# type.
|
||||
/// </summary>
|
||||
/// <param name="jsonType">The JTokenType.</param>
|
||||
/// <returns>The corresponding C# type as a string.</returns>
|
||||
public static string GetCSharpType(JTokenType jsonType)
|
||||
{
|
||||
return jsonType switch
|
||||
{
|
||||
JTokenType.Integer => "int",
|
||||
JTokenType.Float => "double",
|
||||
JTokenType.String => "string",
|
||||
JTokenType.Boolean => "bool",
|
||||
JTokenType.Object => "object",
|
||||
JTokenType.Array => "List<object>",
|
||||
_ => "string", // Default to string for other types
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string to PascalCase for naming conventions.
|
||||
/// </summary>
|
||||
/// <param name="input">The string to convert.</param>
|
||||
/// <returns>A PascalCase version of the input string.</returns>
|
||||
public static string ToPascalCase(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return input;
|
||||
|
||||
return char.ToUpper(input[0]) + input.Substring(1);
|
||||
}
|
||||
|
||||
static Type CreateTypeFromJson(JsonElement root, string typeName)
|
||||
{
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
var assemblyName = new AssemblyName("DynamicAssembly");
|
||||
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
|
||||
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
|
||||
|
||||
var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public);
|
||||
|
||||
foreach (var prop in root.EnumerateObject())
|
||||
{
|
||||
Type propType = prop.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => typeof(string),
|
||||
JsonValueKind.Number => typeof(int),
|
||||
JsonValueKind.True or JsonValueKind.False => typeof(bool),
|
||||
_ => typeof(object)
|
||||
};
|
||||
|
||||
var fieldBuilder = typeBuilder.DefineField("_" + prop.Name, propType, FieldAttributes.Private);
|
||||
var propertyBuilder = typeBuilder.DefineProperty(prop.Name, PropertyAttributes.HasDefault, propType, null);
|
||||
|
||||
// Getter
|
||||
var getter = typeBuilder.DefineMethod(
|
||||
"get_" + prop.Name,
|
||||
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
|
||||
propType,
|
||||
Type.EmptyTypes
|
||||
);
|
||||
var getterIL = getter.GetILGenerator();
|
||||
getterIL.Emit(OpCodes.Ldarg_0);
|
||||
getterIL.Emit(OpCodes.Ldfld, fieldBuilder);
|
||||
getterIL.Emit(OpCodes.Ret);
|
||||
propertyBuilder.SetGetMethod(getter);
|
||||
|
||||
// Setter
|
||||
var setter = typeBuilder.DefineMethod(
|
||||
"set_" + prop.Name,
|
||||
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
|
||||
null,
|
||||
new Type[] { propType }
|
||||
);
|
||||
var setterIL = setter.GetILGenerator();
|
||||
setterIL.Emit(OpCodes.Ldarg_0);
|
||||
setterIL.Emit(OpCodes.Ldarg_1);
|
||||
setterIL.Emit(OpCodes.Stfld, fieldBuilder);
|
||||
setterIL.Emit(OpCodes.Ret);
|
||||
propertyBuilder.SetSetMethod(setter);
|
||||
}
|
||||
return typeBuilder.CreateType()!;
|
||||
#endif
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private void CreateModelsFromString(string jsonString)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
|
||||
var jsonDoc = JsonDocument.Parse(jsonString);
|
||||
var root = jsonDoc.RootElement;
|
||||
|
||||
if (root.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
ParseFromJsonelement(root);
|
||||
}
|
||||
else if(root.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in root.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
ParseFromJsonelement(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
private void ParseFromJsonelement(JsonElement jsonElem)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
Type dynamicType = CreateTypeFromJson(jsonElem, "DynamicPerson");
|
||||
|
||||
object instance = Activator.CreateInstance(dynamicType);
|
||||
|
||||
foreach (var prop in jsonElem.EnumerateObject())
|
||||
{
|
||||
PropertyInfo pi = dynamicType.GetProperty(prop.Name);
|
||||
object value = prop.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => prop.Value.GetString(),
|
||||
JsonValueKind.Number => prop.Value.GetDouble(),
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
_ => null
|
||||
};
|
||||
pi.SetValue(instance, value);
|
||||
}
|
||||
Console.WriteLine("Properties of runtime class:");
|
||||
foreach (var prop in dynamicType.GetProperties())
|
||||
{
|
||||
Console.WriteLine($"{prop.Name} = {prop.GetValue(instance)}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void StartExecution(OperationViewModel op, ICollection<ConnectionViewModel> connections)
|
||||
{
|
||||
var url = op.Title ?? "";
|
||||
if (url.ToLower().Contains("create model"))
|
||||
{
|
||||
var conByTitle = connections.Where(c => c.Input.Operation.Title.ToLower().Contains("create model")).ToList();
|
||||
if (conByTitle.Count <= 0)
|
||||
{
|
||||
OnLogMe?.Invoke("No input found", logType.Error);
|
||||
throw new Exception("Input connection missig for : " + url);
|
||||
}
|
||||
var flowConnection = conByTitle.Where(c=>c.Input.Shape != ConnectorShape.Triangle).FirstOrDefault();
|
||||
if (flowConnection == null)
|
||||
{
|
||||
OnLogMe?.Invoke("No input found", logType.Error);
|
||||
throw new Exception("Input connection missig for : " + url);
|
||||
}
|
||||
|
||||
var outputNodeId = flowConnection.InputNodeId;
|
||||
|
||||
string outputValue = string.Empty;
|
||||
if (outputs.TryGetValue(outputNodeId, out outputValue))
|
||||
{
|
||||
//Convert model here
|
||||
//CreateModelsFromString(outputValue);
|
||||
var customModelDir = "CustomModels";
|
||||
Directory.CreateDirectory(customModelDir);
|
||||
string className = "Class 1";
|
||||
className = className.Replace(" ", "");
|
||||
var classStruct = GenerateClassFromJson(outputValue, className);
|
||||
string flPath = Path.Join(customModelDir, $"{className}.cs");
|
||||
File.WriteAllText(flPath, classStruct);
|
||||
OperationInfoViewModel opv = new OperationInfoViewModel()
|
||||
{
|
||||
Title = className,
|
||||
IsModelNode = true,
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.GET_SET,
|
||||
ClassName = className
|
||||
};
|
||||
this.editorViewModel.Calculator.OperationsMenu.AddNewModel(opv);
|
||||
}
|
||||
}
|
||||
|
||||
if (url.ToLower().Contains("set"))
|
||||
{
|
||||
// Handle simple variable SET (e.g., "SET myVar (string)")
|
||||
if (op is SystemOperationViewModel sysVarOp && url.Contains("(") && url.Contains(")"))
|
||||
{
|
||||
var parts = url.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
var varName = parts[1];
|
||||
// Find the data input connection (non-triangle)
|
||||
var inputCons = connections.Where(c => c.Input.Operation == op && c.Input.Shape != ConnectorShape.Triangle).ToList();
|
||||
if (inputCons.Any())
|
||||
{
|
||||
var sourceConn = inputCons.First();
|
||||
var sourceNodeId = sourceConn.Output.Operation.NodeId;
|
||||
if (outputs.TryGetValue(sourceNodeId, out var sourceVal))
|
||||
{
|
||||
variables[varName] = sourceVal;
|
||||
OnLogMe?.Invoke($"Variable '{varName}' SET to: {sourceVal}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use the connector value directly
|
||||
variables[varName] = sourceConn.Output.Value.ToString();
|
||||
OnLogMe?.Invoke($"Variable '{varName}' SET to connector value: {sourceConn.Output.Value}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No connection — use default value from the Value input connector
|
||||
var valueInput = op.Input.FirstOrDefault(i => i.Title == "Value");
|
||||
if (valueInput != null)
|
||||
{
|
||||
variables[varName] = valueInput.Value.ToString();
|
||||
OnLogMe?.Invoke($"Variable '{varName}' SET to default: {valueInput.Value}");
|
||||
}
|
||||
}
|
||||
outputs[op.NodeId] = variables.ContainsKey(varName) ? variables[varName]?.ToString() ?? "" : "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var fullTitle = url;
|
||||
var classNameToSet = fullTitle.Split(" ", StringSplitOptions.RemoveEmptyEntries)[1];
|
||||
var inputConnectionList = connections.Where(c => c.Input.Operation.Title == url).ToList();
|
||||
var inputConnection = inputConnectionList.Where(c => c.Input.Shape != ConnectorShape.Triangle).FirstOrDefault();
|
||||
if (inputConnection == null)
|
||||
{
|
||||
OnLogMe?.Invoke("No input found", logType.Error);
|
||||
throw new Exception("Input connection missig for : " + url);
|
||||
}
|
||||
var outPutNode = inputConnection.Output;
|
||||
if (outPutNode == null)
|
||||
{
|
||||
OnLogMe?.Invoke("No output found", logType.Error);
|
||||
throw new Exception("Output connection missig for : " + url);
|
||||
}
|
||||
if (outPutNode.Operation.Title.ToLower().Contains("parse json"))
|
||||
{
|
||||
var inputNodeForParseJson = connections.Where(c => c.Input.Operation.Title == outPutNode.Title).FirstOrDefault();
|
||||
if (inputNodeForParseJson == null)
|
||||
{
|
||||
OnLogMe?.Invoke("No input found", logType.Error);
|
||||
throw new Exception("Input connection missig for : " + url);
|
||||
}
|
||||
var inputNodeOutput = outputs[inputNodeForParseJson.InputNodeId];
|
||||
if (inputNodeOutput != null)
|
||||
{
|
||||
var obj = JsonConvert.DeserializeObject(inputNodeOutput);
|
||||
variables[op.NodeId] = obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnLogMe?.Invoke($"Execution started : {url}");
|
||||
if (url.ToLower() == "begin" || url.ToLower() == "end")
|
||||
{
|
||||
OnLogMe?.Invoke($"Being or End node found. skipping it");
|
||||
return;
|
||||
}
|
||||
if (url.ToLower() == "auth")
|
||||
{
|
||||
ResolveAuthNode(editorViewModel.Calculator.Operations);
|
||||
OnLogMe?.Invoke($"Auth node resolved. Base URL: {_authBaseUrl}, Auth Type: {_authType}");
|
||||
return;
|
||||
}
|
||||
OnLogMe?.Invoke($"Starting Execution : {url}");
|
||||
var res = GetResponse(url, "get");
|
||||
if (!string.IsNullOrEmpty(res))
|
||||
{
|
||||
outputs.Add(op.NodeId, res);
|
||||
}
|
||||
OnLogMe?.Invoke($"Response Result : {res}");
|
||||
}
|
||||
|
||||
private void ResolveAuthNode(ICollection<OperationViewModel> allNodes)
|
||||
{
|
||||
var authNode = allNodes.OfType<AuthOperationViewModel>().FirstOrDefault();
|
||||
if (authNode == null)
|
||||
{
|
||||
OnLogMe?.Invoke("No Auth node found. Using defaults.", logType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var inp in authNode.Input)
|
||||
{
|
||||
var title = inp.Title?.Trim() ?? string.Empty;
|
||||
// For unconnected inputs the user types a value into the textbox, which is stored as Value (double).
|
||||
// But the actual text is stored in the Title's textbox or the Value textbox.
|
||||
// We read the connector's Value textbox string representation via the bound text.
|
||||
var val = GetConnectorTextValue(inp);
|
||||
|
||||
switch (title)
|
||||
{
|
||||
case "Base URL": _authBaseUrl = val; break;
|
||||
case "Auth Type": _authType = val; break;
|
||||
case "Token": _authToken = val; break;
|
||||
case "API Key": _authApiKey = val; break;
|
||||
case "Username": _authUsername = val; break;
|
||||
case "Password": _authPassword = val; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Also read from the view-model properties which are bound to the text boxes in the node
|
||||
if (!string.IsNullOrWhiteSpace(authNode.BaseUrl))
|
||||
_authBaseUrl = authNode.BaseUrl;
|
||||
if (!string.IsNullOrWhiteSpace(authNode.AuthType))
|
||||
_authType = authNode.AuthType;
|
||||
|
||||
OnLogMe?.Invoke($"Auth configured — Base URL: {_authBaseUrl}, Auth Type: {_authType}");
|
||||
}
|
||||
|
||||
private void ResolveGetVariableNodes(ICollection<OperationViewModel> allNodes, ICollection<ConnectionViewModel> connections)
|
||||
{
|
||||
// Find all GET variable nodes (not in flow chain) and populate their output
|
||||
foreach (var node in allNodes)
|
||||
{
|
||||
var title = node.Title ?? "";
|
||||
if (!title.StartsWith("GET ") || !title.Contains("(") || !title.Contains(")"))
|
||||
continue;
|
||||
|
||||
var parts = title.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 2) continue;
|
||||
var varName = parts[1];
|
||||
|
||||
// Check if the variable already has a value
|
||||
if (variables.TryGetValue(varName, out var existingVal))
|
||||
{
|
||||
outputs[node.NodeId] = existingVal?.ToString() ?? "";
|
||||
// Set the output connector value for downstream nodes
|
||||
var outConn = node.Output.FirstOrDefault(c => c.Shape != ConnectorShape.Triangle);
|
||||
if (outConn != null)
|
||||
{
|
||||
if (double.TryParse(existingVal?.ToString(), out double dVal))
|
||||
outConn.Value = dVal;
|
||||
}
|
||||
OnLogMe?.Invoke($"GET variable '{varName}' resolved to: {existingVal}");
|
||||
}
|
||||
else
|
||||
{
|
||||
OnLogMe?.Invoke($"GET variable '{varName}' has no value yet (will resolve during execution).", logType.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetConnectorTextValue(ConnectorViewModel connector)
|
||||
{
|
||||
// When the connector is not connected, the user enters text in the Value text box.
|
||||
// Value is a double, so we try to use it as a string representation.
|
||||
// However, for string inputs (URL, token, etc.) the value won't be meaningful as a double.
|
||||
// The user-typed string is actually stored in the Title field for unconnected inputs in some cases,
|
||||
// but here we rely on the ConnectorViewModel.Value being 0 (default) and the actual string
|
||||
// is not captured as double. So we return empty — the AuthOperationViewModel properties are the
|
||||
// primary source set via the node's text boxes.
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private string GetResponse(string url, string type)
|
||||
{
|
||||
string baseURL = !string.IsNullOrWhiteSpace(_authBaseUrl) ? _authBaseUrl : "https://localhost:7107";
|
||||
string responseString = string.Empty;
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
using (HttpClient client = new HttpClient())
|
||||
{
|
||||
client.BaseAddress = new Uri(baseURL);
|
||||
|
||||
// Apply authentication headers
|
||||
if (!string.IsNullOrWhiteSpace(_authType))
|
||||
{
|
||||
var authTypeLower = _authType.Trim().ToLower();
|
||||
if (authTypeLower.Contains("bearer") && !string.IsNullOrWhiteSpace(_authToken))
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _authToken);
|
||||
}
|
||||
else if (authTypeLower.Contains("basic") &&
|
||||
!string.IsNullOrWhiteSpace(_authUsername))
|
||||
{
|
||||
var credentials = Convert.ToBase64String(
|
||||
Encoding.UTF8.GetBytes($"{_authUsername}:{_authPassword}"));
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
|
||||
}
|
||||
else if (authTypeLower.Contains("api") && !string.IsNullOrWhiteSpace(_authApiKey))
|
||||
{
|
||||
client.DefaultRequestHeaders.Add("X-Api-Key", _authApiKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == "get")
|
||||
{
|
||||
HttpResponseMessage response = client.GetAsync(url).Result;
|
||||
response.EnsureSuccessStatusCode();
|
||||
responseString = response.Content.ReadAsStringAsync().Result;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return responseString;
|
||||
}
|
||||
|
||||
private void PerformChainCheck(ICollection<OperationViewModel> allNodes, ICollection<ConnectionViewModel> connections, bool isExecute)
|
||||
{
|
||||
string startNodeTitle = "begin";
|
||||
string endNodeTitle = "end";
|
||||
|
||||
// Find the starting node
|
||||
var startNode = allNodes.FirstOrDefault(node => node.Title?.ToLower() == startNodeTitle);
|
||||
if (startNode == null)
|
||||
{
|
||||
OnLogMe?.Invoke("No Begin node found", logType.Error);
|
||||
throw new Exception("Begin node not found");
|
||||
}
|
||||
|
||||
// Track visited nodes to prevent infinite loops or revisits
|
||||
HashSet<string> visitedNodes = new HashSet<string>();
|
||||
|
||||
// Perform the DFS Traversal
|
||||
bool isValidChain = TraverseChain(startNode, endNodeTitle, connections, visitedNodes, isExecute);
|
||||
|
||||
if (isValidChain)
|
||||
{
|
||||
OnLogMe?.Invoke("Found complete chain...");
|
||||
OnLogMe?.Invoke("You did it champ...");
|
||||
}
|
||||
else
|
||||
{
|
||||
OnLogMe?.Invoke("Broken Chain found!!! Please fix the chain from begin to end.", logType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TraverseChain(OperationViewModel currentNode, string endNodeTitle,
|
||||
ICollection<ConnectionViewModel> connections,
|
||||
HashSet<string> visitedNodes, bool isExecute)
|
||||
{
|
||||
OnLogMe?.Invoke($"Checking node: {currentNode.Title} , NodeId : {currentNode.NodeId}");
|
||||
|
||||
// If we've reached the "end" node, the chain is valid
|
||||
if (currentNode.Title?.Equals(endNodeTitle, StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Detect cycles and avoid infinite recursion
|
||||
if (visitedNodes.Contains(currentNode.NodeId))
|
||||
{
|
||||
OnLogMe?.Invoke($"Cycle detected at node: {currentNode.Title}. Broken chain found!", logType.Warning);
|
||||
return false;
|
||||
}
|
||||
visitedNodes.Add(currentNode.NodeId);
|
||||
|
||||
// Find all outgoing connections from the current node
|
||||
var outgoingConnections = connections.Where(conn => conn.Output?.Operation == currentNode && conn.Output.Shape == ConnectorShape.Triangle);
|
||||
|
||||
if (!outgoingConnections.Any())
|
||||
{
|
||||
// Only log the first "broken chain" warning, and stop further logging on backtrack
|
||||
OnLogMe?.Invoke($"Broken chain detected at node: {currentNode.Title}", logType.Warning);
|
||||
isAlreadyLogged = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check all outgoing connections
|
||||
foreach (var connection in outgoingConnections)
|
||||
{
|
||||
var nextNode = connection.Input?.Operation;
|
||||
if (nextNode == null)
|
||||
{
|
||||
// Handle null input in the connection
|
||||
OnLogMe?.Invoke($"Broken chain detected due to null input connection from node: {currentNode.Title}", logType.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
OnLogMe?.Invoke($"Following connection from {currentNode.Title} to {nextNode.Title}");
|
||||
|
||||
if (isExecute)
|
||||
{
|
||||
StartExecution(currentNode, connections);
|
||||
}
|
||||
|
||||
// Recursively check the next node; stop on the first failure
|
||||
if (TraverseChain(nextNode, endNodeTitle, connections, visitedNodes, isExecute))
|
||||
{
|
||||
return true; // If a valid path to the "end" is found, exit
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAlreadyLogged)
|
||||
{
|
||||
// If no connections lead to the end, this is the broken point => log here ONLY
|
||||
OnLogMe?.Invoke($"Broken chain detected at node: {currentNode.Title}", logType.Warning);
|
||||
isAlreadyLogged = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string ValidateRequiredNodes(string nodeName, ICollection<OperationViewModel> allnodes)
|
||||
{
|
||||
var requireNode = allnodes.Where(c => c.Title.ToLower() == nodeName).ToList();
|
||||
if (requireNode.Count > 1)
|
||||
{
|
||||
return $"One or more {nodeName} node found. Only one allowed at a time.";
|
||||
}
|
||||
else if (requireNode.Count == 0)
|
||||
{
|
||||
return $"No {nodeName} node found. At least one {nodeName} node required system to work";
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Examples/Nodify.Calculator/ExpandoOperationViewModel.cs
Normal file
33
Examples/Nodify.Calculator/ExpandoOperationViewModel.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class ExpandoOperationViewModel : OperationViewModel
|
||||
{
|
||||
public ExpandoOperationViewModel()
|
||||
{
|
||||
AddInputCommand = new RequeryCommand(
|
||||
() => Input.Add(new ConnectorViewModel()),
|
||||
() => Input.Count < MaxInput);
|
||||
|
||||
RemoveInputCommand = new RequeryCommand(
|
||||
() => Input.RemoveAt(Input.Count - 1),
|
||||
() => Input.Count > MinInput);
|
||||
}
|
||||
|
||||
public INodifyCommand AddInputCommand { get; }
|
||||
public INodifyCommand RemoveInputCommand { get; }
|
||||
|
||||
private uint _minInput = 0;
|
||||
public uint MinInput
|
||||
{
|
||||
get => _minInput;
|
||||
set => SetProperty(ref _minInput, value);
|
||||
}
|
||||
|
||||
private uint _maxInput = uint.MaxValue;
|
||||
public uint MaxInput
|
||||
{
|
||||
get => _maxInput;
|
||||
set => SetProperty(ref _maxInput, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Examples/Nodify.Calculator/ExpressionOperationViewModel.cs
Normal file
59
Examples/Nodify.Calculator/ExpressionOperationViewModel.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using StringMath;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class ExpressionOperationViewModel : OperationViewModel
|
||||
{
|
||||
private MathExpr? _expr;
|
||||
private string? _expression;
|
||||
public string? Expression
|
||||
{
|
||||
get => _expression;
|
||||
set => SetProperty(ref _expression, value)
|
||||
.Then(GenerateInput);
|
||||
}
|
||||
|
||||
private void GenerateInput()
|
||||
{
|
||||
try
|
||||
{
|
||||
_expr = Expression!.ToMathExpr();
|
||||
ConnectorViewModel[]? toRemove = Input.Where(i => !_expr.LocalVariables.Contains(i.Title)).ToArray();
|
||||
toRemove.ForEach(i => Input.Remove(i));
|
||||
HashSet<string> existingVars = Input.Select(s => s.Title).Where(s => s != null).ToHashSet()!;
|
||||
|
||||
foreach (string variable in _expr.LocalVariables.Except(existingVars))
|
||||
{
|
||||
Input.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = variable
|
||||
});
|
||||
}
|
||||
|
||||
OnInputValueChanged();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInputValueChanged()
|
||||
{
|
||||
if (Output != null && _expr != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Input.ForEach(i => _expr.Substitute(i.Title!, i.Value));
|
||||
//Output.Value = _expr.Result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Examples/Nodify.Calculator/FlowRunner.xaml
Normal file
23
Examples/Nodify.Calculator/FlowRunner.xaml
Normal file
@@ -0,0 +1,23 @@
|
||||
<Window x:Class="Nodify.Calculator.FlowRunner"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Nodify.Calculator"
|
||||
mc:Ignorable="d"
|
||||
Loaded="Window_Loaded"
|
||||
Title="FlowRunner" Height="450" Width="800" ResizeMode="NoResize" WindowStartupLocation="CenterOwner">
|
||||
<Grid>
|
||||
<RichTextBox x:Name="LogRichTextBox"
|
||||
IsReadOnly="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Background="#FF2D2D30"
|
||||
Foreground="White"
|
||||
FontFamily="Consolas"
|
||||
FontSize="14"
|
||||
BorderThickness="0">
|
||||
<!-- Initialize with an empty document -->
|
||||
<FlowDocument />
|
||||
</RichTextBox>
|
||||
</Grid>
|
||||
</Window>
|
||||
73
Examples/Nodify.Calculator/FlowRunner.xaml.cs
Normal file
73
Examples/Nodify.Calculator/FlowRunner.xaml.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for FlowRunner.xaml
|
||||
/// </summary>
|
||||
public partial class FlowRunner : Window
|
||||
{
|
||||
private readonly Executor _executor;
|
||||
|
||||
public FlowRunner(Executor executor)
|
||||
{
|
||||
InitializeComponent();
|
||||
_executor = executor;
|
||||
}
|
||||
|
||||
private void WriteLog(string message, logType logtype)
|
||||
{
|
||||
// IMPORTANT: UI updates must happen on the main UI thread.
|
||||
// Dispatcher.Invoke ensures that, even if the event is fired from a background thread.
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
// Choose a color based on the log type
|
||||
Brush color = Brushes.White; // Default for Information
|
||||
switch (logtype)
|
||||
{
|
||||
case logType.Warning:
|
||||
color = Brushes.Yellow;
|
||||
break;
|
||||
case logType.Error:
|
||||
color = Brushes.Red;
|
||||
break;
|
||||
}
|
||||
|
||||
// Create a text run with the specified color
|
||||
var run = new Run($"{DateTime.Now:HH:mm:ss} [{logtype}]: {message}\n")
|
||||
{
|
||||
Foreground = color
|
||||
};
|
||||
|
||||
// Add the text to a new paragraph and add the paragraph to the RichTextBox
|
||||
var paragraph = new Paragraph();
|
||||
paragraph.Inlines.Add(run);
|
||||
LogRichTextBox.Document.Blocks.Add(paragraph);
|
||||
|
||||
// Auto-scroll to the bottom
|
||||
LogRichTextBox.ScrollToEnd();
|
||||
});
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Task.Run(() => {
|
||||
_executor.OnLogMe += WriteLog;
|
||||
_executor.Execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
189
Examples/Nodify.Calculator/LiteDBHelper.cs
Normal file
189
Examples/Nodify.Calculator/LiteDBHelper.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using LiteDB;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
public class LiteDbHelper<T> : IDisposable where T : class
|
||||
{
|
||||
private string dbPath = @"MyData.db";
|
||||
private readonly LiteDatabase _database;
|
||||
private readonly ILiteCollection<T> _collection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the LiteDbHelper class.
|
||||
/// </summary>
|
||||
/// <param name="databasePath">The path to the LiteDB database file.</param>
|
||||
/// <param name="collectionName">The name of the collection to work with.</param>
|
||||
public LiteDbHelper(string collectionName)
|
||||
{
|
||||
_database = new LiteDatabase(dbPath);
|
||||
_collection = _database.GetCollection<T>(collectionName);
|
||||
}
|
||||
|
||||
// --- CRUD Operations ---
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a new document into the collection.
|
||||
/// </summary>
|
||||
/// <param name="document">The document to insert.</param>
|
||||
/// <returns>The BsonValue of the inserted document's Id.</returns>
|
||||
public BsonValue Insert(T document)
|
||||
{
|
||||
return _collection.Insert(document);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a collection of documents.
|
||||
/// </summary>
|
||||
/// <param name="documents">The documents to insert.</param>
|
||||
/// <returns>The number of documents inserted.</returns>
|
||||
public int BulkInsert(IEnumerable<T> documents)
|
||||
{
|
||||
return _collection.Insert(documents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a document in the collection.
|
||||
/// </summary>
|
||||
/// <param name="document">The document to update.</param>
|
||||
/// <returns>True if the document was updated, otherwise false.</returns>
|
||||
public bool Update(T document)
|
||||
{
|
||||
return _collection.Update(document);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a document from the collection by its Id.
|
||||
/// </summary>
|
||||
/// <param name="id">The Id of the document to delete.</param>
|
||||
/// <returns>True if the document was deleted, otherwise false.</returns>
|
||||
public bool Delete(BsonValue id)
|
||||
{
|
||||
return _collection.Delete(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes documents from the collection based on a predicate.
|
||||
/// </summary>
|
||||
/// <param name="predicate">The expression to filter documents to delete.</param>
|
||||
/// <returns>The number of documents deleted.</returns>
|
||||
public int DeleteMany(Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
return _collection.DeleteMany(predicate);
|
||||
}
|
||||
|
||||
// --- Query Operations ---
|
||||
|
||||
/// <summary>
|
||||
/// Finds a single document by its Id.
|
||||
/// </summary>
|
||||
/// <param name="id">The Id of the document.</param>
|
||||
/// <returns>The document if found, otherwise null.</returns>
|
||||
public T FindById(BsonValue id)
|
||||
{
|
||||
return _collection.FindById(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first document that matches the predicate.
|
||||
/// </summary>
|
||||
/// <param name="predicate">The expression to filter documents.</param>
|
||||
/// <returns>The first matching document, or null if none are found.</returns>
|
||||
public T FindOne(Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
return _collection.FindOne(predicate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all documents in the collection.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of all documents.</returns>
|
||||
public IEnumerable<T> FindAll()
|
||||
{
|
||||
return _collection.FindAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds documents based on a predicate.
|
||||
/// </summary>
|
||||
/// <param name="predicate">The expression to filter documents.</param>
|
||||
/// <returns>An enumerable of matching documents.</returns>
|
||||
public IEnumerable<T> Find(Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
return _collection.Find(predicate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any document exists that matches the predicate.
|
||||
/// </summary>
|
||||
/// <param name="predicate">The expression to filter documents.</param>
|
||||
/// <returns>True if a matching document exists, otherwise false.</returns>
|
||||
public bool Exists(Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
return _collection.Exists(predicate);
|
||||
}
|
||||
|
||||
// --- Indexing ---
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that an index is created for the specified field.
|
||||
/// </summary>
|
||||
/// <param name="field">The expression representing the field to be indexed.</param>
|
||||
/// <param name="unique">Whether the index should enforce unique values.</param>
|
||||
/// <returns>True if the index was created, otherwise false.</returns>
|
||||
public bool EnsureIndex<K>(Expression<Func<T, K>> field, bool unique = false)
|
||||
{
|
||||
return _collection.EnsureIndex(field, unique);
|
||||
}
|
||||
|
||||
// --- File Storage Operations ---
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file to the LiteDB file storage.
|
||||
/// </summary>
|
||||
/// <param name="id">A unique identifier for the file.</param>
|
||||
/// <param name="filePath">The path to the file to upload.</param>
|
||||
public void UploadFile(string id, string filePath)
|
||||
{
|
||||
_database.FileStorage.Upload(id, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a file from the LiteDB file storage.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the file.</param>
|
||||
/// <param name="destinationPath">The path to save the downloaded file.</param>
|
||||
public void DownloadFile(string id, Stream destinationPath)
|
||||
{
|
||||
_database.FileStorage.Download(id, destinationPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a file from the LiteDB file storage.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the file to delete.</param>
|
||||
/// <returns>True if the file was deleted, otherwise false.</returns>
|
||||
public bool DeleteFile(string id)
|
||||
{
|
||||
return _database.FileStorage.Delete(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a file's metadata in the LiteDB file storage.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the file.</param>
|
||||
/// <returns>The LiteFileInfo object if found, otherwise null.</returns>
|
||||
public LiteFileInfo<string> FindFileById(string id)
|
||||
{
|
||||
return _database.FileStorage.FindById(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the LiteDatabase connection.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_database?.Dispose();
|
||||
}
|
||||
}
|
||||
164
Examples/Nodify.Calculator/MainWindow.xaml
Normal file
164
Examples/Nodify.Calculator/MainWindow.xaml
Normal file
@@ -0,0 +1,164 @@
|
||||
<Window x:Class="Nodify.Calculator.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Nodify.Calculator"
|
||||
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
|
||||
Background="{DynamicResource NodifyEditor.BackgroundBrush}"
|
||||
Foreground="{DynamicResource ForegroundBrush}"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow"
|
||||
Height="650"
|
||||
Width="1200">
|
||||
<Window.DataContext>
|
||||
<local:ApplicationViewModel />
|
||||
</Window.DataContext>
|
||||
|
||||
<Window.InputBindings>
|
||||
<KeyBinding Key="T"
|
||||
Modifiers="Ctrl"
|
||||
Command="{Binding Source={x:Static shared:ThemeManager.SetNextThemeCommand}}" />
|
||||
<KeyBinding Key="N"
|
||||
Modifiers="Ctrl"
|
||||
Command="{Binding AddEditorCommand}" />
|
||||
|
||||
<KeyBinding Key="R"
|
||||
Modifiers="Ctrl"
|
||||
Command="{Binding RunFlowCommand}" />
|
||||
|
||||
<KeyBinding Key="S"
|
||||
Modifiers="Ctrl"
|
||||
Command="{Binding SaveFileCommand}" />
|
||||
|
||||
<KeyBinding Key="O"
|
||||
Modifiers="Ctrl"
|
||||
Command="{Binding OpenFileCommand}" />
|
||||
|
||||
<KeyBinding Key="W"
|
||||
Modifiers="Ctrl"
|
||||
Command="{Binding CloseEditorCommand}"
|
||||
CommandParameter="{Binding SelectedEditor.Id}"/>
|
||||
</Window.InputBindings>
|
||||
|
||||
<Window.Resources>
|
||||
<shared:BindingProxy x:Key="Proxy"
|
||||
DataContext="{Binding}"/>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:EditorViewModel}">
|
||||
<local:EditorView/>
|
||||
</DataTemplate>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<shared:TabControlEx ItemsSource="{Binding Editors}"
|
||||
SelectedItem="{Binding SelectedEditor}"
|
||||
AddTabCommand="{Binding AddEditorCommand}"
|
||||
AutoScrollToEnd="{Binding AutoSelectNewEditor}">
|
||||
<shared:TabControlEx.ItemContainerStyle>
|
||||
<Style TargetType="{x:Type shared:TabItemEx}"
|
||||
BasedOn="{StaticResource {x:Type shared:TabItemEx}}">
|
||||
<Setter Property="Header"
|
||||
Value="{Binding Name}"/>
|
||||
<Setter Property="CloseTabCommand"
|
||||
Value="{Binding DataContext.CloseEditorCommand ,Source={StaticResource Proxy}}"/>
|
||||
<Setter Property="CloseTabCommandParameter"
|
||||
Value="{Binding Id}"/>
|
||||
<Setter Property="ToolTip"
|
||||
Value="Double click to edit" />
|
||||
</Style>
|
||||
</shared:TabControlEx.ItemContainerStyle>
|
||||
</shared:TabControlEx>
|
||||
|
||||
<Expander Header="Click to hide/show"
|
||||
IsExpanded="True"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom">
|
||||
<Border MaxWidth="325"
|
||||
MaxHeight="300"
|
||||
CornerRadius="3">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{DynamicResource BackgroundColor}"
|
||||
Opacity="0.7" />
|
||||
</Border.Background>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="10"
|
||||
IsHitTestVisible="False">
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="{x:Type TextBlock}"
|
||||
BasedOn="{StaticResource {x:Type TextBlock}}">
|
||||
<Setter Property="Margin"
|
||||
Value="0 0 0 5" />
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
|
||||
<StackPanel Margin="0 0 0 20">
|
||||
<TextBlock Text="(New) Drag and drop nodes from the toolbox"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource NodeInput.BorderBrush}"
|
||||
FontWeight="Bold"/>
|
||||
</StackPanel>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Foreground="Red"
|
||||
FontWeight="Bold">CTRL + N/W</Run>
|
||||
<Run>: open/close editor</Run>
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Foreground="Red"
|
||||
FontWeight="Bold">CTRL + R</Run>
|
||||
<Run>: Run Flow</Run>
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Foreground="Red"
|
||||
FontWeight="Bold">CTRL + S</Run>
|
||||
<Run>: Save Flow</Run>
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Foreground="Red"
|
||||
FontWeight="Bold">CTRL + O</Run>
|
||||
<Run>: Open Saved Flow</Run>
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Foreground="Red"
|
||||
FontWeight="Bold">ALT + Click</Run>
|
||||
<Run>: disconnect connector</Run>
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Foreground="Red"
|
||||
FontWeight="Bold">Right Click</Run>
|
||||
<Run>: show operations menu (create nodes)</Run>
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Foreground="Red"
|
||||
FontWeight="Bold">Delete</Run>
|
||||
<Run>: delete selection</Run>
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Foreground="Red"
|
||||
FontWeight="Bold">CTRL + T</Run>
|
||||
<Run>: change theme</Run>
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Foreground="Red"
|
||||
FontWeight="Bold">CTRL + G</Run>
|
||||
<Run>: group selection (hold SHIFT and mouse drag the header to move the group node alone)</Run>
|
||||
</TextBlock>
|
||||
<TextBlock Text="Drag a connection and drop it on the editor"
|
||||
TextWrapping="Wrap"
|
||||
FontWeight="Bold" />
|
||||
<TextBlock Text="Hover over a connector to see its value"
|
||||
TextWrapping="Wrap"
|
||||
FontWeight="Bold" />
|
||||
<TextBlock Text="Create a Calculator node and double click it to open"
|
||||
TextWrapping="Wrap"
|
||||
FontWeight="Bold" />
|
||||
<TextBlock Text="Create an Operation Graph and add operations to it"
|
||||
TextWrapping="Wrap"
|
||||
FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Expander>
|
||||
</Grid>
|
||||
</Window>
|
||||
26
Examples/Nodify.Calculator/MainWindow.xaml.cs
Normal file
26
Examples/Nodify.Calculator/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Nodify.Interactivity;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
EditorGestures.Mappings.Editor.Cutting.Unbind();
|
||||
|
||||
EventManager.RegisterClassHandler(
|
||||
typeof(UIElement),
|
||||
Keyboard.PreviewGotKeyboardFocusEvent,
|
||||
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
|
||||
}
|
||||
|
||||
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||
{
|
||||
Title = e.NewFocus.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Examples/Nodify.Calculator/Models/SaveGraphModel.cs
Normal file
22
Examples/Nodify.Calculator/Models/SaveGraphModel.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator.Models
|
||||
{
|
||||
public class SaveGraphModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<SaveNodes> Nodes { get; set; } = new List<SaveNodes>();
|
||||
}
|
||||
|
||||
public class SaveNodes
|
||||
{
|
||||
public Point Location { get; set; }
|
||||
}
|
||||
}
|
||||
13
Examples/Nodify.Calculator/Models/SwaggerNodeModel.cs
Normal file
13
Examples/Nodify.Calculator/Models/SwaggerNodeModel.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Nodify.Calculator.Models
|
||||
{
|
||||
public class SwaggerNodeModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string OPType { get; set; } = string.Empty;
|
||||
public List<string> InputNames { get; set; } = new List<string>();
|
||||
public string SwaggerFileName { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
12
Examples/Nodify.Calculator/Models/VariableModel.cs
Normal file
12
Examples/Nodify.Calculator/Models/VariableModel.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Nodify.Calculator.Models
|
||||
{
|
||||
public class VariableModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string VariableType { get; set; } = "string";
|
||||
public string DefaultValue { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
30
Examples/Nodify.Calculator/Nodify.Calculator.csproj
Normal file
30
Examples/Nodify.Calculator/Nodify.Calculator.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFrameworks>net9-windows;</TargetFrameworks>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="leet swagger.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="leet swagger.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LiteDB" Version="5.0.21" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageReference Include="NSwag.CodeGeneration.CSharp" Version="14.5.0" />
|
||||
<PackageReference Include="NSwag.Core" Version="14.5.0" />
|
||||
<PackageReference Include="StringMath" Version="4.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Nodify\Nodify.csproj" />
|
||||
<ProjectReference Include="..\Nodify.Shared\Nodify.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
44
Examples/Nodify.Calculator/OperationGraphViewModel.cs
Normal file
44
Examples/Nodify.Calculator/OperationGraphViewModel.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class OperationGraphViewModel : CalculatorOperationViewModel
|
||||
{
|
||||
private Size _size;
|
||||
public Size DesiredSize
|
||||
{
|
||||
get => _size;
|
||||
set => SetProperty(ref _size, value);
|
||||
}
|
||||
|
||||
private Size _prevSize;
|
||||
|
||||
private bool _isExpanded = true;
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isExpanded, value))
|
||||
{
|
||||
if (_isExpanded)
|
||||
{
|
||||
DesiredSize = _prevSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
_prevSize = Size;
|
||||
// Fit content
|
||||
DesiredSize = new Size(double.NaN, double.NaN);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public OperationGraphViewModel()
|
||||
{
|
||||
InnerCalculator.Operations[0].Location = new Point(50, 50);
|
||||
InnerCalculator.Operations[1].Location = new Point(200, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Examples/Nodify.Calculator/OperationGroupViewModel.cs
Normal file
14
Examples/Nodify.Calculator/OperationGroupViewModel.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class OperationGroupViewModel : OperationViewModel
|
||||
{
|
||||
private Size _size;
|
||||
public Size GroupSize
|
||||
{
|
||||
get => _size;
|
||||
set => SetProperty(ref _size, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Examples/Nodify.Calculator/OperationInfoViewModel.cs
Normal file
36
Examples/Nodify.Calculator/OperationInfoViewModel.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public enum OperationType
|
||||
{
|
||||
Normal,
|
||||
Expando,
|
||||
Expression,
|
||||
Calculator,
|
||||
Group,
|
||||
Graph,
|
||||
API,
|
||||
System
|
||||
}
|
||||
|
||||
public class OperationInfoViewModel
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public OperationType Type { get; set; }
|
||||
public IOperation? Operation { get; set; }
|
||||
public SystemOperations sysOp { get; set; }
|
||||
public List<string?> Input { get; } = new List<string?>();
|
||||
public List<string?> Output { get; } = new List<string?>();
|
||||
public uint MinInput { get; set; }
|
||||
public uint MaxInput { get; set; }
|
||||
public string InputType { get; set; } = string.Empty;
|
||||
public string OPType { get; set; }
|
||||
public bool IsFlowNode { get; set; }
|
||||
public bool IsModelNode { get; set; }
|
||||
public string ClassName { get; set; }
|
||||
public string VariableType { get; set; } = string.Empty;
|
||||
public string DefaultValue { get; set; } = string.Empty;
|
||||
public bool IsSimpleVariable { get; set; }
|
||||
}
|
||||
}
|
||||
128
Examples/Nodify.Calculator/OperationViewModel.cs
Normal file
128
Examples/Nodify.Calculator/OperationViewModel.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using LiteDB;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class OperationViewModel : ObservableObject
|
||||
{
|
||||
public OperationViewModel()
|
||||
{
|
||||
Input.WhenAdded(x =>
|
||||
{
|
||||
x.Operation = this;
|
||||
x.IsInput = true;
|
||||
x.PropertyChanged += OnInputValueChanged;
|
||||
})
|
||||
.WhenRemoved(x =>
|
||||
{
|
||||
x.PropertyChanged -= OnInputValueChanged;
|
||||
});
|
||||
|
||||
Output.WhenAdded(c =>
|
||||
{
|
||||
c.Operation = this;
|
||||
c.IsInput = false;
|
||||
c.Value = 0;
|
||||
c.PropertyChanged += OnInputValueChanged;
|
||||
})
|
||||
.WhenRemoved(x =>
|
||||
{
|
||||
x.PropertyChanged -= OnInputValueChanged;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnInputValueChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(ConnectorViewModel.Value))
|
||||
{
|
||||
OnInputValueChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private Point _location;
|
||||
public Point Location
|
||||
{
|
||||
get => _location;
|
||||
set => SetProperty(ref _location, value);
|
||||
}
|
||||
|
||||
private Size _size;
|
||||
public Size Size
|
||||
{
|
||||
get => _size;
|
||||
set => SetProperty(ref _size, value);
|
||||
}
|
||||
|
||||
private string? _title;
|
||||
public string? Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
|
||||
private bool _isSelected;
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetProperty(ref _isSelected, value);
|
||||
}
|
||||
|
||||
public bool IsReadOnly { get; set; }
|
||||
|
||||
[BsonIgnore]
|
||||
private IOperation? _operation;
|
||||
[BsonIgnore]
|
||||
public IOperation? Operation
|
||||
{
|
||||
get => _operation;
|
||||
set => SetProperty(ref _operation, value)
|
||||
.Then(OnInputValueChanged);
|
||||
}
|
||||
|
||||
private string nodeId;
|
||||
|
||||
public string NodeId
|
||||
{
|
||||
get { return nodeId; }
|
||||
set => SetProperty(ref nodeId, value);
|
||||
}
|
||||
|
||||
[BsonIgnore]
|
||||
public NodifyObservableCollection<ConnectorViewModel> Input { get; } = new NodifyObservableCollection<ConnectorViewModel>();
|
||||
|
||||
[BsonIgnore]
|
||||
public NodifyObservableCollection<ConnectorViewModel> Output { get; } = new NodifyObservableCollection<ConnectorViewModel>();
|
||||
|
||||
//private ConnectorViewModel? _output;
|
||||
//public ConnectorViewModel? Output
|
||||
//{
|
||||
// get => _output;
|
||||
// set
|
||||
// {
|
||||
// if (SetProperty(ref _output, value) && _output != null)
|
||||
// {
|
||||
// _output.Operation = this;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
protected virtual void OnInputValueChanged()
|
||||
{
|
||||
//if (Output != null && Operation != null)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// var input = Input.Select(i => i.Value).ToArray();
|
||||
// Output.Value = Operation?.Execute(input) ?? 0;
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Examples/Nodify.Calculator/Operations/BinaryOperation.cs
Normal file
14
Examples/Nodify.Calculator/Operations/BinaryOperation.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class BinaryOperation : IOperation
|
||||
{
|
||||
private readonly Func<double, double, double> _func;
|
||||
|
||||
public BinaryOperation(Func<double, double, double> func) => _func = func;
|
||||
|
||||
public double Execute(params double[] operands)
|
||||
=> _func.Invoke(operands[0], operands[1]);
|
||||
}
|
||||
}
|
||||
7
Examples/Nodify.Calculator/Operations/IOperation.cs
Normal file
7
Examples/Nodify.Calculator/Operations/IOperation.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public interface IOperation
|
||||
{
|
||||
double Execute(params double[] operands);
|
||||
}
|
||||
}
|
||||
469
Examples/Nodify.Calculator/Operations/OperationFactory.cs
Normal file
469
Examples/Nodify.Calculator/Operations/OperationFactory.cs
Normal file
@@ -0,0 +1,469 @@
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Shapes;
|
||||
using Size = System.Windows.Size;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public static class OperationFactory
|
||||
{
|
||||
|
||||
public static List<OperationInfoViewModel> GetSystemNodes()
|
||||
{
|
||||
List<OperationInfoViewModel> systemNodes = new List<OperationInfoViewModel>();
|
||||
|
||||
var copynode = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "COPY",
|
||||
Type = OperationType.System,
|
||||
OPType = "copy",
|
||||
IsFlowNode = false,
|
||||
};
|
||||
copynode.Input.Add("");
|
||||
copynode.Output.Add("");
|
||||
copynode.Output.Add("");
|
||||
|
||||
var begin = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "Begin",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.BEGIN,
|
||||
IsFlowNode = true
|
||||
};
|
||||
begin.Output.Add("");
|
||||
|
||||
var ending = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "End",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.END,
|
||||
IsFlowNode = true
|
||||
};
|
||||
ending.Input.Add("");
|
||||
|
||||
var debugAndCreateModels = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "Debug & Create Model",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.DEBUG_AND_CREATE_MODEL,
|
||||
IsFlowNode = true
|
||||
};
|
||||
debugAndCreateModels.Input.Add("");
|
||||
debugAndCreateModels.Output.Add("");
|
||||
|
||||
var jsonParseNode = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "Parse Json",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.PARSEJSON,
|
||||
IsFlowNode = false
|
||||
};
|
||||
jsonParseNode.Input.Add("");
|
||||
jsonParseNode.Output.Add("");
|
||||
|
||||
var splitNode = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "Split",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.SPLIT,
|
||||
IsFlowNode = false
|
||||
};
|
||||
splitNode.Input.Add("");
|
||||
splitNode.Output.Add("");
|
||||
|
||||
var takeNode = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "TAKE",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.TAKE,
|
||||
IsFlowNode = false
|
||||
};
|
||||
takeNode.Input.Add("");
|
||||
takeNode.Output.Add("");
|
||||
|
||||
var authNode = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "Auth",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.AUTH,
|
||||
IsFlowNode = true
|
||||
};
|
||||
authNode.Input.Add("Base URL");
|
||||
authNode.Input.Add("Auth Type");
|
||||
authNode.Input.Add("Token");
|
||||
authNode.Input.Add("API Key");
|
||||
authNode.Input.Add("Username");
|
||||
authNode.Input.Add("Password");
|
||||
|
||||
systemNodes.Add(authNode);
|
||||
systemNodes.Add(copynode);
|
||||
systemNodes.Add(begin);
|
||||
systemNodes.Add(ending);
|
||||
systemNodes.Add(debugAndCreateModels);
|
||||
systemNodes.Add(jsonParseNode);
|
||||
systemNodes.Add(splitNode);
|
||||
systemNodes.Add(takeNode);
|
||||
return systemNodes;
|
||||
}
|
||||
|
||||
public static List<OperationInfoViewModel> GetOperationsInfo(Type container)
|
||||
{
|
||||
List<OperationInfoViewModel> result = new List<OperationInfoViewModel>();
|
||||
|
||||
foreach (var method in container.GetMethods())
|
||||
{
|
||||
if (method.IsStatic)
|
||||
{
|
||||
OperationInfoViewModel op = new OperationInfoViewModel
|
||||
{
|
||||
Title = method.Name
|
||||
};
|
||||
|
||||
var attr = method.GetCustomAttribute<OperationAttribute>();
|
||||
var para = method.GetParameters();
|
||||
|
||||
bool generateInputNames = true;
|
||||
|
||||
op.Type = OperationType.Normal;
|
||||
|
||||
if (para.Length == 2)
|
||||
{
|
||||
var delType = typeof(Func<double, double, double>);
|
||||
var del = (Func<double, double, double>)Delegate.CreateDelegate(delType, method);
|
||||
|
||||
op.Operation = new BinaryOperation(del);
|
||||
}
|
||||
else if (para.Length == 1)
|
||||
{
|
||||
if (para[0].ParameterType.IsArray)
|
||||
{
|
||||
op.Type = OperationType.Expando;
|
||||
|
||||
var delType = typeof(Func<double[], double>);
|
||||
var del = (Func<double[], double>)Delegate.CreateDelegate(delType, method);
|
||||
|
||||
op.Operation = new ParamsOperation(del);
|
||||
op.MaxInput = int.MaxValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var delType = typeof(Func<double, double>);
|
||||
var del = (Func<double, double>)Delegate.CreateDelegate(delType, method);
|
||||
|
||||
op.Operation = new UnaryOperation(del);
|
||||
}
|
||||
}
|
||||
else if (para.Length == 0)
|
||||
{
|
||||
var delType = typeof(Func<double>);
|
||||
var del = (Func<double>)Delegate.CreateDelegate(delType, method);
|
||||
|
||||
op.Operation = new ValueOperation(del);
|
||||
}
|
||||
|
||||
if (attr != null)
|
||||
{
|
||||
op.MinInput = attr.MinInput;
|
||||
op.MaxInput = attr.MaxInput;
|
||||
generateInputNames = attr.GenerateInputNames;
|
||||
}
|
||||
else
|
||||
{
|
||||
op.MinInput = (uint)para.Length;
|
||||
op.MaxInput = (uint)para.Length;
|
||||
}
|
||||
|
||||
foreach (var param in para)
|
||||
{
|
||||
op.Input.Add(generateInputNames ? param.Name : null);
|
||||
}
|
||||
|
||||
for (int i = op.Input.Count; i < op.MinInput; i++)
|
||||
{
|
||||
op.Input.Add(null);
|
||||
}
|
||||
op.Output.Add("");
|
||||
result.Add(op);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static OperationViewModel GetOperation(OperationInfoViewModel info)
|
||||
{
|
||||
var input = info.Input.Select(i => new ConnectorViewModel
|
||||
{
|
||||
Title = i
|
||||
});
|
||||
|
||||
switch (info.Type)
|
||||
{
|
||||
case OperationType.Expression:
|
||||
var eo = new ExpressionOperationViewModel
|
||||
{
|
||||
Title = info.Title,
|
||||
Operation = info.Operation,
|
||||
Expression = "1 + sin {a} + cos {b}"
|
||||
};
|
||||
eo.Output.Add(new ConnectorViewModel());
|
||||
return eo;
|
||||
case OperationType.Calculator:
|
||||
return new CalculatorOperationViewModel
|
||||
{
|
||||
Title = info.Title,
|
||||
Operation = info.Operation,
|
||||
};
|
||||
|
||||
case OperationType.Expando:
|
||||
var o = new ExpandoOperationViewModel
|
||||
{
|
||||
MaxInput = info.MaxInput,
|
||||
MinInput = info.MinInput,
|
||||
Title = info.Title,
|
||||
Operation = info.Operation
|
||||
};
|
||||
o.Output.Add(new ConnectorViewModel());
|
||||
o.Input.AddRange(input);
|
||||
return o;
|
||||
|
||||
case OperationType.Group:
|
||||
return new OperationGroupViewModel
|
||||
{
|
||||
Title = info.Title,
|
||||
};
|
||||
|
||||
case OperationType.Graph:
|
||||
return new OperationGraphViewModel
|
||||
{
|
||||
Title = info.Title,
|
||||
DesiredSize = new Size(420, 250)
|
||||
};
|
||||
|
||||
case OperationType.API:
|
||||
var _o = new APIOperationViewModel
|
||||
{
|
||||
Title = info.Title,
|
||||
OperationType = info.OPType.ToUpper()
|
||||
};
|
||||
var connectorViewModel = new ConnectorViewModel()
|
||||
{
|
||||
Title = "",
|
||||
Shape = ConnectorShape.Triangle
|
||||
};
|
||||
var connectorViewModel2 = new ConnectorViewModel()
|
||||
{
|
||||
Title = "",
|
||||
Shape = ConnectorShape.Triangle,
|
||||
IsInput = false
|
||||
};
|
||||
_o.Output.Add(connectorViewModel2);
|
||||
_o.Output.Add(new ConnectorViewModel());
|
||||
_o.Input.Add(connectorViewModel);
|
||||
foreach (var item in input)
|
||||
{
|
||||
item.ConnectorColor = Color.GreenYellow;
|
||||
_o.Input.Add(item);
|
||||
}
|
||||
//_o.Input.AddRange(input);
|
||||
return _o;
|
||||
case OperationType.System:
|
||||
if (info.sysOp == SystemOperations.AUTH)
|
||||
{
|
||||
var authOp = new AuthOperationViewModel
|
||||
{
|
||||
Title = info.Title,
|
||||
SystemOperationType = SystemOperations.AUTH
|
||||
};
|
||||
// Add flow connectors (triangle)
|
||||
var flowIn = new ConnectorViewModel()
|
||||
{
|
||||
Title = "",
|
||||
Shape = ConnectorShape.Triangle
|
||||
};
|
||||
var flowOut = new ConnectorViewModel()
|
||||
{
|
||||
Title = "",
|
||||
Shape = ConnectorShape.Triangle,
|
||||
IsInput = false
|
||||
};
|
||||
authOp.Input.Add(flowIn);
|
||||
authOp.Output.Add(flowOut);
|
||||
// Add data input connectors
|
||||
foreach (var inp in input)
|
||||
{
|
||||
inp.ConnectorColor = System.Drawing.Color.Orange;
|
||||
authOp.Input.Add(inp);
|
||||
}
|
||||
return authOp;
|
||||
}
|
||||
|
||||
var sysOp = new SystemOperationViewModel
|
||||
{
|
||||
Title = info.Title,
|
||||
SystemOperationType = info.sysOp
|
||||
};
|
||||
|
||||
if (info.sysOp == SystemOperations.GET_SET && info.IsModelNode)
|
||||
{
|
||||
if (info.Title == "GET")
|
||||
{
|
||||
info.Output.Add("");
|
||||
info.IsFlowNode = false;
|
||||
}
|
||||
else if (info.Title == "SET")
|
||||
{
|
||||
info.Input.Add("");
|
||||
var customModelDir = "CustomModels";
|
||||
Directory.CreateDirectory(customModelDir);
|
||||
var flpath = System.IO.Path.Combine(customModelDir, info.ClassName + ".cs");
|
||||
if (File.Exists(flpath))
|
||||
{
|
||||
//Read all the properties from the file
|
||||
var fileContent = File.ReadAllText(flpath);
|
||||
|
||||
// Parse and analyze properties
|
||||
var properties = GetPropertiesFromClass(fileContent);
|
||||
|
||||
// Print out the extracted properties and their types
|
||||
Console.WriteLine("Properties found:");
|
||||
foreach (var property in properties)
|
||||
{
|
||||
Console.WriteLine($"Property Name: {property.Name}, Type: {property.Type}");
|
||||
info.Output.Add(property.Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
info.IsFlowNode = true;
|
||||
}
|
||||
var flTitle = $"{info.Title} {info.ClassName}";
|
||||
sysOp.Title = flTitle;
|
||||
}
|
||||
|
||||
if (info.sysOp == SystemOperations.GET_SET && info.IsSimpleVariable)
|
||||
{
|
||||
var varLabel = $"{info.Title} {info.ClassName} ({info.VariableType})";
|
||||
sysOp.Title = varLabel;
|
||||
|
||||
if (info.Title == "GET")
|
||||
{
|
||||
// GET variable: output the value, no flow needed
|
||||
info.Output.Add("Value");
|
||||
info.IsFlowNode = false;
|
||||
}
|
||||
else if (info.Title == "SET")
|
||||
{
|
||||
// SET variable: input connector for the value, flow node
|
||||
info.Input.Add("Value");
|
||||
info.IsFlowNode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.sysOp != SystemOperations.BEGIN &&
|
||||
info.sysOp != SystemOperations.END &&
|
||||
info.IsFlowNode)
|
||||
{
|
||||
var flowinputnode = new ConnectorViewModel()
|
||||
{
|
||||
Title = "",
|
||||
Shape = ConnectorShape.Triangle
|
||||
};
|
||||
var flowoutputnode = new ConnectorViewModel()
|
||||
{
|
||||
Title = "",
|
||||
Shape = ConnectorShape.Triangle,
|
||||
IsInput = false
|
||||
};
|
||||
sysOp.Input.Add(flowinputnode);
|
||||
sysOp.Output.Add(flowoutputnode);
|
||||
}
|
||||
foreach (var item in info.Output)
|
||||
{
|
||||
var out1 = new ConnectorViewModel()
|
||||
{
|
||||
Title = string.IsNullOrEmpty(item) ? "" : item,
|
||||
IsInput = false,
|
||||
ConnectorColor = Color.DarkRed,
|
||||
Shape = (info.sysOp == SystemOperations.BEGIN || info.sysOp == SystemOperations.END) ? ConnectorShape.Triangle : ConnectorShape.Circle,
|
||||
};
|
||||
if (out1.Shape != ConnectorShape.Triangle)
|
||||
{
|
||||
out1.ConnectorColor = Color.DeepPink;
|
||||
}
|
||||
sysOp.Output.Add(out1);
|
||||
}
|
||||
foreach (var item in input)
|
||||
{
|
||||
item.Shape = (info.sysOp == SystemOperations.BEGIN || info.sysOp == SystemOperations.END) ? ConnectorShape.Triangle : ConnectorShape.Circle;
|
||||
sysOp.Input.Add(item);
|
||||
}
|
||||
return sysOp;
|
||||
default:
|
||||
{
|
||||
var op = new OperationViewModel
|
||||
{
|
||||
Title = info.Title,
|
||||
Operation = info.Operation,
|
||||
};
|
||||
var ccv = new ConnectorViewModel()
|
||||
{
|
||||
IsInput = false,
|
||||
Shape = ConnectorShape.Circle,
|
||||
Title = ""
|
||||
};
|
||||
op.Output.Add(ccv);
|
||||
|
||||
op.Input.AddRange(input);
|
||||
return op;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static List<CustomProperty> GetPropertiesFromClass(string classContent)
|
||||
{
|
||||
// Parse the C# class content using Roslyn
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(classContent);
|
||||
var root = syntaxTree.GetRoot();
|
||||
|
||||
// Find the first class declaration (you can refine this if there are multiple classes)
|
||||
var classDeclaration = root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
|
||||
|
||||
// List to store extracted properties
|
||||
var properties = new List<CustomProperty>();
|
||||
|
||||
if (classDeclaration != null)
|
||||
{
|
||||
// Look for property declarations inside the class
|
||||
var propertyDeclarations = classDeclaration.Members.OfType<PropertyDeclarationSyntax>();
|
||||
foreach (var property in propertyDeclarations)
|
||||
{
|
||||
var propertyName = property.Identifier.Text; // Property name
|
||||
var propertyType = property.Type.ToString(); // Property type
|
||||
|
||||
// Add the property and its type to the list
|
||||
properties.Add(new CustomProperty
|
||||
{
|
||||
Name = propertyName,
|
||||
Type = propertyType
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CustomProperty // <- Renamed to avoid conflicts with System.Reflection.PropertyInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
35
Examples/Nodify.Calculator/Operations/OperationsContainer.cs
Normal file
35
Examples/Nodify.Calculator/Operations/OperationsContainer.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public static class OperationsContainer
|
||||
{
|
||||
[Operation(MinInput = 2, MaxInput = 10, GenerateInputNames = false)]
|
||||
public static double Add(params double[] operands)
|
||||
=> operands.Sum();
|
||||
|
||||
[Operation(MinInput = 2, MaxInput = 10, GenerateInputNames = false)]
|
||||
public static double Multiply(params double[] operands)
|
||||
=> operands.Aggregate((x, y) => x * y);
|
||||
|
||||
public static double Divide(double a, double b)
|
||||
=> a / b;
|
||||
|
||||
public static double Subtract(double a, double b)
|
||||
=> a - b;
|
||||
|
||||
public static double Pow(double value, double exp)
|
||||
=> (double)Math.Pow((double)value, (double)exp);
|
||||
|
||||
public static double PI()
|
||||
=> (double)Math.PI;
|
||||
}
|
||||
|
||||
public sealed class OperationAttribute : Attribute
|
||||
{
|
||||
public uint MaxInput { get; set; }
|
||||
public uint MinInput { get; set; }
|
||||
public bool GenerateInputNames { get; set; }
|
||||
}
|
||||
}
|
||||
14
Examples/Nodify.Calculator/Operations/ParamsOperation.cs
Normal file
14
Examples/Nodify.Calculator/Operations/ParamsOperation.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class ParamsOperation : IOperation
|
||||
{
|
||||
private readonly Func<double[], double> _func;
|
||||
|
||||
public ParamsOperation(Func<double[], double> func) => _func = func;
|
||||
|
||||
public double Execute(params double[] operands)
|
||||
=> _func.Invoke(operands);
|
||||
}
|
||||
}
|
||||
14
Examples/Nodify.Calculator/Operations/UnaryOperation.cs
Normal file
14
Examples/Nodify.Calculator/Operations/UnaryOperation.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class UnaryOperation : IOperation
|
||||
{
|
||||
private readonly Func<double, double> _func;
|
||||
|
||||
public UnaryOperation(Func<double, double> func) => _func = func;
|
||||
|
||||
public double Execute(params double[] operands)
|
||||
=> _func.Invoke(operands[0]);
|
||||
}
|
||||
}
|
||||
14
Examples/Nodify.Calculator/Operations/ValueOperation.cs
Normal file
14
Examples/Nodify.Calculator/Operations/ValueOperation.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class ValueOperation : IOperation
|
||||
{
|
||||
private readonly Func<double> _func;
|
||||
|
||||
public ValueOperation(Func<double> func) => _func = func;
|
||||
|
||||
public double Execute(params double[] operands)
|
||||
=> _func();
|
||||
}
|
||||
}
|
||||
50
Examples/Nodify.Calculator/OperationsExtensions.cs
Normal file
50
Examples/Nodify.Calculator/OperationsExtensions.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public static class OperationsExtensions
|
||||
{
|
||||
public static Rect GetBoundingBox(this IEnumerable<OperationViewModel> nodes, double padding = 0, int gridCellSize = 15)
|
||||
{
|
||||
var minX = double.MaxValue;
|
||||
var minY = double.MaxValue;
|
||||
|
||||
var maxX = double.MinValue;
|
||||
var maxY = double.MinValue;
|
||||
|
||||
const int width = 200; //node.Width
|
||||
const int height = 100; //node.Height
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.Location.X < minX)
|
||||
{
|
||||
minX = node.Location.X;
|
||||
}
|
||||
|
||||
if (node.Location.Y < minY)
|
||||
{
|
||||
minY = node.Location.Y;
|
||||
}
|
||||
|
||||
var sizeX = node.Location.X + width;
|
||||
if (sizeX > maxX)
|
||||
{
|
||||
maxX = sizeX;
|
||||
}
|
||||
|
||||
var sizeY = node.Location.Y + height;
|
||||
if (sizeY > maxY)
|
||||
{
|
||||
maxY = sizeY;
|
||||
}
|
||||
}
|
||||
|
||||
var result = new Rect(minX - padding, minY - padding, maxX - minX + padding * 2, maxY - minY + padding * 2);
|
||||
result.X = (int)result.X / gridCellSize * gridCellSize;
|
||||
result.Y = (int)result.Y / gridCellSize * gridCellSize;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
Examples/Nodify.Calculator/OperationsMenuView.xaml
Normal file
81
Examples/Nodify.Calculator/OperationsMenuView.xaml
Normal file
@@ -0,0 +1,81 @@
|
||||
<UserControl x:Class="Nodify.Calculator.OperationsMenuView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Nodify.Calculator"
|
||||
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
|
||||
mc:Ignorable="d"
|
||||
MinWidth="250"
|
||||
d:DesignHeight="400"
|
||||
d:DesignWidth="250"
|
||||
d:DataContext="{d:DesignInstance local:OperationsMenuViewModel}"
|
||||
Visibility="{Binding IsVisible, Converter={shared:BooleanToVisibilityConverter}}">
|
||||
<UserControl.Resources>
|
||||
<Style TargetType="{x:Type TextBlock}"
|
||||
BasedOn="{StaticResource {x:Type TextBlock}}">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource ForegroundBrush}" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Padding="7"
|
||||
CornerRadius="3"
|
||||
Background="{DynamicResource Node.BackgroundBrush}"
|
||||
BorderBrush="{StaticResource NodifyEditor.SelectionRectangleStrokeBrush}"
|
||||
BorderThickness="2">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ItemsControl Grid.Row="1"
|
||||
x:Name="OperationsList"
|
||||
Focusable="True"
|
||||
KeyboardNavigation.TabNavigation="Cycle"
|
||||
ItemsSource="{Binding MenuAvailableOperations}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:OperationInfoViewModel}">
|
||||
<Button Content="{Binding Title}"
|
||||
Command="{Binding DataContext.CreateOperationCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
ClickMode="Press"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
Foreground="{DynamicResource ForegroundBrush}"
|
||||
Padding="3"
|
||||
Cursor="Hand"
|
||||
HorizontalContentAlignment="Left">
|
||||
<Button.Style>
|
||||
<Style TargetType="{x:Type Button}">
|
||||
<Setter Property="FocusVisualStyle"
|
||||
Value="{StaticResource {x:Static SystemParameters.FocusVisualStyleKey}}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type Button}">
|
||||
<Border Name="Border"
|
||||
Background="{TemplateBinding Background}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver"
|
||||
Value="True">
|
||||
<Setter Property="Background"
|
||||
TargetName="Border"
|
||||
Value="{DynamicResource NodeInput.BorderBrush}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
51
Examples/Nodify.Calculator/OperationsMenuView.xaml.cs
Normal file
51
Examples/Nodify.Calculator/OperationsMenuView.xaml.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public partial class OperationsMenuView : UserControl
|
||||
{
|
||||
private readonly WeakReference<UIElement?> _focusToRestore = new WeakReference<UIElement?>(null!);
|
||||
|
||||
public OperationsMenuView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
IsVisibleChanged += OperationsMenuView_IsVisibleChanged;
|
||||
}
|
||||
|
||||
private void OperationsMenuView_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.NewValue is true)
|
||||
{
|
||||
_focusToRestore.SetTarget(Keyboard.FocusedElement as UIElement);
|
||||
Dispatcher.BeginInvoke(new Action(() => OperationsList.Focus()), System.Windows.Threading.DispatcherPriority.Input);
|
||||
}
|
||||
else if (e.NewValue is false)
|
||||
{
|
||||
if (_focusToRestore.TryGetTarget(out var elementToFocus))
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
elementToFocus!.Focus();
|
||||
}), System.Windows.Threading.DispatcherPriority.Input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
SetCurrentValue(VisibilityProperty, Visibility.Collapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
380
Examples/Nodify.Calculator/OperationsMenuViewModel.cs
Normal file
380
Examples/Nodify.Calculator/OperationsMenuViewModel.cs
Normal file
@@ -0,0 +1,380 @@
|
||||
using LiteDB;
|
||||
using Microsoft.Win32;
|
||||
using NSwag;
|
||||
using NSwag.CodeGeneration.CSharp;
|
||||
using Nodify.Calculator.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class OperationsMenuViewModel : ObservableObject
|
||||
{
|
||||
private bool _isVisible;
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _isVisible, value);
|
||||
if (!value)
|
||||
{
|
||||
Closed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Point _location;
|
||||
public Point Location
|
||||
{
|
||||
get => _location;
|
||||
set => SetProperty(ref _location, value);
|
||||
}
|
||||
|
||||
public event Action? Closed;
|
||||
|
||||
public void OpenAt(Point targetLocation)
|
||||
{
|
||||
MenuAvailableOperations.Clear();
|
||||
MenuAvailableOperations.AddRange(AvailableOperations);
|
||||
Close();
|
||||
Location = targetLocation;
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
public void OpenOnlyGetSetVariable(Point targetLocation, string className)
|
||||
{
|
||||
var getVM = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "GET",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.GET_SET,
|
||||
IsModelNode = true,
|
||||
ClassName = className,
|
||||
};
|
||||
var setVM = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "SET",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.GET_SET,
|
||||
IsModelNode = true,
|
||||
ClassName = className
|
||||
};
|
||||
MenuAvailableOperations.Clear();
|
||||
MenuAvailableOperations.Add(getVM);
|
||||
MenuAvailableOperations.Add(setVM);
|
||||
Close();
|
||||
Location = targetLocation;
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
public void OpenGetSetForVariable(Point targetLocation, OperationInfoViewModel variableInfo)
|
||||
{
|
||||
var getVM = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "GET",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.GET_SET,
|
||||
IsSimpleVariable = true,
|
||||
VariableType = variableInfo.VariableType,
|
||||
DefaultValue = variableInfo.DefaultValue,
|
||||
ClassName = variableInfo.Title ?? string.Empty
|
||||
};
|
||||
var setVM = new OperationInfoViewModel()
|
||||
{
|
||||
Title = "SET",
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.GET_SET,
|
||||
IsSimpleVariable = true,
|
||||
VariableType = variableInfo.VariableType,
|
||||
DefaultValue = variableInfo.DefaultValue,
|
||||
ClassName = variableInfo.Title ?? string.Empty
|
||||
};
|
||||
MenuAvailableOperations.Clear();
|
||||
MenuAvailableOperations.Add(getVM);
|
||||
MenuAvailableOperations.Add(setVM);
|
||||
Close();
|
||||
Location = targetLocation;
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
|
||||
public void Close()
|
||||
{
|
||||
IsVisible = false;
|
||||
}
|
||||
|
||||
public NodifyObservableCollection<OperationInfoViewModel> MenuAvailableOperations { get; }
|
||||
public NodifyObservableCollection<OperationInfoViewModel> AvailableOperations { get; }
|
||||
public NodifyObservableCollection<OperationInfoViewModel> SwaggerOperations { get; }
|
||||
public NodifyObservableCollection<OperationInfoViewModel> AvailableModels { get; }
|
||||
public NodifyObservableCollection<OperationInfoViewModel> AvailableVariables { get; }
|
||||
public INodifyCommand CreateOperationCommand { get; }
|
||||
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[BsonIgnore]
|
||||
private readonly CalculatorViewModel _calculator;
|
||||
|
||||
public OperationsMenuViewModel(CalculatorViewModel calculator)
|
||||
{
|
||||
_calculator = calculator;
|
||||
List<OperationInfoViewModel> operations = new List<OperationInfoViewModel>();
|
||||
AvailableModels = new NodifyObservableCollection<OperationInfoViewModel>();
|
||||
AvailableVariables = new NodifyObservableCollection<OperationInfoViewModel>();
|
||||
LoadVariablesFromDb();
|
||||
|
||||
var customModelDir = "CustomModels";
|
||||
Directory.CreateDirectory(customModelDir);
|
||||
var dirInfo = new DirectoryInfo(customModelDir);
|
||||
var allFiles = dirInfo.GetFiles("*.cs");
|
||||
foreach (var item in allFiles)
|
||||
{
|
||||
var flName = Path.GetFileNameWithoutExtension(item.Name);
|
||||
var opVInfo = new OperationInfoViewModel()
|
||||
{
|
||||
Title = flName,
|
||||
IsModelNode = true,
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.GET_SET,
|
||||
ClassName = flName
|
||||
};
|
||||
AvailableModels.Add(opVInfo);
|
||||
}
|
||||
|
||||
operations.AddRange(OperationFactory.GetSystemNodes());
|
||||
operations.AddRange(OperationFactory.GetOperationsInfo(typeof(OperationsContainer)));
|
||||
|
||||
SwaggerOperations = new NodifyObservableCollection<OperationInfoViewModel>();
|
||||
LoadSwaggerNodesFromDb();
|
||||
|
||||
operations.AddRange(SwaggerOperations);
|
||||
|
||||
AvailableOperations = new NodifyObservableCollection<OperationInfoViewModel>(operations);
|
||||
MenuAvailableOperations = new NodifyObservableCollection<OperationInfoViewModel>(operations);
|
||||
CreateOperationCommand = new DelegateCommand<OperationInfoViewModel>(CreateOperation);
|
||||
ImportSwaggerCommand = new DelegateCommand(ImportSwagger);
|
||||
AddVariableCommand = new DelegateCommand(AddVariable);
|
||||
}
|
||||
|
||||
|
||||
public void AddNewModel(OperationInfoViewModel opModel)
|
||||
{
|
||||
if (System.Windows.Application.Current.Dispatcher.CheckAccess())
|
||||
{
|
||||
// Add directly if on UI thread
|
||||
AvailableModels.Add(opModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, marshal the call to the UI thread
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
AvailableModels.Add(opModel);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public INodifyCommand ImportSwaggerCommand { get; }
|
||||
public INodifyCommand AddVariableCommand { get; }
|
||||
|
||||
private void AddVariable()
|
||||
{
|
||||
var dialog = new AddVariableDialog();
|
||||
dialog.Owner = System.Windows.Application.Current.MainWindow;
|
||||
if (dialog.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
var varName = dialog.VariableName;
|
||||
var varType = dialog.VariableType;
|
||||
var defaultVal = dialog.DefaultValue;
|
||||
|
||||
// Check for duplicate name
|
||||
if (AvailableVariables.Any(v => v.Title == varName))
|
||||
{
|
||||
MessageBox.Show($"A variable named '{varName}' already exists.", "Duplicate Variable", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var varInfo = CreateVariableInfo(varName, varType, defaultVal);
|
||||
AvailableVariables.Add(varInfo);
|
||||
SaveVariableToDb(varName, varType, defaultVal);
|
||||
}
|
||||
|
||||
private OperationInfoViewModel CreateVariableInfo(string name, string varType, string defaultValue)
|
||||
{
|
||||
return new OperationInfoViewModel
|
||||
{
|
||||
Title = name,
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.GET_SET,
|
||||
IsSimpleVariable = true,
|
||||
VariableType = varType,
|
||||
DefaultValue = defaultValue,
|
||||
IsModelNode = false
|
||||
};
|
||||
}
|
||||
|
||||
private void SaveVariableToDb(string name, string varType, string defaultValue)
|
||||
{
|
||||
using var db = new LiteDbHelper<VariableModel>("Variables");
|
||||
db.Insert(new VariableModel
|
||||
{
|
||||
Name = name,
|
||||
VariableType = varType,
|
||||
DefaultValue = defaultValue
|
||||
});
|
||||
}
|
||||
|
||||
private void LoadVariablesFromDb()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var db = new LiteDbHelper<VariableModel>("Variables");
|
||||
foreach (var v in db.FindAll())
|
||||
{
|
||||
AvailableVariables.Add(CreateVariableInfo(v.Name, v.VariableType, v.DefaultValue));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// DB may not exist yet
|
||||
}
|
||||
}
|
||||
|
||||
private void ImportSwagger()
|
||||
{
|
||||
var openFileDialog = new OpenFileDialog
|
||||
{
|
||||
Filter = "JSON files (*.json)|*.json|Text files (*.txt)|*.txt|All files (*.*)|*.*",
|
||||
Title = "Import Swagger JSON File"
|
||||
};
|
||||
|
||||
if (openFileDialog.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var nodes = ParseSwaggerFile(openFileDialog.FileName);
|
||||
var fileName = Path.GetFileName(openFileDialog.FileName);
|
||||
SaveSwaggerNodesToDb(nodes, fileName);
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
SwaggerOperations.Add(node);
|
||||
AvailableOperations.Add(node);
|
||||
MenuAvailableOperations.Add(node);
|
||||
}
|
||||
|
||||
MessageBox.Show($"Successfully imported {nodes.Count} API endpoints from Swagger.", "Import Swagger", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Failed to parse Swagger file: {ex.Message}", "Import Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private List<OperationInfoViewModel> ParseSwaggerFile(string jsonFilePath)
|
||||
{
|
||||
var operations = new List<OperationInfoViewModel>();
|
||||
var openApiDocument = OpenApiDocument.FromFileAsync(jsonFilePath).Result;
|
||||
|
||||
foreach (var path in openApiDocument.Paths)
|
||||
{
|
||||
foreach (var method in path.Value)
|
||||
{
|
||||
var ovmodel = new OperationInfoViewModel
|
||||
{
|
||||
Title = path.Key,
|
||||
OPType = method.Key,
|
||||
Type = OperationType.API
|
||||
};
|
||||
|
||||
var addedParams = new HashSet<string>();
|
||||
foreach (var parameter in method.Value.Parameters)
|
||||
{
|
||||
if (addedParams.Add(parameter.Name))
|
||||
{
|
||||
ovmodel.Input.Add(parameter.Name);
|
||||
}
|
||||
}
|
||||
|
||||
ovmodel.Output.Add("");
|
||||
operations.Add(ovmodel);
|
||||
}
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
private void SaveSwaggerNodesToDb(List<OperationInfoViewModel> nodes, string swaggerFileName)
|
||||
{
|
||||
using var db = new LiteDbHelper<SwaggerNodeModel>("SwaggerNodes");
|
||||
db.DeleteMany(n => n.SwaggerFileName == swaggerFileName);
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
db.Insert(new SwaggerNodeModel
|
||||
{
|
||||
Title = node.Title ?? string.Empty,
|
||||
OPType = node.OPType ?? string.Empty,
|
||||
InputNames = new List<string>(node.Input),
|
||||
SwaggerFileName = swaggerFileName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSwaggerNodesFromDb()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var db = new LiteDbHelper<SwaggerNodeModel>("SwaggerNodes");
|
||||
var savedNodes = db.FindAll();
|
||||
|
||||
foreach (var saved in savedNodes)
|
||||
{
|
||||
var ovmodel = new OperationInfoViewModel
|
||||
{
|
||||
Title = saved.Title,
|
||||
OPType = saved.OPType,
|
||||
Type = OperationType.API
|
||||
};
|
||||
|
||||
foreach (var inputName in saved.InputNames)
|
||||
{
|
||||
ovmodel.Input.Add(inputName);
|
||||
}
|
||||
|
||||
ovmodel.Output.Add("");
|
||||
SwaggerOperations.Add(ovmodel);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// DB may not exist yet on first run
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateOperation(OperationInfoViewModel operationInfo)
|
||||
{
|
||||
OperationViewModel op = OperationFactory.GetOperation(operationInfo);
|
||||
op.Location = Location;
|
||||
|
||||
_calculator.Operations.Add(op);
|
||||
|
||||
var pending = _calculator.PendingConnection;
|
||||
if (pending.IsVisible)
|
||||
{
|
||||
var connector = pending.Source.IsInput ? op.Output.FirstOrDefault() : op.Input.FirstOrDefault();
|
||||
if (connector != null && _calculator.CanCreateConnection(pending.Source, connector))
|
||||
{
|
||||
_calculator.CreateConnection(pending.Source, connector);
|
||||
}
|
||||
}
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Examples/Nodify.Calculator/PendingConnectionViewModel.cs
Normal file
36
Examples/Nodify.Calculator/PendingConnectionViewModel.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class PendingConnectionViewModel : ObservableObject
|
||||
{
|
||||
private ConnectorViewModel _source = default!;
|
||||
public ConnectorViewModel Source
|
||||
{
|
||||
get => _source;
|
||||
set => SetProperty(ref _source, value);
|
||||
}
|
||||
|
||||
private ConnectorViewModel? _target;
|
||||
public ConnectorViewModel? Target
|
||||
{
|
||||
get => _target;
|
||||
set => SetProperty(ref _target, value);
|
||||
}
|
||||
|
||||
private bool _isVisible;
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
set => SetProperty(ref _isVisible, value);
|
||||
}
|
||||
|
||||
private Point _targetLocation;
|
||||
|
||||
public Point TargetLocation
|
||||
{
|
||||
get => _targetLocation;
|
||||
set => SetProperty(ref _targetLocation, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Examples/Nodify.Calculator/SystemOperationViewModel.cs
Normal file
33
Examples/Nodify.Calculator/SystemOperationViewModel.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public enum SystemOperations
|
||||
{
|
||||
COPY,
|
||||
IF,
|
||||
TAKE,
|
||||
BEGIN,
|
||||
END,
|
||||
DEBUG_AND_CREATE_MODEL,
|
||||
GET_SET,
|
||||
PARSEJSON,
|
||||
SPLIT,
|
||||
AUTH
|
||||
}
|
||||
|
||||
public class SystemOperationViewModel : OperationViewModel
|
||||
{
|
||||
private SystemOperations _systemOperation;
|
||||
|
||||
public SystemOperations SystemOperationType
|
||||
{
|
||||
get => _systemOperation;
|
||||
set => SetProperty(ref _systemOperation, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
189
Examples/Nodify.Calculator/leet swagger.txt
Normal file
189
Examples/Nodify.Calculator/leet swagger.txt
Normal file
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"openapi": "3.0.1",
|
||||
"info": {
|
||||
"title": "LeetU",
|
||||
"version": "1.0"
|
||||
},
|
||||
"paths": {
|
||||
"/course": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Course"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Course"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Course"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Course"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Course"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/course/{courseId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Course"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "courseId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/student/{studentId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Student"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "studentId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/student": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Student"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/student/{studentId}/course": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Student"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "studentId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/student/{studentId}/course/{courseId}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Student"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "studentId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "courseId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Course": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"startDate": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Examples/Nodify.Playground/App.xaml
Normal file
53
Examples/Nodify.Playground/App.xaml
Normal file
@@ -0,0 +1,53 @@
|
||||
<Application x:Class="Nodify.Playground.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:nodify="https://miroiu.github.io/nodify"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="{x:Static SystemParameters.FocusVisualStyleKey}">
|
||||
<Setter Property="Control.Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate>
|
||||
<Rectangle StrokeThickness="1"
|
||||
StrokeDashArray="2"
|
||||
Margin="-2"
|
||||
RadiusX="3"
|
||||
RadiusY="3"
|
||||
Stroke="DodgerBlue" />
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/Nodify.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/FocusVisual.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Icons.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Nodify.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Nodify.Playground;component/Themes/Nodify.xaml" />
|
||||
<ResourceDictionary>
|
||||
<Color x:Key="NodifyEditor.FocusVisualColor">DodgerBlue</Color>
|
||||
|
||||
<Style TargetType="{x:Type nodify:HotKeyControl}"
|
||||
BasedOn="{StaticResource {x:Type nodify:HotKeyControl}}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type nodify:HotKeyControl}">
|
||||
<Border CornerRadius="3"
|
||||
Background="OrangeRed">
|
||||
<TextBlock Text="{Binding Number, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="White" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
17
Examples/Nodify.Playground/App.xaml.cs
Normal file
17
Examples/Nodify.Playground/App.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
}
|
||||
10
Examples/Nodify.Playground/AssemblyInfo.cs
Normal file
10
Examples/Nodify.Playground/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
41
Examples/Nodify.Playground/BaseSettingViewModel.cs
Normal file
41
Examples/Nodify.Playground/BaseSettingViewModel.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class BaseSettingViewModel<T> : ObservableObject, ISettingViewModel
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Description { get; }
|
||||
|
||||
private object? _value;
|
||||
|
||||
object? ISettingViewModel.Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value);
|
||||
}
|
||||
|
||||
public SettingsType Type { get;}
|
||||
|
||||
public T Value
|
||||
{
|
||||
get => (T)((ISettingViewModel)this).Value!;
|
||||
set => ((ISettingViewModel)this).Value = value;
|
||||
}
|
||||
|
||||
public BaseSettingViewModel(string name, string? description = default)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
Type = typeof(T) switch
|
||||
{
|
||||
{ } t when t == typeof(string) => SettingsType.Text,
|
||||
{ } t when t == typeof(bool) => SettingsType.Boolean,
|
||||
{ } t when t == typeof(uint) || t == typeof(double) => SettingsType.Number,
|
||||
{ } t when t == typeof(PointEditor) => SettingsType.Point,
|
||||
{ IsEnum: true } => SettingsType.Option,
|
||||
_ => throw new InvalidOperationException($"Type {typeof(T).Name} does not have a matching {nameof(SettingsType)}.")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class FlowToConnectorPositionConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is ConnectionViewModel connection)
|
||||
{
|
||||
var connector = parameter is "Input" ? connection.Input : connection.Output;
|
||||
|
||||
if (connector.Node is KnotNodeViewModel)
|
||||
{
|
||||
var otherConnector = connection.Input == connector ? connection.Output : connection.Input;
|
||||
|
||||
if (otherConnector.Node is KnotNodeViewModel)
|
||||
{
|
||||
return ToPosition(connector == connection.Input ? ConnectorFlow.Input : ConnectorFlow.Output, connector.Node.Orientation);
|
||||
}
|
||||
|
||||
return ToPosition(otherConnector.Flow == ConnectorFlow.Output ? ConnectorFlow.Input : ConnectorFlow.Output, connector.Node.Orientation);
|
||||
}
|
||||
|
||||
return ToPosition(connector.Flow, connector.Node.Orientation);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private ConnectorPosition ToPosition(ConnectorFlow flow, Orientation orientation)
|
||||
{
|
||||
if (orientation == Orientation.Horizontal)
|
||||
{
|
||||
return flow == ConnectorFlow.Output
|
||||
? ConnectorPosition.Right
|
||||
: ConnectorPosition.Left;
|
||||
}
|
||||
|
||||
return flow == ConnectorFlow.Output
|
||||
? ConnectorPosition.Bottom
|
||||
: ConnectorPosition.Top;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class FlowToDirectionConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is ConnectorFlow flow)
|
||||
{
|
||||
return flow == ConnectorFlow.Output ? ConnectionDirection.Forward : ConnectionDirection.Backward;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is ConnectionDirection dir)
|
||||
{
|
||||
return dir == ConnectionDirection.Forward ? ConnectorFlow.Output : ConnectorFlow.Input;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Examples/Nodify.Playground/Converters/UIntToRectConverter.cs
Normal file
27
Examples/Nodify.Playground/Converters/UIntToRectConverter.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Markup;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class UIntToRectConverter : MarkupExtension, IValueConverter
|
||||
{
|
||||
public uint Multiplier { get; set; } = 1;
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
uint size = System.Convert.ToUInt32(value) * Multiplier;
|
||||
return new Rect(0d, 0d, size, size);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
=> this;
|
||||
}
|
||||
}
|
||||
21
Examples/Nodify.Playground/Editor/CommentNodeViewModel.cs
Normal file
21
Examples/Nodify.Playground/Editor/CommentNodeViewModel.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class CommentNodeViewModel : NodeViewModel
|
||||
{
|
||||
private string? _title;
|
||||
public string? Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
|
||||
private Size _size;
|
||||
public Size Size
|
||||
{
|
||||
get => _size;
|
||||
set => SetProperty(ref _size, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Examples/Nodify.Playground/Editor/ConnectionViewModel.cs
Normal file
51
Examples/Nodify.Playground/Editor/ConnectionViewModel.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class ConnectionViewModel : ObservableObject
|
||||
{
|
||||
private NodifyEditorViewModel _graph = default!;
|
||||
public NodifyEditorViewModel Graph
|
||||
{
|
||||
get => _graph;
|
||||
internal set => SetProperty(ref _graph, value);
|
||||
}
|
||||
|
||||
private ConnectorViewModel _input = default!;
|
||||
public ConnectorViewModel Input
|
||||
{
|
||||
get => _input;
|
||||
set => SetProperty(ref _input, value);
|
||||
}
|
||||
|
||||
private ConnectorViewModel _output = default!;
|
||||
public ConnectorViewModel Output
|
||||
{
|
||||
get => _output;
|
||||
set => SetProperty(ref _output, value);
|
||||
}
|
||||
|
||||
private bool _isSelected;
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetProperty(ref _isSelected, value);
|
||||
}
|
||||
|
||||
public ICommand SplitCommand { get; }
|
||||
public ICommand DisconnectCommand { get; }
|
||||
|
||||
public ConnectionViewModel()
|
||||
{
|
||||
SplitCommand = new DelegateCommand<Point>(Split);
|
||||
DisconnectCommand = new DelegateCommand(Remove);
|
||||
}
|
||||
|
||||
public void Split(Point point)
|
||||
=> Graph.Schema.SplitConnection(this, point);
|
||||
|
||||
public void Remove()
|
||||
=> Graph.Connections.Remove(this);
|
||||
}
|
||||
}
|
||||
109
Examples/Nodify.Playground/Editor/ConnectorViewModel.cs
Normal file
109
Examples/Nodify.Playground/Editor/ConnectorViewModel.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public enum ConnectorFlow
|
||||
{
|
||||
Input,
|
||||
Output
|
||||
}
|
||||
|
||||
public enum ConnectorShape
|
||||
{
|
||||
Circle,
|
||||
Triangle,
|
||||
Square,
|
||||
}
|
||||
|
||||
public class ConnectorViewModel : ObservableObject
|
||||
{
|
||||
private string? _title;
|
||||
public string? Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
|
||||
private bool _isConnected;
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set => SetProperty(ref _isConnected, value);
|
||||
}
|
||||
|
||||
private Point _anchor;
|
||||
public Point Anchor
|
||||
{
|
||||
get => _anchor;
|
||||
set => SetProperty(ref _anchor, value);
|
||||
}
|
||||
|
||||
private NodeViewModel _node = default!;
|
||||
public NodeViewModel Node
|
||||
{
|
||||
get => _node;
|
||||
internal set
|
||||
{
|
||||
if (SetProperty(ref _node, value))
|
||||
{
|
||||
OnNodeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectorShape _shape;
|
||||
public ConnectorShape Shape
|
||||
{
|
||||
get => _shape;
|
||||
set => SetProperty(ref _shape, value);
|
||||
}
|
||||
|
||||
public ConnectorFlow Flow { get; private set; }
|
||||
|
||||
public int MaxConnections { get; set; } = 2;
|
||||
|
||||
public NodifyObservableCollection<ConnectionViewModel> Connections { get; } = new NodifyObservableCollection<ConnectionViewModel>();
|
||||
|
||||
public ConnectorViewModel()
|
||||
{
|
||||
Connections.WhenAdded(c =>
|
||||
{
|
||||
c.Input.IsConnected = true;
|
||||
c.Output.IsConnected = true;
|
||||
}).WhenRemoved(c =>
|
||||
{
|
||||
if (c.Input.Connections.Count == 0)
|
||||
{
|
||||
c.Input.IsConnected = false;
|
||||
}
|
||||
|
||||
if (c.Output.Connections.Count == 0)
|
||||
{
|
||||
c.Output.IsConnected = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected virtual void OnNodeChanged()
|
||||
{
|
||||
if (Node is FlowNodeViewModel flow)
|
||||
{
|
||||
Flow = flow.Input.Contains(this) ? ConnectorFlow.Input : ConnectorFlow.Output;
|
||||
}
|
||||
else if (Node is KnotNodeViewModel knot)
|
||||
{
|
||||
Flow = knot.Flow;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsConnectedTo(ConnectorViewModel con)
|
||||
=> Connections.Any(c => c.Input == con || c.Output == con);
|
||||
|
||||
public virtual bool AllowsNewConnections()
|
||||
=> Connections.Count < MaxConnections;
|
||||
|
||||
public void Disconnect()
|
||||
=> Node.Graph.Schema.DisconnectConnector(this);
|
||||
}
|
||||
}
|
||||
34
Examples/Nodify.Playground/Editor/FlowNodeViewModel.cs
Normal file
34
Examples/Nodify.Playground/Editor/FlowNodeViewModel.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class FlowNodeViewModel : NodeViewModel
|
||||
{
|
||||
private string? _title;
|
||||
public string? Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
|
||||
public NodifyObservableCollection<ConnectorViewModel> Input { get; } = new NodifyObservableCollection<ConnectorViewModel>();
|
||||
public NodifyObservableCollection<ConnectorViewModel> Output { get; } = new NodifyObservableCollection<ConnectorViewModel>();
|
||||
|
||||
public FlowNodeViewModel()
|
||||
{
|
||||
Orientation = Orientation.Horizontal;
|
||||
|
||||
Input.WhenAdded(c => c.Node = this)
|
||||
.WhenRemoved(c => c.Disconnect());
|
||||
|
||||
Output.WhenAdded(c => c.Node = this)
|
||||
.WhenRemoved(c => c.Disconnect());
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
Input.Clear();
|
||||
Output.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Examples/Nodify.Playground/Editor/GraphSchema.cs
Normal file
133
Examples/Nodify.Playground/Editor/GraphSchema.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class GraphSchema
|
||||
{
|
||||
#region Add Connection
|
||||
|
||||
public bool CanAddConnection(ConnectorViewModel source, object target)
|
||||
{
|
||||
if (target is ConnectorViewModel con)
|
||||
{
|
||||
return source != con
|
||||
&& source.Node != con.Node
|
||||
&& source.Node.Graph == con.Node.Graph
|
||||
&& source.Shape == con.Shape
|
||||
&& source.AllowsNewConnections()
|
||||
&& con.AllowsNewConnections()
|
||||
&& (source.Flow != con.Flow || con.Node is KnotNodeViewModel)
|
||||
&& !source.IsConnectedTo(con);
|
||||
}
|
||||
else if (source.AllowsNewConnections() && target is FlowNodeViewModel node)
|
||||
{
|
||||
var allConnectors = source.Flow == ConnectorFlow.Input ? node.Output : node.Input;
|
||||
return allConnectors.Any(c => c.AllowsNewConnections());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryAddConnection(ConnectorViewModel source, object? target)
|
||||
{
|
||||
if (target != null && CanAddConnection(source, target))
|
||||
{
|
||||
if (target is ConnectorViewModel connector)
|
||||
{
|
||||
AddConnection(source, connector);
|
||||
return true;
|
||||
}
|
||||
else if (target is FlowNodeViewModel node)
|
||||
{
|
||||
AddConnection(source, node);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddConnection(ConnectorViewModel source, ConnectorViewModel target)
|
||||
{
|
||||
var sourceIsInput = source.Flow == ConnectorFlow.Input;
|
||||
|
||||
source.Node.Graph.Connections.Add(new ConnectionViewModel
|
||||
{
|
||||
Input = sourceIsInput ? source : target,
|
||||
Output = sourceIsInput ? target : source
|
||||
});
|
||||
}
|
||||
|
||||
private void AddConnection(ConnectorViewModel source, FlowNodeViewModel target)
|
||||
{
|
||||
var allConnectors = source.Flow == ConnectorFlow.Input ? target.Output : target.Input;
|
||||
var connector = allConnectors.First(c => c.AllowsNewConnections());
|
||||
|
||||
AddConnection(source, connector);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void DisconnectConnector(ConnectorViewModel connector)
|
||||
{
|
||||
var graph = connector.Node.Graph;
|
||||
var connections = connector.Connections.ToList();
|
||||
connections.ForEach(c => graph.Connections.Remove(c));
|
||||
}
|
||||
|
||||
public void SplitConnection(ConnectionViewModel connection, Point location)
|
||||
{
|
||||
var knot = new KnotNodeViewModel(connection.Output.Node.Orientation)
|
||||
{
|
||||
Location = location,
|
||||
Flow = connection.Output.Flow,
|
||||
Connector = new ConnectorViewModel
|
||||
{
|
||||
MaxConnections = connection.Output.MaxConnections + connection.Input.MaxConnections,
|
||||
Shape = connection.Input.Shape
|
||||
}
|
||||
};
|
||||
connection.Graph.Nodes.Add(knot);
|
||||
|
||||
AddConnection(connection.Output, knot.Connector);
|
||||
AddConnection(knot.Connector, connection.Input);
|
||||
|
||||
connection.Remove();
|
||||
}
|
||||
|
||||
public void AddCommentAroundNodes(IList<NodeViewModel> nodes, string? text = default)
|
||||
{
|
||||
var rect = nodes.GetBoundingBox(50);
|
||||
var comment = new CommentNodeViewModel
|
||||
{
|
||||
Location = rect.Location,
|
||||
Size = rect.Size,
|
||||
Title = text ?? "New comment"
|
||||
};
|
||||
|
||||
nodes[0].Graph.Nodes.Add(comment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rewires all connections from the source connector to the target connector if possible.
|
||||
/// </summary>
|
||||
/// <remarks>The source must be an input connector.</remarks>
|
||||
public void Rewire(ConnectorViewModel source, ConnectorViewModel target)
|
||||
{
|
||||
if (source == target || source.Flow != ConnectorFlow.Input)
|
||||
return;
|
||||
|
||||
var connectionsToRewire = source.Connections.ToList();
|
||||
foreach (var connection in connectionsToRewire)
|
||||
{
|
||||
if (CanAddConnection(connection.Output, target))
|
||||
{
|
||||
source.Node.Graph.Connections.Remove(connection);
|
||||
AddConnection(connection.Output, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Examples/Nodify.Playground/Editor/KnotNodeViewModel.cs
Normal file
31
Examples/Nodify.Playground/Editor/KnotNodeViewModel.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class KnotNodeViewModel : NodeViewModel
|
||||
{
|
||||
public KnotNodeViewModel(Orientation orientation)
|
||||
{
|
||||
Orientation = orientation;
|
||||
}
|
||||
|
||||
public KnotNodeViewModel() : this(Orientation.Horizontal)
|
||||
{
|
||||
}
|
||||
|
||||
private ConnectorViewModel _connector = default!;
|
||||
public ConnectorViewModel Connector
|
||||
{
|
||||
get => _connector;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _connector, value))
|
||||
{
|
||||
_connector.Node = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ConnectorFlow Flow { get; set; }
|
||||
}
|
||||
}
|
||||
32
Examples/Nodify.Playground/Editor/NodeViewModel.cs
Normal file
32
Examples/Nodify.Playground/Editor/NodeViewModel.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public abstract class NodeViewModel : ObservableObject
|
||||
{
|
||||
private NodifyEditorViewModel _graph = default!;
|
||||
public NodifyEditorViewModel Graph
|
||||
{
|
||||
get => _graph;
|
||||
internal set => SetProperty(ref _graph, value);
|
||||
}
|
||||
|
||||
private Point _location;
|
||||
public Point Location
|
||||
{
|
||||
get => _location;
|
||||
set => SetProperty(ref _location, value);
|
||||
}
|
||||
|
||||
public Orientation Orientation { get; protected set; }
|
||||
|
||||
public ICommand DeleteCommand { get; }
|
||||
|
||||
public NodeViewModel()
|
||||
{
|
||||
DeleteCommand = new DelegateCommand(() => Graph.Nodes.Remove(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
735
Examples/Nodify.Playground/Editor/NodifyEditorView.xaml
Normal file
735
Examples/Nodify.Playground/Editor/NodifyEditorView.xaml
Normal file
@@ -0,0 +1,735 @@
|
||||
<UserControl x:Class="Nodify.Playground.NodifyEditorView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Nodify.Playground"
|
||||
xmlns:nodify="https://miroiu.github.io/nodify"
|
||||
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
|
||||
mc:Ignorable="d"
|
||||
Background="{DynamicResource NodifyEditor.BackgroundBrush}"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800">
|
||||
|
||||
<UserControl.DataContext>
|
||||
<local:NodifyEditorViewModel />
|
||||
</UserControl.DataContext>
|
||||
|
||||
<UserControl.Resources>
|
||||
<shared:RandomBrushConverter x:Key="RandomBrushConverter" />
|
||||
<local:FlowToDirectionConverter x:Key="FlowToDirectionConverter" />
|
||||
<local:FlowToConnectorPositionConverter x:Key="FlowToConnectorPositionConverter" />
|
||||
|
||||
<GeometryDrawing x:Key="SmallGridGeometry"
|
||||
Geometry="M0,0 L0,1 0.03,1 0.03,0.03 1,0.03 1,0 Z"
|
||||
Brush="{DynamicResource GridLinesBrush}" />
|
||||
|
||||
<GeometryDrawing x:Key="LargeGridGeometry"
|
||||
Geometry="M0,0 L0,1 0.015,1 0.015,0.015 1,0.015 1,0 Z"
|
||||
Brush="{DynamicResource GridLinesBrush}" />
|
||||
|
||||
<DrawingBrush x:Key="SmallGridLinesDrawingBrush"
|
||||
TileMode="Tile"
|
||||
ViewportUnits="Absolute"
|
||||
Viewport="{Binding GridSpacing, Source={x:Static local:EditorSettings.Instance}, Converter={local:UIntToRectConverter}}"
|
||||
Transform="{Binding ViewportTransform, ElementName=Editor}"
|
||||
Drawing="{StaticResource SmallGridGeometry}" />
|
||||
|
||||
<DrawingBrush x:Key="LargeGridLinesDrawingBrush"
|
||||
TileMode="Tile"
|
||||
ViewportUnits="Absolute"
|
||||
Opacity="0.5"
|
||||
Viewport="{Binding GridSpacing, Source={x:Static local:EditorSettings.Instance}, Converter={local:UIntToRectConverter Multiplier=10}}"
|
||||
Transform="{Binding ViewportTransform, ElementName=Editor}"
|
||||
Drawing="{StaticResource LargeGridGeometry}" />
|
||||
|
||||
<SolidColorBrush x:Key="SquareConnectorColor"
|
||||
Color="MediumSlateBlue" />
|
||||
<SolidColorBrush x:Key="TriangleConnectorColor"
|
||||
Color="MediumVioletRed" />
|
||||
<SolidColorBrush x:Key="SquareConnectorOutline"
|
||||
Color="MediumSlateBlue"
|
||||
Opacity="0.15" />
|
||||
<SolidColorBrush x:Key="TriangleConnectorOutline"
|
||||
Color="MediumVioletRed"
|
||||
Opacity="0.15" />
|
||||
|
||||
<UIElement x:Key="ConnectionAnimationPlaceholder"
|
||||
Opacity="1" />
|
||||
|
||||
<Storyboard x:Key="HighlightConnection">
|
||||
<DoubleAnimation Storyboard.Target="{StaticResource ConnectionAnimationPlaceholder}"
|
||||
Storyboard.TargetProperty="(UIElement.Opacity)"
|
||||
Duration="0:0:0.3"
|
||||
From="1"
|
||||
To="0.3" />
|
||||
</Storyboard>
|
||||
|
||||
<Style x:Key="ConnectionStyle"
|
||||
TargetType="{x:Type nodify:BaseConnection}"
|
||||
BasedOn="{StaticResource {x:Type nodify:BaseConnection}}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Input.Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Square}">
|
||||
<Setter Property="Stroke"
|
||||
Value="{StaticResource SquareConnectorColor}" />
|
||||
<Setter Property="Fill"
|
||||
Value="{StaticResource SquareConnectorColor}" />
|
||||
<Setter Property="OutlineBrush"
|
||||
Value="{StaticResource SquareConnectorOutline}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Input.Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Triangle}">
|
||||
<Setter Property="Stroke"
|
||||
Value="{StaticResource TriangleConnectorColor}" />
|
||||
<Setter Property="Fill"
|
||||
Value="{StaticResource TriangleConnectorColor}" />
|
||||
<Setter Property="OutlineBrush"
|
||||
Value="{StaticResource TriangleConnectorOutline}" />
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsMouseDirectlyOver"
|
||||
Value="True">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard Name="HighlightConnection"
|
||||
Storyboard="{StaticResource HighlightConnection}" />
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<RemoveStoryboard BeginStoryboardName="HighlightConnection" />
|
||||
</Trigger.ExitActions>
|
||||
<Setter Property="Opacity"
|
||||
Value="1" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelectable"
|
||||
Value="True">
|
||||
<Setter Property="Cursor"
|
||||
Value="Hand" />
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseDirectlyOver"
|
||||
Value="False" />
|
||||
<Condition Property="IsSelected"
|
||||
Value="False" />
|
||||
</MultiTrigger.Conditions>
|
||||
<MultiTrigger.Setters>
|
||||
<Setter Property="OutlineBrush"
|
||||
Value="Transparent" />
|
||||
</MultiTrigger.Setters>
|
||||
</MultiTrigger>
|
||||
</Style.Triggers>
|
||||
<Setter Property="Opacity"
|
||||
Value="{Binding Source={StaticResource ConnectionAnimationPlaceholder}, Path=Opacity}" />
|
||||
<Setter Property="Stroke"
|
||||
Value="{DynamicResource Connection.StrokeBrush}" />
|
||||
<Setter Property="Fill"
|
||||
Value="{DynamicResource Connection.StrokeBrush}" />
|
||||
<Setter Property="OutlineBrush">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush Color="{DynamicResource Connection.StrokeColor}"
|
||||
Opacity="0.15" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="ToolTip"
|
||||
Value="Double click to split" />
|
||||
<Setter Property="Source"
|
||||
Value="{Binding Output.Anchor}" />
|
||||
<Setter Property="Target"
|
||||
Value="{Binding Input.Anchor}" />
|
||||
<Setter Property="SplitCommand"
|
||||
Value="{Binding SplitCommand}" />
|
||||
<Setter Property="DisconnectCommand"
|
||||
Value="{Binding DisconnectCommand}" />
|
||||
<Setter Property="SourceOffsetMode"
|
||||
Value="{Binding ConnectionSourceOffsetMode, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="TargetOffsetMode"
|
||||
Value="{Binding ConnectionTargetOffsetMode, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="SourceOffset"
|
||||
Value="{Binding ConnectionSourceOffset.Size, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="TargetOffset"
|
||||
Value="{Binding ConnectionTargetOffset.Size, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="ArrowSize"
|
||||
Value="{Binding ConnectionArrowSize.Size, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="ArrowEnds"
|
||||
Value="{Binding ArrowHeadEnds, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="ArrowShape"
|
||||
Value="{Binding ArrowHeadShape, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="Spacing"
|
||||
Value="{Binding ConnectionSpacing, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="Direction"
|
||||
Value="{Binding Output.Flow, Converter={StaticResource FlowToDirectionConverter}}" />
|
||||
<Setter Property="SourceOrientation"
|
||||
Value="{Binding Output.Node.Orientation}" />
|
||||
<Setter Property="TargetOrientation"
|
||||
Value="{Binding Input.Node.Orientation}" />
|
||||
<Setter Property="DirectionalArrowsCount"
|
||||
Value="{Binding DirectionalArrowsCount, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="DirectionalArrowsOffset"
|
||||
Value="{Binding DirectionalArrowsOffset, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="IsAnimatingDirectionalArrows"
|
||||
Value="{Binding IsAnimatingConnections, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="DirectionalArrowsAnimationDuration"
|
||||
Value="{Binding DirectionalArrowsAnimationDuration, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="Text"
|
||||
Value="{Binding ConnectionText, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="IsSelectable"
|
||||
Value="{Binding SelectableConnections, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="IsSelected"
|
||||
Value="{Binding IsSelected}" />
|
||||
<Setter Property="StrokeThickness"
|
||||
Value="{Binding ConnectionStrokeThickness, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="OutlineThickness"
|
||||
Value="{Binding ConnectionOutlineThickness, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="FocusVisualPadding"
|
||||
Value="{Binding ConnectionFocusVisualPadding, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="CircuitConnectionTemplate">
|
||||
<nodify:CircuitConnection Style="{StaticResource ConnectionStyle}"
|
||||
Angle="{Binding CircuitConnectionAngle, Source={x:Static local:EditorSettings.Instance}}"
|
||||
CornerRadius="{Binding ConnectionCornerRadius, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="StepConnectionTemplate">
|
||||
<nodify:StepConnection Style="{StaticResource ConnectionStyle}"
|
||||
CornerRadius="{Binding ConnectionCornerRadius, Source={x:Static local:EditorSettings.Instance}}"
|
||||
SourcePosition="{Binding ., Converter={StaticResource FlowToConnectorPositionConverter}, ConverterParameter=Output}"
|
||||
TargetPosition="{Binding ., Converter={StaticResource FlowToConnectorPositionConverter}, ConverterParameter=Input}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="LineConnectionTemplate">
|
||||
<nodify:LineConnection Style="{StaticResource ConnectionStyle}"
|
||||
CornerRadius="{Binding ConnectionCornerRadius, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ConnectionTemplate">
|
||||
<nodify:Connection Style="{StaticResource ConnectionStyle}" />
|
||||
</DataTemplate>
|
||||
|
||||
<ControlTemplate x:Key="SquareConnector"
|
||||
TargetType="Control">
|
||||
<Rectangle Width="14"
|
||||
Height="14"
|
||||
StrokeDashCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
Stroke="{TemplateBinding BorderBrush}"
|
||||
Fill="{TemplateBinding Background}"
|
||||
StrokeThickness="2" />
|
||||
</ControlTemplate>
|
||||
|
||||
<ControlTemplate x:Key="TriangleConnector"
|
||||
TargetType="Control">
|
||||
<Polygon Width="14"
|
||||
Height="14"
|
||||
Points="1,13 13,13 7,1"
|
||||
StrokeDashCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
Stroke="{TemplateBinding BorderBrush}"
|
||||
Fill="{TemplateBinding Background}"
|
||||
StrokeThickness="2" />
|
||||
</ControlTemplate>
|
||||
|
||||
<Storyboard x:Key="MarchingAnts">
|
||||
<DoubleAnimation RepeatBehavior="Forever"
|
||||
Storyboard.TargetProperty="StrokeDashOffset"
|
||||
BeginTime="00:00:00"
|
||||
Duration="0:3:0"
|
||||
From="1000"
|
||||
To="0" />
|
||||
</Storyboard>
|
||||
|
||||
<Style x:Key="SelectionRectangleStyle"
|
||||
TargetType="Rectangle"
|
||||
BasedOn="{StaticResource NodifyEditor.SelectionRectangleStyle}">
|
||||
<Setter Property="StrokeDashArray"
|
||||
Value="4 4" />
|
||||
<Setter Property="StrokeThickness"
|
||||
Value="2" />
|
||||
<Style.Triggers>
|
||||
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
|
||||
<BeginStoryboard Storyboard="{StaticResource MarchingAnts}" />
|
||||
</EventTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="CuttingLineStyle"
|
||||
TargetType="{x:Type nodify:CuttingLine}"
|
||||
BasedOn="{StaticResource {x:Type nodify:CuttingLine}}">
|
||||
<Setter Property="StrokeDashArray"
|
||||
Value="1 1" />
|
||||
<Setter Property="StrokeThickness"
|
||||
Value="2" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<nodify:NodifyEditor x:Name="Editor"
|
||||
ItemsSource="{Binding Nodes}"
|
||||
SelectedItem="{Binding SelectedNode}"
|
||||
SelectedItems="{Binding SelectedNodes}"
|
||||
CanSelectMultipleItems="{Binding CanSelectMultipleNodes, Source={x:Static local:EditorSettings.Instance}}"
|
||||
Connections="{Binding Connections}"
|
||||
SelectedConnection="{Binding SelectedConnection}"
|
||||
SelectedConnections="{Binding SelectedConnections}"
|
||||
CanSelectMultipleConnections="{Binding CanSelectMultipleConnections, Source={x:Static local:EditorSettings.Instance}}"
|
||||
PendingConnection="{Binding PendingConnection}"
|
||||
DisconnectConnectorCommand="{Binding DisconnectConnectorCommand}"
|
||||
ViewportLocation="{Binding Location.Value, Source={x:Static local:EditorSettings.Instance}}"
|
||||
ViewportSize="{Binding ViewportSize, Mode=OneWayToSource}"
|
||||
ViewportZoom="{Binding Zoom, Source={x:Static local:EditorSettings.Instance}}"
|
||||
MinViewportZoom="{Binding MinZoom, Source={x:Static local:EditorSettings.Instance}}"
|
||||
MaxViewportZoom="{Binding MaxZoom, Source={x:Static local:EditorSettings.Instance}}"
|
||||
AutoPanSpeed="{Binding AutoPanningSpeed, Source={x:Static local:EditorSettings.Instance}}"
|
||||
AutoPanEdgeDistance="{Binding AutoPanningEdgeDistance, Source={x:Static local:EditorSettings.Instance}}"
|
||||
GridCellSize="{Binding GridSpacing, Source={x:Static local:EditorSettings.Instance}}"
|
||||
EnableRealtimeSelection="{Binding EnableRealtimeSelection, Source={x:Static local:EditorSettings.Instance}}"
|
||||
DisableAutoPanning="{Binding DisableAutoPanning, Source={x:Static local:EditorSettings.Instance}}"
|
||||
DisablePanning="{Binding DisablePanning, Source={x:Static local:EditorSettings.Instance}}"
|
||||
DisableZooming="{Binding DisableZooming, Source={x:Static local:EditorSettings.Instance}}"
|
||||
DisplayConnectionsOnTop="{Binding DisplayConnectionsOnTop, Source={x:Static local:EditorSettings.Instance}}"
|
||||
BringIntoViewSpeed="{Binding BringIntoViewSpeed, Source={x:Static local:EditorSettings.Instance}}"
|
||||
BringIntoViewMaxDuration="{Binding BringIntoViewMaxDuration, Source={x:Static local:EditorSettings.Instance}}"
|
||||
SelectionRectangleStyle="{StaticResource SelectionRectangleStyle}"
|
||||
CuttingLineStyle="{StaticResource CuttingLineStyle}">
|
||||
<nodify:NodifyEditor.Style>
|
||||
<Style TargetType="{x:Type nodify:NodifyEditor}"
|
||||
BasedOn="{StaticResource {x:Type nodify:NodifyEditor}}">
|
||||
<Setter Property="ConnectionTemplate"
|
||||
Value="{StaticResource ConnectionTemplate}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowGridLines, Source={x:Static local:PlaygroundSettings.Instance}}"
|
||||
Value="True">
|
||||
<Setter Property="Background"
|
||||
Value="{StaticResource SmallGridLinesDrawingBrush}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding ConnectionStyle, Mode=TwoWay, Source={x:Static local:EditorSettings.Instance}}"
|
||||
Value="Line">
|
||||
<Setter Property="ConnectionTemplate"
|
||||
Value="{StaticResource LineConnectionTemplate}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding ConnectionStyle, Mode=TwoWay, Source={x:Static local:EditorSettings.Instance}}"
|
||||
Value="Circuit">
|
||||
<Setter Property="ConnectionTemplate"
|
||||
Value="{StaticResource CircuitConnectionTemplate}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding ConnectionStyle, Mode=TwoWay, Source={x:Static local:EditorSettings.Instance}}"
|
||||
Value="Step">
|
||||
<Setter Property="ConnectionTemplate"
|
||||
Value="{StaticResource StepConnectionTemplate}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</nodify:NodifyEditor.Style>
|
||||
|
||||
<nodify:NodifyEditor.InputBindings>
|
||||
<KeyBinding Key="Delete"
|
||||
Command="{Binding DeleteSelectionCommand}" />
|
||||
<KeyBinding Key="C"
|
||||
Command="{Binding CommentSelectionCommand}" />
|
||||
</nodify:NodifyEditor.InputBindings>
|
||||
|
||||
<nodify:NodifyEditor.Resources>
|
||||
<Style TargetType="{x:Type nodify:PendingConnection}"
|
||||
BasedOn="{StaticResource {x:Type nodify:PendingConnection}}">
|
||||
<Setter Property="CompletedCommand"
|
||||
Value="{Binding Graph.CreateConnectionCommand}" />
|
||||
<Setter Property="Source"
|
||||
Value="{Binding Source, Mode=OneWayToSource}" />
|
||||
<Setter Property="Target"
|
||||
Value="{Binding PreviewTarget, Mode=OneWayToSource}" />
|
||||
<Setter Property="PreviewTarget"
|
||||
Value="{Binding PreviewTarget, Mode=OneWayToSource}" />
|
||||
<Setter Property="Content"
|
||||
Value="{Binding PreviewText}" />
|
||||
<Setter Property="EnablePreview"
|
||||
Value="{Binding EnablePendingConnectionPreview, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="EnableSnapping"
|
||||
Value="{Binding EnablePendingConnectionSnapping, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="AllowOnlyConnectors"
|
||||
Value="{Binding AllowConnectingToConnectorsOnly, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="Direction"
|
||||
Value="{Binding Source.Flow, Converter={StaticResource FlowToDirectionConverter}}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type nodify:PendingConnection}">
|
||||
<Canvas>
|
||||
<nodify:Connection Source="{TemplateBinding SourceAnchor}"
|
||||
Target="{TemplateBinding TargetAnchor}"
|
||||
Direction="{TemplateBinding Direction}"
|
||||
SourceOrientation="{Binding Source.Node.Orientation}"
|
||||
TargetOrientation="{Binding TargetOrientation}"
|
||||
DirectionalArrowsCount="{Binding DirectionalArrowsCount, Source={x:Static local:EditorSettings.Instance}}"
|
||||
StrokeThickness="{TemplateBinding StrokeThickness}"
|
||||
SourceOffset="{Binding ConnectionSourceOffset.Size, Source={x:Static local:EditorSettings.Instance}}"
|
||||
TargetOffset="{Binding ConnectionTargetOffset.Size, Source={x:Static local:EditorSettings.Instance}}"
|
||||
SourceOffsetMode="{Binding ConnectionSourceOffsetMode, Source={x:Static local:EditorSettings.Instance}}"
|
||||
TargetOffsetMode="None"
|
||||
ArrowSize="{Binding ConnectionArrowSize.Size, Source={x:Static local:EditorSettings.Instance}}"
|
||||
ArrowEnds="{Binding ArrowHeadEnds, Source={x:Static local:EditorSettings.Instance}}"
|
||||
ArrowShape="{Binding ArrowHeadShape, Source={x:Static local:EditorSettings.Instance}}"
|
||||
Spacing="{Binding ConnectionSpacing, Source={x:Static local:EditorSettings.Instance}}">
|
||||
<nodify:Connection.Style>
|
||||
<Style TargetType="nodify:Connection"
|
||||
BasedOn="{StaticResource {x:Type nodify:Connection}}">
|
||||
<Setter Property="Stroke"
|
||||
Value="{DynamicResource Connection.StrokeBrush}" />
|
||||
<Setter Property="Fill"
|
||||
Value="{DynamicResource Connection.StrokeBrush}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Source.Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Square}">
|
||||
<Setter Property="Stroke"
|
||||
Value="{StaticResource SquareConnectorColor}" />
|
||||
<Setter Property="Fill"
|
||||
Value="{StaticResource SquareConnectorColor}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Source.Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Triangle}">
|
||||
<Setter Property="Stroke"
|
||||
Value="{StaticResource TriangleConnectorColor}" />
|
||||
<Setter Property="Fill"
|
||||
Value="{StaticResource TriangleConnectorColor}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</nodify:Connection.Style>
|
||||
</nodify:Connection>
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
Canvas.Left="{Binding TargetAnchor.X, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Canvas.Top="{Binding TargetAnchor.Y, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Visibility="{Binding PreviewText, Converter={shared:StringToVisibilityConverter}}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
CornerRadius="3"
|
||||
Margin="15">
|
||||
<ContentPresenter />
|
||||
</Border>
|
||||
</Canvas>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type nodify:Connector}"
|
||||
BasedOn="{StaticResource {x:Type nodify:Connector}}">
|
||||
<Setter Property="Anchor"
|
||||
Value="{Binding Anchor, Mode=OneWayToSource}" />
|
||||
<Setter Property="IsConnected"
|
||||
Value="{Binding IsConnected}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type nodify:NodeInput}"
|
||||
BasedOn="{StaticResource {x:Type nodify:NodeInput}}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Square}">
|
||||
<Setter Property="ConnectorTemplate"
|
||||
Value="{StaticResource SquareConnector}" />
|
||||
<Setter Property="BorderBrush"
|
||||
Value="{StaticResource SquareConnectorColor}" />
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Title}"
|
||||
Margin="0 0 5 0" />
|
||||
<TextBox Text="{Binding MaxConnections}"
|
||||
MinWidth="30" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Triangle}">
|
||||
<Setter Property="ConnectorTemplate"
|
||||
Value="{StaticResource TriangleConnector}" />
|
||||
<Setter Property="BorderBrush"
|
||||
Value="{StaticResource TriangleConnectorColor}" />
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Title}"
|
||||
Margin="0 0 5 0"
|
||||
VerticalAlignment="Center" />
|
||||
<CheckBox />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
<Setter Property="HeaderTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
|
||||
<TextBlock Text="{Binding Title}" />
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Header"
|
||||
Value="{Binding}" />
|
||||
<Setter Property="Anchor"
|
||||
Value="{Binding Anchor, Mode=OneWayToSource}" />
|
||||
<Setter Property="IsConnected"
|
||||
Value="{Binding IsConnected}" />
|
||||
<Setter Property="Background"
|
||||
Value="Transparent" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type nodify:NodeOutput}"
|
||||
BasedOn="{StaticResource {x:Type nodify:NodeOutput}}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Square}">
|
||||
<Setter Property="ConnectorTemplate"
|
||||
Value="{StaticResource SquareConnector}" />
|
||||
<Setter Property="BorderBrush"
|
||||
Value="{StaticResource SquareConnectorColor}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Shape}"
|
||||
Value="{x:Static local:ConnectorShape.Triangle}">
|
||||
<Setter Property="ConnectorTemplate"
|
||||
Value="{StaticResource TriangleConnector}" />
|
||||
<Setter Property="BorderBrush"
|
||||
Value="{StaticResource TriangleConnectorColor}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
<Setter Property="Header"
|
||||
Value="{Binding Title}" />
|
||||
<Setter Property="Anchor"
|
||||
Value="{Binding Anchor, Mode=OneWayToSource}" />
|
||||
<Setter Property="IsConnected"
|
||||
Value="{Binding IsConnected}" />
|
||||
<Setter Property="Background"
|
||||
Value="Transparent" />
|
||||
</Style>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:KnotNodeViewModel}">
|
||||
<nodify:KnotNode Content="{Binding Connector}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:CommentNodeViewModel}">
|
||||
<nodify:GroupingNode ActualSize="{Binding Size}"
|
||||
Header="{Binding Title}"
|
||||
MovementMode="{Binding GroupingNodeMovement, Mode=TwoWay, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:FlowNodeViewModel}">
|
||||
<nodify:Node Input="{Binding Input}"
|
||||
Output="{Binding Output}"
|
||||
Header="{Binding Title}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:VerticalNodeViewModel}">
|
||||
<nodify:Node Header="{Binding Input}"
|
||||
Footer="{Binding Output}"
|
||||
Content="{Binding Title}">
|
||||
<nodify:Node.ContentTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}"
|
||||
Margin="5" />
|
||||
</DataTemplate>
|
||||
</nodify:Node.ContentTemplate>
|
||||
<nodify:Node.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<ItemsControl ItemsSource="{Binding}"
|
||||
Focusable="False">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
|
||||
<nodify:NodeInput Orientation="Vertical" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</DataTemplate>
|
||||
</nodify:Node.HeaderTemplate>
|
||||
<nodify:Node.FooterTemplate>
|
||||
<DataTemplate>
|
||||
<ItemsControl ItemsSource="{Binding}"
|
||||
Focusable="False">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
|
||||
<nodify:NodeOutput Orientation="Vertical" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</DataTemplate>
|
||||
</nodify:Node.FooterTemplate>
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
</nodify:NodifyEditor.Resources>
|
||||
|
||||
<nodify:NodifyEditor.ItemContainerStyle>
|
||||
<Style TargetType="{x:Type nodify:ItemContainer}"
|
||||
BasedOn="{StaticResource {x:Type nodify:ItemContainer}}">
|
||||
<Setter Property="BorderThickness"
|
||||
Value="2" />
|
||||
<Setter Property="SelectedBorderThickness"
|
||||
Value="4" />
|
||||
<Setter Property="IsSelectable"
|
||||
Value="{Binding SelectableNodes, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="IsDraggable"
|
||||
Value="{Binding DraggableNodes, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
<Setter Property="CacheMode">
|
||||
<Setter.Value>
|
||||
<BitmapCache RenderAtScale="{Binding MaxZoom, Source={x:Static local:EditorSettings.Instance}}"
|
||||
EnableClearType="True" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Location"
|
||||
Value="{Binding Location}" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected"
|
||||
Value="True">
|
||||
<Setter Property="Panel.ZIndex"
|
||||
Value="1" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</nodify:NodifyEditor.ItemContainerStyle>
|
||||
</nodify:NodifyEditor>
|
||||
|
||||
<Grid Background="{StaticResource LargeGridLinesDrawingBrush}"
|
||||
Visibility="{Binding ShowGridLines, Source={x:Static local:PlaygroundSettings.Instance}, Converter={shared:BooleanToVisibilityConverter}}"
|
||||
Panel.ZIndex="-2" />
|
||||
|
||||
<nodify:Minimap ItemsSource="{Binding ItemsSource, ElementName=Editor}"
|
||||
ViewportSize="{Binding ViewportSize, ElementName=Editor}"
|
||||
ViewportLocation="{Binding ViewportLocation, ElementName=Editor}"
|
||||
Visibility="{Binding ShowMinimap, Source={x:Static local:PlaygroundSettings.Instance}, Converter={shared:BooleanToVisibilityConverter}}"
|
||||
IsReadOnly="{Binding DisableMinimapControls, Source={x:Static local:PlaygroundSettings.Instance}}"
|
||||
ResizeToViewport="{Binding ResizeToViewport, Source={x:Static local:PlaygroundSettings.Instance}}"
|
||||
MaxViewportOffset="{Binding MinimapMaxViewportOffset.Size, Source={x:Static local:PlaygroundSettings.Instance}}"
|
||||
Zoom="Minimap_Zoom"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Width="300"
|
||||
Height="200"
|
||||
Margin="5 40">
|
||||
<nodify:Minimap.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:NodeViewModel}">
|
||||
<Grid />
|
||||
</DataTemplate>
|
||||
</nodify:Minimap.ItemTemplate>
|
||||
<nodify:Minimap.ItemContainerStyle>
|
||||
<Style TargetType="{x:Type nodify:MinimapItem}"
|
||||
BasedOn="{StaticResource {x:Type nodify:MinimapItem}}">
|
||||
<Setter Property="Location"
|
||||
Value="{Binding Location}" />
|
||||
<Setter Property="Width"
|
||||
Value="150" />
|
||||
<Setter Property="Height"
|
||||
Value="130" />
|
||||
</Style>
|
||||
</nodify:Minimap.ItemContainerStyle>
|
||||
</nodify:Minimap>
|
||||
|
||||
<StackPanel HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="5 60"
|
||||
Width="250">
|
||||
<Border CornerRadius="3"
|
||||
Visibility="{Binding SelectedConnection, Converter={shared:BooleanToVisibilityConverter}}"
|
||||
Background="{DynamicResource PanelBackgroundBrush}"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
Margin="0 0 0 10">
|
||||
<StackPanel Margin="10">
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="{x:Type TextBlock}"
|
||||
BasedOn="{StaticResource {x:Type TextBlock}}">
|
||||
<Setter Property="Margin"
|
||||
Value="0 0 0 5" />
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
|
||||
<StackPanel Margin="0 0 0 14">
|
||||
<TextBlock Text="Selected connection"
|
||||
Foreground="{DynamicResource Node.ForegroundBrush}"
|
||||
FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Margin="0 0 0 14"
|
||||
Foreground="{DynamicResource Node.ForegroundBrush}">
|
||||
<Run>From</Run>
|
||||
<Run Text="{Binding SelectedConnection.Output.Node.Title}"
|
||||
Foreground="Red" />
|
||||
<Run> - </Run>
|
||||
<Run Text="{Binding SelectedConnection.Output.Title}"
|
||||
Foreground="Red" />
|
||||
<Run>to</Run>
|
||||
<Run Text="{Binding SelectedConnection.Input.Node.Title}"
|
||||
Foreground="Red" />
|
||||
<Run> - </Run>
|
||||
<Run Text="{Binding SelectedConnection.Input.Title}"
|
||||
Foreground="Red" />
|
||||
</TextBlock>
|
||||
|
||||
<Button Command="{Binding SelectedConnection.DisconnectCommand}"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource HollowButton}"
|
||||
Content="Delete" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border CornerRadius="3"
|
||||
Visibility="{Binding SelectedNode, Converter={shared:BooleanToVisibilityConverter}}"
|
||||
Background="{DynamicResource PanelBackgroundBrush}"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderBrush}">
|
||||
<StackPanel Margin="10">
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="{x:Type TextBlock}"
|
||||
BasedOn="{StaticResource {x:Type TextBlock}}">
|
||||
<Setter Property="Margin"
|
||||
Value="0 0 0 5" />
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
|
||||
<StackPanel Margin="0 0 0 14">
|
||||
<TextBlock Text="Selected node"
|
||||
Foreground="{DynamicResource Node.ForegroundBrush}"
|
||||
FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Margin="0 0 0 14"
|
||||
Foreground="{DynamicResource Node.ForegroundBrush}">
|
||||
<Run>Title: </Run>
|
||||
<Run Text="{Binding SelectedNode.Title}"
|
||||
Foreground="Red" />
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Margin="0 0 0 14"
|
||||
Foreground="{DynamicResource Node.ForegroundBrush}">
|
||||
<Run>Location: </Run>
|
||||
<Run Text="{Binding SelectedNode.Location}"
|
||||
Foreground="Red" />
|
||||
</TextBlock>
|
||||
|
||||
<Button Command="{Binding SelectedNode.DeleteCommand}"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource HollowButton}"
|
||||
Content="Delete" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
51
Examples/Nodify.Playground/Editor/NodifyEditorView.xaml.cs
Normal file
51
Examples/Nodify.Playground/Editor/NodifyEditorView.xaml.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Nodify.Events;
|
||||
using Nodify.Interactivity;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public partial class NodifyEditorView : UserControl
|
||||
{
|
||||
public NodifyEditor EditorInstance => Editor;
|
||||
|
||||
public NodifyEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
EditorInstance.ActiveNavigationLayerChanged += DisplayActiveNavigationLayer;
|
||||
}
|
||||
|
||||
static NodifyEditorView()
|
||||
{
|
||||
InputProcessor.Shared<Connector>.ReplaceHandlerFactory<ConnectorState.Connecting>(elem => new CustomConnecting(elem));
|
||||
InputProcessor.Shared<Connector>.RegisterHandlerFactory(elem => new RetargetConnections(elem));
|
||||
}
|
||||
|
||||
private void Minimap_Zoom(object sender, ZoomEventArgs e)
|
||||
{
|
||||
EditorInstance.ZoomAtPosition(e.Zoom, e.Location);
|
||||
}
|
||||
|
||||
private void DisplayActiveNavigationLayer(KeyboardNavigationLayerId layerId)
|
||||
{
|
||||
var editorVm = (NodifyEditorViewModel)EditorInstance.DataContext;
|
||||
|
||||
if (layerId == KeyboardNavigationLayerId.Nodes)
|
||||
{
|
||||
editorVm.KeyboardNavigationLayer = nameof(KeyboardNavigationLayerId.Nodes);
|
||||
}
|
||||
else if (layerId == KeyboardNavigationLayerId.Connections)
|
||||
{
|
||||
editorVm.KeyboardNavigationLayer = nameof(KeyboardNavigationLayerId.Connections);
|
||||
}
|
||||
else if (layerId == KeyboardNavigationLayerId.Decorators)
|
||||
{
|
||||
editorVm.KeyboardNavigationLayer = nameof(KeyboardNavigationLayerId.Decorators);
|
||||
}
|
||||
else
|
||||
{
|
||||
editorVm.KeyboardNavigationLayer = "Custom";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Examples/Nodify.Playground/Editor/NodifyEditorViewModel.cs
Normal file
133
Examples/Nodify.Playground/Editor/NodifyEditorViewModel.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class NodifyEditorViewModel : ObservableObject
|
||||
{
|
||||
public NodifyEditorViewModel()
|
||||
{
|
||||
Schema = new GraphSchema();
|
||||
|
||||
PendingConnection = new PendingConnectionViewModel
|
||||
{
|
||||
Graph = this
|
||||
};
|
||||
|
||||
DeleteSelectionCommand = new DelegateCommand(DeleteSelection, () => SelectedNodes.Count > 0 || SelectedConnections.Count > 0);
|
||||
CommentSelectionCommand = new RequeryCommand(() => Schema.AddCommentAroundNodes(SelectedNodes, "New comment"), () => SelectedNodes.Count > 0);
|
||||
DisconnectConnectorCommand = new DelegateCommand<ConnectorViewModel>(c => c.Disconnect());
|
||||
CreateConnectionCommand = new DelegateCommand<object>(target => Schema.TryAddConnection(PendingConnection.Source!, target),
|
||||
target => PendingConnection.Source != null && target != null && Schema.CanAddConnection(PendingConnection.Source, target));
|
||||
|
||||
Connections.WhenAdded(c =>
|
||||
{
|
||||
c.Graph = this;
|
||||
c.Input.Connections.Add(c);
|
||||
c.Output.Connections.Add(c);
|
||||
})
|
||||
// Called when the collection is cleared
|
||||
.WhenRemoved(c =>
|
||||
{
|
||||
c.Input.Connections.Remove(c);
|
||||
c.Output.Connections.Remove(c);
|
||||
});
|
||||
|
||||
Nodes.WhenAdded(x => x.Graph = this)
|
||||
// Not called when the collection is cleared
|
||||
.WhenRemoved(x =>
|
||||
{
|
||||
if (x is FlowNodeViewModel flow)
|
||||
{
|
||||
flow.Disconnect();
|
||||
}
|
||||
else if (x is KnotNodeViewModel knot)
|
||||
{
|
||||
knot.Connector.Disconnect();
|
||||
}
|
||||
})
|
||||
.WhenCleared(x => Connections.Clear());
|
||||
}
|
||||
|
||||
private NodifyObservableCollection<NodeViewModel> _nodes = new NodifyObservableCollection<NodeViewModel>();
|
||||
public NodifyObservableCollection<NodeViewModel> Nodes
|
||||
{
|
||||
get => _nodes;
|
||||
set => SetProperty(ref _nodes, value);
|
||||
}
|
||||
|
||||
private NodifyObservableCollection<NodeViewModel> _selectedNodes = new NodifyObservableCollection<NodeViewModel>();
|
||||
public NodifyObservableCollection<NodeViewModel> SelectedNodes
|
||||
{
|
||||
get => _selectedNodes;
|
||||
set => SetProperty(ref _selectedNodes, value);
|
||||
}
|
||||
|
||||
private NodifyObservableCollection<ConnectionViewModel> _selectedConnections = new NodifyObservableCollection<ConnectionViewModel>();
|
||||
public NodifyObservableCollection<ConnectionViewModel> SelectedConnections
|
||||
{
|
||||
get => _selectedConnections;
|
||||
set => SetProperty(ref _selectedConnections, value);
|
||||
}
|
||||
|
||||
private NodifyObservableCollection<ConnectionViewModel> _connections = new NodifyObservableCollection<ConnectionViewModel>();
|
||||
public NodifyObservableCollection<ConnectionViewModel> Connections
|
||||
{
|
||||
get => _connections;
|
||||
set => SetProperty(ref _connections, value);
|
||||
}
|
||||
|
||||
private Size _viewportSize;
|
||||
public Size ViewportSize
|
||||
{
|
||||
get => _viewportSize;
|
||||
set => SetProperty(ref _viewportSize, value);
|
||||
}
|
||||
|
||||
public PendingConnectionViewModel PendingConnection { get; }
|
||||
|
||||
private ConnectionViewModel? _selectedConnection;
|
||||
public ConnectionViewModel? SelectedConnection
|
||||
{
|
||||
get => _selectedConnection;
|
||||
set => SetProperty(ref _selectedConnection, value);
|
||||
}
|
||||
|
||||
private NodeViewModel? _selectedNode;
|
||||
public NodeViewModel? SelectedNode
|
||||
{
|
||||
get => _selectedNode;
|
||||
set => SetProperty(ref _selectedNode, value);
|
||||
}
|
||||
|
||||
public GraphSchema Schema { get; }
|
||||
|
||||
private string? _keyboardNavigationLayer;
|
||||
public string? KeyboardNavigationLayer
|
||||
{
|
||||
get => _keyboardNavigationLayer;
|
||||
set => SetProperty(ref _keyboardNavigationLayer, value);
|
||||
}
|
||||
|
||||
public ICommand DeleteSelectionCommand { get; }
|
||||
public ICommand DisconnectConnectorCommand { get; }
|
||||
public ICommand CreateConnectionCommand { get; }
|
||||
public ICommand CommentSelectionCommand { get; }
|
||||
|
||||
private void DeleteSelection()
|
||||
{
|
||||
foreach (var connection in SelectedConnections.ToList())
|
||||
{
|
||||
connection.Remove();
|
||||
}
|
||||
|
||||
var selected = SelectedNodes.ToList();
|
||||
|
||||
for (int i = 0; i < selected.Count; i++)
|
||||
{
|
||||
Nodes.Remove(selected[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class PendingConnectionViewModel : ObservableObject
|
||||
{
|
||||
private NodifyEditorViewModel _graph = default!;
|
||||
public NodifyEditorViewModel Graph
|
||||
{
|
||||
get => _graph;
|
||||
internal set => SetProperty(ref _graph, value);
|
||||
}
|
||||
|
||||
private ConnectorViewModel? _source;
|
||||
public ConnectorViewModel? Source
|
||||
{
|
||||
get => _source;
|
||||
set
|
||||
{
|
||||
if(SetProperty(ref _source, value))
|
||||
{
|
||||
SetTargetOrientation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object? _previewTarget;
|
||||
public object? PreviewTarget
|
||||
{
|
||||
get => _previewTarget;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _previewTarget, value))
|
||||
{
|
||||
OnPreviewTargetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string? _previewText;
|
||||
public string? PreviewText
|
||||
{
|
||||
get => _previewText;
|
||||
set => SetProperty(ref _previewText, value);
|
||||
}
|
||||
|
||||
private Orientation _targetOrientation;
|
||||
public Orientation TargetOrientation
|
||||
{
|
||||
get => _targetOrientation;
|
||||
set => SetProperty(ref _targetOrientation, value);
|
||||
}
|
||||
|
||||
protected virtual void OnPreviewTargetChanged()
|
||||
{
|
||||
bool canConnect = PreviewTarget != null && Graph.Schema.CanAddConnection(Source!, PreviewTarget);
|
||||
PreviewText = PreviewTarget switch
|
||||
{
|
||||
ConnectorViewModel con when con == Source => $"Can't connect to self",
|
||||
ConnectorViewModel con => $"{(canConnect ? "Connect" : "Can't connect")} to {con.Title ?? "pin"}",
|
||||
FlowNodeViewModel flow => $"{(canConnect ? "Connect" : "Can't connect")} to {flow.Title ?? "node"}",
|
||||
_ => null
|
||||
};
|
||||
|
||||
SetTargetOrientation();
|
||||
}
|
||||
|
||||
private void SetTargetOrientation()
|
||||
{
|
||||
TargetOrientation = PreviewTarget switch
|
||||
{
|
||||
ConnectorViewModel con when con.Node is FlowNodeViewModel flow => flow.Orientation,
|
||||
FlowNodeViewModel flow => flow.Orientation,
|
||||
NodifyEditorViewModel editor when Source?.Node is FlowNodeViewModel flow => flow.Orientation,
|
||||
_ => Orientation.Horizontal,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
132
Examples/Nodify.Playground/Editor/RetargetConnections.cs
Normal file
132
Examples/Nodify.Playground/Editor/RetargetConnections.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using Nodify.Interactivity;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
/// <summary>
|
||||
/// Connecting state that prevents connecting when <see cref="RetargetConnections"/> is in progress.
|
||||
/// </summary>
|
||||
public class CustomConnecting : ConnectorState.Connecting
|
||||
{
|
||||
protected override bool CanBegin => !RetargetConnections.InProgress;
|
||||
|
||||
public CustomConnecting(Connector connector) : base(connector)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hold CTRL+LeftClick on a connector to start reconnecting it.
|
||||
/// </summary>
|
||||
public class RetargetConnections : DragState<Connector>
|
||||
{
|
||||
public static InputGestureRef Reconnect { get; } = new Interactivity.MouseGesture(MouseAction.LeftClick, ModifierKeys.Control)
|
||||
{
|
||||
IgnoreModifierKeysOnRelease = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Used to prevent connecting when <see cref="EditorSettings.EnableStickyConnectors"/> is enabled.
|
||||
/// </summary>
|
||||
public static bool InProgress { get; private set; }
|
||||
|
||||
protected override bool CanBegin => ViewModel.IsConnected && ViewModel.Flow == ConnectorFlow.Input;
|
||||
protected override bool IsToggle => EditorSettings.Instance.EnableStickyConnectors;
|
||||
|
||||
private ConnectorViewModel ViewModel => (ConnectorViewModel)Element.DataContext;
|
||||
private Vector _connectorOffset;
|
||||
private Connector? _targetConnector;
|
||||
|
||||
public RetargetConnections(Connector element) : base(element, Reconnect, EditorGestures.Mappings.Connector.CancelAction)
|
||||
{
|
||||
PositionElement = Element.Editor ?? (IInputElement)Element;
|
||||
}
|
||||
|
||||
protected override void OnBegin(InputEventArgs e)
|
||||
{
|
||||
_connectorOffset = ViewModel.Node.Orientation == Orientation.Horizontal
|
||||
? (Vector)EditorSettings.Instance.ConnectionTargetOffset.Value
|
||||
: new Vector(EditorSettings.Instance.ConnectionTargetOffset.Value.Y, EditorSettings.Instance.ConnectionTargetOffset.Value.X);
|
||||
|
||||
InProgress = true;
|
||||
}
|
||||
|
||||
protected override void OnMouseMove(MouseEventArgs e)
|
||||
{
|
||||
var position = Element.Editor!.MouseLocation;
|
||||
|
||||
if (EditorSettings.Instance.EnablePendingConnectionHitTesting)
|
||||
{
|
||||
var connector = Element.FindTargetConnector(position);
|
||||
connector?.UpdateAnchor();
|
||||
|
||||
SetTargetConnector(connector);
|
||||
UpdateConnections(connector != null ? connector.Anchor : position + _connectorOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateConnections(position + _connectorOffset);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateConnections(Point position)
|
||||
{
|
||||
foreach (var connection in ViewModel.Connections)
|
||||
{
|
||||
connection.Input.Anchor = position;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnEnd(InputEventArgs e)
|
||||
{
|
||||
var position = Element.Editor!.MouseLocation;
|
||||
var target = Element.FindTargetConnector(position);
|
||||
target?.UpdateAnchor();
|
||||
|
||||
if (target?.DataContext is ConnectorViewModel targetVM && ViewModel != targetVM)
|
||||
{
|
||||
ViewModel.Node.Graph.Schema.Rewire(ViewModel, targetVM);
|
||||
}
|
||||
|
||||
SetTargetConnector(null);
|
||||
|
||||
// Reset the position of connections that were not rewired
|
||||
Element.UpdateAnchor();
|
||||
InProgress = false;
|
||||
}
|
||||
|
||||
protected override void OnCancel(InputEventArgs e)
|
||||
{
|
||||
SetTargetConnector(null);
|
||||
|
||||
// Reset the position of connections that were not rewired
|
||||
Element.UpdateAnchor();
|
||||
InProgress = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the connection target and updates the visual state of the target element.
|
||||
/// </summary>
|
||||
private void SetTargetConnector(Connector? target)
|
||||
{
|
||||
if (target == _targetConnector)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_targetConnector != null)
|
||||
{
|
||||
PendingConnection.SetIsOverElement(_targetConnector, false);
|
||||
}
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
PendingConnection.SetIsOverElement(target, true);
|
||||
}
|
||||
|
||||
_targetConnector = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Examples/Nodify.Playground/Editor/VerticalNodeViewModel.cs
Normal file
12
Examples/Nodify.Playground/Editor/VerticalNodeViewModel.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public class VerticalNodeViewModel : FlowNodeViewModel
|
||||
{
|
||||
public VerticalNodeViewModel()
|
||||
{
|
||||
Orientation = Orientation.Vertical;
|
||||
}
|
||||
}
|
||||
}
|
||||
82
Examples/Nodify.Playground/EditorInputMode.cs
Normal file
82
Examples/Nodify.Playground/EditorInputMode.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Nodify.Interactivity;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public enum EditorInputMode
|
||||
{
|
||||
Default,
|
||||
PanOnly,
|
||||
SelectOnly,
|
||||
CutOnly
|
||||
}
|
||||
|
||||
public enum EditorGesturesMappings
|
||||
{
|
||||
Default,
|
||||
Custom
|
||||
}
|
||||
|
||||
public static class EditorInputModeExtensions
|
||||
{
|
||||
public static void Apply(this EditorGestures mappings, EditorInputMode inputMode)
|
||||
{
|
||||
mappings.Apply(PlaygroundSettings.Instance.EditorGesturesMappings.ToGesturesMappings());
|
||||
|
||||
switch (inputMode)
|
||||
{
|
||||
case EditorInputMode.PanOnly:
|
||||
mappings.Editor.Selection.Unbind();
|
||||
mappings.Editor.Cutting.Unbind();
|
||||
mappings.ItemContainer.Selection.Unbind();
|
||||
mappings.ItemContainer.Drag.Unbind();
|
||||
mappings.Connector.Connect.Unbind();
|
||||
break;
|
||||
case EditorInputMode.SelectOnly:
|
||||
mappings.Editor.Pan.Unbind();
|
||||
mappings.Editor.Cutting.Unbind();
|
||||
mappings.ItemContainer.Drag.Unbind();
|
||||
mappings.Connector.Connect.Unbind();
|
||||
break;
|
||||
case EditorInputMode.CutOnly:
|
||||
mappings.Editor.Cutting.Value = new Interactivity.MouseGesture(MouseAction.LeftClick);
|
||||
mappings.Editor.Selection.Unbind();
|
||||
mappings.Editor.Pan.Unbind();
|
||||
mappings.ItemContainer.Selection.Unbind();
|
||||
mappings.ItemContainer.Drag.Unbind();
|
||||
mappings.Connector.Connect.Unbind();
|
||||
break;
|
||||
case EditorInputMode.Default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Apply(this EditorGestures value, EditorGesturesMappings mappings)
|
||||
{
|
||||
var newMappings = mappings.ToGesturesMappings();
|
||||
value.Apply(newMappings);
|
||||
}
|
||||
|
||||
public static EditorGestures ToGesturesMappings(this EditorGesturesMappings mappings)
|
||||
{
|
||||
return mappings switch
|
||||
{
|
||||
EditorGesturesMappings.Custom => new CustomGesturesMappings(),
|
||||
_ => new EditorGestures()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomGesturesMappings : EditorGestures
|
||||
{
|
||||
public CustomGesturesMappings()
|
||||
{
|
||||
Editor.Pan.Value = new AnyGesture(new Interactivity.MouseGesture(MouseAction.LeftClick), new Interactivity.MouseGesture(MouseAction.MiddleClick));
|
||||
Editor.ZoomModifierKey = ModifierKeys.Control;
|
||||
Editor.Selection.Apply(new SelectionGestures(MouseAction.RightClick));
|
||||
// comment to drag with right click - we copy the default gestures of the item container which uses left click for selection
|
||||
ItemContainer.Drag.Value = new AnyGesture(ItemContainer.Selection.Replace.Value, ItemContainer.Selection.Remove.Value, ItemContainer.Selection.Append.Value, ItemContainer.Selection.Invert.Value);
|
||||
ItemContainer.Selection.Apply(Editor.Selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
951
Examples/Nodify.Playground/EditorSettings.cs
Normal file
951
Examples/Nodify.Playground/EditorSettings.cs
Normal file
@@ -0,0 +1,951 @@
|
||||
using Nodify.Interactivity;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public enum ConnectionStyle
|
||||
{
|
||||
Default,
|
||||
Line,
|
||||
Circuit,
|
||||
Step
|
||||
}
|
||||
|
||||
public class EditorSettings : ObservableObject
|
||||
{
|
||||
private readonly IReadOnlyCollection<ISettingViewModel> _settings;
|
||||
public IEnumerable<ISettingViewModel> Settings => PlaygroundSettings.Instance.FilterAndSort(_settings);
|
||||
|
||||
private readonly IReadOnlyCollection<ISettingViewModel> _advancedSettings;
|
||||
public IEnumerable<ISettingViewModel> AdvancedSettings => PlaygroundSettings.Instance.FilterAndSort(_advancedSettings);
|
||||
|
||||
private EditorSettings()
|
||||
{
|
||||
PlaygroundSettings.Instance.PropertyChanged += OnSearchTextChanged;
|
||||
|
||||
_settings = new List<ISettingViewModel>()
|
||||
{
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableRealtimeSelection,
|
||||
val => Instance.EnableRealtimeSelection = val,
|
||||
"Realtime selection: ",
|
||||
"Selects items when finished if disabled."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.SelectableNodes,
|
||||
val => Instance.SelectableNodes = val,
|
||||
"Selectable nodes: ",
|
||||
"Whether nodes can be selected."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.DraggableNodes,
|
||||
val => Instance.DraggableNodes= val,
|
||||
"Draggable nodes: ",
|
||||
"Whether nodes can be dragged."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.CanSelectMultipleNodes,
|
||||
val => Instance.CanSelectMultipleNodes = val,
|
||||
"Can select multiple nodes: "),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnablePendingConnectionSnapping,
|
||||
val => Instance.EnablePendingConnectionSnapping = val,
|
||||
"Pending connection snapping: ",
|
||||
"Whether to snap the pending connection to connectors"),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnablePendingConnectionPreview,
|
||||
val => Instance.EnablePendingConnectionPreview = val,
|
||||
"Pending connection preview: ",
|
||||
"Show information about the pending connection."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowConnectingToConnectorsOnly,
|
||||
val => Instance.AllowConnectingToConnectorsOnly = val,
|
||||
"Disable drop connection on node: ",
|
||||
"Can connect directly to nodes if enabled"),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.DisableAutoPanning,
|
||||
val => Instance.DisableAutoPanning = val,
|
||||
"Disable auto panning: "),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.DisablePanning,
|
||||
val => Instance.DisablePanning = val,
|
||||
"Disable panning: "),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.DisableZooming,
|
||||
val => Instance.DisableZooming = val,
|
||||
"Disable zooming: "),
|
||||
new ProxySettingViewModel<uint>(
|
||||
() => Instance.GridSpacing,
|
||||
val => Instance.GridSpacing = val,
|
||||
"Grid spacing: ",
|
||||
"Snapping value in pixels"),
|
||||
new ProxySettingViewModel<PointEditor>(
|
||||
() => Instance.Location,
|
||||
val => Instance.Location = val,
|
||||
"Location: ",
|
||||
"The viewport's location."),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.Zoom,
|
||||
val => Instance.Zoom = val,
|
||||
"Zoom: ",
|
||||
"The viewport's zoom. Not accurate when trying to zoom outside the MinViewportZoom and MaxViewportZoom because of dependency property coercion not updating the binding with the final result."),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.MinZoom,
|
||||
val => Instance.MinZoom = val,
|
||||
"Min zoom: "),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.MaxZoom,
|
||||
val => Instance.MaxZoom = val,
|
||||
"Max zoom: "),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.AutoPanningSpeed,
|
||||
val => Instance.AutoPanningSpeed = val,
|
||||
"Auto panning speed: ",
|
||||
"Speed value in pixels per tick"),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.AutoPanningEdgeDistance,
|
||||
val => Instance.AutoPanningEdgeDistance = val,
|
||||
"Auto panning edge distance: ",
|
||||
"Distance from edge to trigger auto panning"),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableStickyConnectors,
|
||||
val => Instance.EnableStickyConnectors = val,
|
||||
"Enable sticky connectors: ",
|
||||
"The connection can be completed in two steps (e.g. click to create pending connection, click to connect)"),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.SelectableConnections,
|
||||
val => Instance.SelectableConnections = val,
|
||||
"Selectable connections: ",
|
||||
"Whether connections can be selected."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.CanSelectMultipleConnections,
|
||||
val => Instance.CanSelectMultipleConnections = val,
|
||||
"Can select multiple connections: "),
|
||||
new ProxySettingViewModel<ConnectionStyle>(
|
||||
() => Instance.ConnectionStyle,
|
||||
val => Instance.ConnectionStyle = val,
|
||||
"Connection style: "),
|
||||
new ProxySettingViewModel<string?>(
|
||||
() => Instance.ConnectionText,
|
||||
val => Instance.ConnectionText = val,
|
||||
"Connection text: "),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.CircuitConnectionAngle,
|
||||
val => Instance.CircuitConnectionAngle = val,
|
||||
"Connection angle: ",
|
||||
"Applies to circuit connection style"),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.ConnectionCornerRadius,
|
||||
val => Instance.ConnectionCornerRadius = val,
|
||||
"Connection corner radius: ",
|
||||
"The corner radius between the line segments."),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.ConnectionSpacing,
|
||||
val => Instance.ConnectionSpacing = val,
|
||||
"Connection spacing: ",
|
||||
"The distance between the start point and the where the angle breaks"),
|
||||
new ProxySettingViewModel<PointEditor>(
|
||||
() => Instance.ConnectionArrowSize,
|
||||
val => Instance.ConnectionArrowSize = val,
|
||||
"Connection arrowhead size: ",
|
||||
"The size of the arrowhead."),
|
||||
new ProxySettingViewModel<uint>(
|
||||
() => Instance.DirectionalArrowsCount,
|
||||
val => Instance.DirectionalArrowsCount = val,
|
||||
"Directional arrows count: ",
|
||||
"The number of arrowheads to draw on the line flowing in the direction of the connection."),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.DirectionalArrowsOffset,
|
||||
val => Instance.DirectionalArrowsOffset = val,
|
||||
"Directional arrows offset: ",
|
||||
"Used to animate the directional arrowheads flowing in the direction of the connection (value is between 0 and 1)."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.IsAnimatingConnections,
|
||||
val => Instance.IsAnimatingConnections = val,
|
||||
"Animate directional arrows: ",
|
||||
"Used to animate the directional arrowheads by animating the DirectionalArrowsOffset value"),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.DirectionalArrowsAnimationDuration,
|
||||
val => Instance.DirectionalArrowsAnimationDuration = val,
|
||||
"Arrows animation duration: ",
|
||||
"The duration in seconds of a directional arrowhead flowing from start to end."),
|
||||
new ProxySettingViewModel<ArrowHeadEnds>(
|
||||
() => Instance.ArrowHeadEnds,
|
||||
val => Instance.ArrowHeadEnds = val,
|
||||
"Connection arrowhead end: ",
|
||||
"The location of the arrowhead."),
|
||||
new ProxySettingViewModel<ArrowHeadShape>(
|
||||
() => Instance.ArrowHeadShape,
|
||||
val => Instance.ArrowHeadShape = val,
|
||||
"Connection arrowhead shape: ",
|
||||
"The shape of the arrow head."),
|
||||
new ProxySettingViewModel<ConnectionOffsetMode>(
|
||||
() => Instance.ConnectionSourceOffsetMode,
|
||||
val => Instance.ConnectionSourceOffsetMode = val,
|
||||
"Connection source offset mode: "),
|
||||
new ProxySettingViewModel<ConnectionOffsetMode>(
|
||||
() => Instance.ConnectionTargetOffsetMode,
|
||||
val => Instance.ConnectionTargetOffsetMode = val,
|
||||
"Connection target offset mode: "),
|
||||
new ProxySettingViewModel<PointEditor>(
|
||||
() => Instance.ConnectionSourceOffset,
|
||||
val => Instance.ConnectionSourceOffset = val,
|
||||
"Connection source offset: "),
|
||||
new ProxySettingViewModel<PointEditor>(
|
||||
() => Instance.ConnectionTargetOffset,
|
||||
val => Instance.ConnectionTargetOffset = val,
|
||||
"Connection target offset: "),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.ConnectionStrokeThickness,
|
||||
val => Instance.ConnectionStrokeThickness = val,
|
||||
"Connection stroke thickness: "),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.ConnectionOutlineThickness,
|
||||
val => Instance.ConnectionOutlineThickness = val,
|
||||
"Connection outline thickness: "),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.ConnectionFocusVisualPadding,
|
||||
val => Instance.ConnectionFocusVisualPadding = val,
|
||||
"Connection focus visual padding: "),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.DisplayConnectionsOnTop,
|
||||
val => Instance.DisplayConnectionsOnTop = val,
|
||||
"Display connections on top: "),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.BringIntoViewSpeed,
|
||||
val => Instance.BringIntoViewSpeed = val,
|
||||
"Bring into view speed: ",
|
||||
"Bring location into view animation speed in pixels per second"),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.BringIntoViewMaxDuration,
|
||||
val => Instance.BringIntoViewMaxDuration = val,
|
||||
"Bring into view max duration: ",
|
||||
"Bring location into view max animation duration"),
|
||||
new ProxySettingViewModel<GroupingMovementMode>(
|
||||
() => Instance.GroupingNodeMovement,
|
||||
val => Instance.GroupingNodeMovement = val,
|
||||
"Grouping node movement: ",
|
||||
"Whether the grouping node is sticky or not"),
|
||||
};
|
||||
|
||||
_advancedSettings = new List<ISettingViewModel>()
|
||||
{
|
||||
new ProxySettingViewModel<uint>(
|
||||
() => Instance.MaxHotKeys,
|
||||
val => Instance.MaxHotKeys = val,
|
||||
"Max hot keys: ",
|
||||
"The maximum number of generated hot keys"),
|
||||
new ProxySettingViewModel<HotKeysDisplayMode>(
|
||||
() => Instance.HotKeysDisplayMode,
|
||||
val => Instance.HotKeysDisplayMode = val,
|
||||
"Hot keys display mode: ",
|
||||
"Specifies how hotkeys are displayed for a pending connection."),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.MouseActionSuppressionThreshold,
|
||||
val => Instance.MouseActionSuppressionThreshold = val,
|
||||
"Context menu suppression threshold: ",
|
||||
"Disable context menu after mouse moved this far."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.PreserveSelectionOnRightClick,
|
||||
val => Instance.PreserveSelectionOnRightClick = val,
|
||||
"Preserve selection on right click: ",
|
||||
"Whether right-click on the container should preserve the current selection."),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.AutoPanningTickRate,
|
||||
val => Instance.AutoPanningTickRate = val,
|
||||
"Auto panning tick rate: ",
|
||||
"How often is the new position calculated in milliseconds. Disable and enable auto panning for this to have effect."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableSnappingCorrection,
|
||||
val => Instance.EnableSnappingCorrection = val,
|
||||
"Enable snapping correction: ",
|
||||
"Correct the final position when moving a selection."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableCuttingLinePreview,
|
||||
val => Instance.EnableCuttingLinePreview = val,
|
||||
"Enable cutting line preview: ",
|
||||
"Applies custom connection style on intersection (hurts performance due to hit testing)."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnablePendingConnectionHitTesting,
|
||||
val => Instance.EnablePendingConnectionHitTesting = val,
|
||||
"Enable pending connection hit testing: ",
|
||||
"Disable to improve performance."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableConnectorOptimizations,
|
||||
val => Instance.EnableConnectorOptimizations = val,
|
||||
"Enable connector optimizations: ",
|
||||
"Enables optimizations for connectors based on viewport distance and minimum selected nodes."),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.OptimizeSafeZone,
|
||||
val => Instance.OptimizeSafeZone = val,
|
||||
"Optimize connectors safe zone: ",
|
||||
"The minimum distance from the viewport where connectors will start optimizing"),
|
||||
new ProxySettingViewModel<uint>(
|
||||
() => Instance.OptimizeMinimumSelectedItems,
|
||||
val => Instance.OptimizeMinimumSelectedItems = val,
|
||||
"Optimize connectors minimum selection: ",
|
||||
"The minimum selected items needed to start optimizing when outside the safe zone."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableRenderingOptimizations,
|
||||
val => Instance.EnableRenderingOptimizations = val,
|
||||
"Enable nodes rendering optimization: ",
|
||||
"Enables rendering optimizations for nodes based on zoom out percent and nodes count. (zoom in/out to apply optimization)"),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.OptimizeRenderingZoomOutPercent,
|
||||
val => Instance.OptimizeRenderingZoomOutPercent = val,
|
||||
"Rendering optimization zoom out percent: ",
|
||||
"The zoom out percent that triggers the optimization. (percent of x = 1 - MinViewportZoom)"),
|
||||
new ProxySettingViewModel<uint>(
|
||||
() => Instance.OptimizeRenderingMinimumNodes,
|
||||
val => Instance.OptimizeRenderingMinimumNodes = val,
|
||||
"Rendering optimization minimum nodes: ",
|
||||
"The minimum nodes needed to start optimizing when zoom out percent is met."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableDraggingOptimizations,
|
||||
val => Instance.EnableDraggingOptimizations = val,
|
||||
"Enable nodes dragging optimizations: ",
|
||||
"Simulates dragging visually but only commits the changes at the end."),
|
||||
new ProxySettingViewModel<double>(
|
||||
() => Instance.FitToScreenExtentMargin,
|
||||
val => Instance.FitToScreenExtentMargin = val,
|
||||
"Fit to screen extent margin: ",
|
||||
"Adds some margin to the nodes extent when fit to screen"),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowMinimapPanningCancellation,
|
||||
val => Instance.AllowMinimapPanningCancellation = val,
|
||||
"Allow minimap panning cancellation: ",
|
||||
"Right click or escape to cancel panning."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowCuttingCancellation,
|
||||
val => Instance.AllowCuttingCancellation = val,
|
||||
"Allow cutting cancellation: ",
|
||||
"Right click to cancel cutting."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowPushItemsCancellation,
|
||||
val => Instance.AllowPushItemsCancellation = val,
|
||||
"Allow push nodes cancellation: ",
|
||||
"Right click to cancel pushing nodes."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowPanningCancellation,
|
||||
val => Instance.AllowPanningCancellation= val,
|
||||
"Allow panning cancellation: ",
|
||||
"Press Escape to cancel panning."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowSelectionCancellation,
|
||||
val => Instance.AllowSelectionCancellation = val,
|
||||
"Allow selection cancellation: ",
|
||||
"Press Escape to cancel selecting."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowDraggingCancellation,
|
||||
val => Instance.AllowDraggingCancellation = val,
|
||||
"Allow dragging cancellation: ",
|
||||
"Right click to cancel dragging."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowPendingConnectionCancellation,
|
||||
val => Instance.AllowPendingConnectionCancellation = val,
|
||||
"Allow pending connection cancellation: ",
|
||||
"Right click to cancel pending connection."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableToggledCutting,
|
||||
val => Instance.EnableToggledCutting = val,
|
||||
"Enable toggled cutting mode: ",
|
||||
"The interaction will be completed in two steps using the same gesture to start and end."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableToggledPushingItems,
|
||||
val => Instance.EnableToggledPushingItems = val,
|
||||
"Enable toggled pushing items mode: ",
|
||||
"The interaction will be completed in two steps using the same gesture to start and end."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableToggledPanning,
|
||||
val => Instance.EnableToggledPanning = val,
|
||||
"Enable toggled panning mode: ",
|
||||
"The interaction will be completed in two steps using the same gesture to start and end."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableToggledSelecting,
|
||||
val => Instance.EnableToggledSelecting = val,
|
||||
"Enable toggled selecting mode: ",
|
||||
"The interaction will be completed in two steps using the same gesture to start and end."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableToggledDragging,
|
||||
val => Instance.EnableToggledDragging = val,
|
||||
"Enable toggled dragging mode: ",
|
||||
"The interaction will be completed in two steps using the same gesture to start and end."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.EnableMinimapToggledPanning,
|
||||
val => Instance.EnableMinimapToggledPanning = val,
|
||||
"Enable minimap toggled panning mode: ",
|
||||
"The interaction will be completed in two steps using the same gesture to start and end."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowPanningWhileSelecting,
|
||||
val => Instance.AllowPanningWhileSelecting = val,
|
||||
"Allow panning while selecting: ",
|
||||
"Whether panning is allowed while selecting items in the editor."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowPanningWhileCutting,
|
||||
val => Instance.AllowPanningWhileCutting = val,
|
||||
"Allow panning while cutting: ",
|
||||
"Whether panning is allowed while cutting connections in the editor."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowPanningWhilePushingItems,
|
||||
val => Instance.AllowPanningWhilePushingItems = val,
|
||||
"Allow panning while pushing items: ",
|
||||
"Whether panning is allowed while pushing items items in the editor."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowZoomingWhileSelecting,
|
||||
val => Instance.AllowZoomingWhileSelecting = val,
|
||||
"Allow zooming while selecting: ",
|
||||
"Whether zooming is allowed while selecting items in the editor."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowZoomingWhileCutting,
|
||||
val => Instance.AllowZoomingWhileCutting = val,
|
||||
"Allow zooming while cutting: ",
|
||||
"Whether zooming is allowed while cutting connections in the editor."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowZoomingWhilePushingItems,
|
||||
val => Instance.AllowZoomingWhilePushingItems = val,
|
||||
"Allow zooming while pushing items: ",
|
||||
"Whether zooming is allowed while pushing items connections in the editor."),
|
||||
new ProxySettingViewModel<bool>(
|
||||
() => Instance.AllowZoomingWhilePanning,
|
||||
val => Instance.AllowZoomingWhilePanning = val,
|
||||
"Allow zooming while panning: ",
|
||||
"Whether zooming is allowed while panning connections in the editor."),
|
||||
};
|
||||
|
||||
EnableCuttingLinePreview = true;
|
||||
}
|
||||
|
||||
private void OnSearchTextChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(PlaygroundSettings.SearchText))
|
||||
{
|
||||
OnPropertyChanged(nameof(Settings));
|
||||
OnPropertyChanged(nameof(AdvancedSettings));
|
||||
}
|
||||
}
|
||||
|
||||
public static EditorSettings Instance { get; } = new EditorSettings();
|
||||
|
||||
#region Default settings
|
||||
|
||||
private bool _enablePendingConnectionSnapping = true;
|
||||
public bool EnablePendingConnectionSnapping
|
||||
{
|
||||
get => _enablePendingConnectionSnapping;
|
||||
set => SetProperty(ref _enablePendingConnectionSnapping, value);
|
||||
}
|
||||
|
||||
private bool _enablePendingConnectionPreview = true;
|
||||
public bool EnablePendingConnectionPreview
|
||||
{
|
||||
get => _enablePendingConnectionPreview;
|
||||
set => SetProperty(ref _enablePendingConnectionPreview, value);
|
||||
}
|
||||
|
||||
private bool _allowConnectingToConnectorsOnly;
|
||||
public bool AllowConnectingToConnectorsOnly
|
||||
{
|
||||
get => _allowConnectingToConnectorsOnly;
|
||||
set => SetProperty(ref _allowConnectingToConnectorsOnly, value);
|
||||
}
|
||||
|
||||
private bool _realtimeSelection = true;
|
||||
public bool EnableRealtimeSelection
|
||||
{
|
||||
get => _realtimeSelection;
|
||||
set => SetProperty(ref _realtimeSelection, value);
|
||||
}
|
||||
|
||||
private bool _disableAutoPanning = false;
|
||||
public bool DisableAutoPanning
|
||||
{
|
||||
get => _disableAutoPanning;
|
||||
set => SetProperty(ref _disableAutoPanning, value);
|
||||
}
|
||||
|
||||
private double _autoPanningSpeed = 15d;
|
||||
public double AutoPanningSpeed
|
||||
{
|
||||
get => _autoPanningSpeed;
|
||||
set => SetProperty(ref _autoPanningSpeed, value);
|
||||
}
|
||||
|
||||
private double _autoPanningEdgeDistance = 15d;
|
||||
public double AutoPanningEdgeDistance
|
||||
{
|
||||
get => _autoPanningEdgeDistance;
|
||||
set => SetProperty(ref _autoPanningEdgeDistance, value);
|
||||
}
|
||||
|
||||
private bool _disablePanning = false;
|
||||
public bool DisablePanning
|
||||
{
|
||||
get => _disablePanning;
|
||||
set => SetProperty(ref _disablePanning, value);
|
||||
}
|
||||
|
||||
private bool _disableZooming = false;
|
||||
public bool DisableZooming
|
||||
{
|
||||
get => _disableZooming;
|
||||
set => SetProperty(ref _disableZooming, value);
|
||||
}
|
||||
|
||||
private uint _gridSpacing = 15u;
|
||||
public uint GridSpacing
|
||||
{
|
||||
get => _gridSpacing;
|
||||
set => SetProperty(ref _gridSpacing, value);
|
||||
}
|
||||
|
||||
private double _minZoom = 0.1;
|
||||
public double MinZoom
|
||||
{
|
||||
get => _minZoom;
|
||||
set => SetProperty(ref _minZoom, value);
|
||||
}
|
||||
|
||||
private double _maxZoom = 2;
|
||||
public double MaxZoom
|
||||
{
|
||||
get => _maxZoom;
|
||||
set => SetProperty(ref _maxZoom, value);
|
||||
}
|
||||
|
||||
private double _zoom = 1;
|
||||
public double Zoom
|
||||
{
|
||||
get => _zoom;
|
||||
set => SetProperty(ref _zoom, value);
|
||||
}
|
||||
|
||||
private PointEditor _location = new PointEditor();
|
||||
public PointEditor Location
|
||||
{
|
||||
get => _location;
|
||||
set => SetProperty(ref _location, value);
|
||||
}
|
||||
|
||||
private bool _selectableConnections = true;
|
||||
public bool SelectableConnections
|
||||
{
|
||||
get => _selectableConnections;
|
||||
set => SetProperty(ref _selectableConnections, value);
|
||||
}
|
||||
|
||||
private bool _canSelectMultipleConnections = true;
|
||||
public bool CanSelectMultipleConnections
|
||||
{
|
||||
get => _canSelectMultipleConnections;
|
||||
set => SetProperty(ref _canSelectMultipleConnections, value);
|
||||
}
|
||||
|
||||
private bool _draggableNodes = true;
|
||||
public bool DraggableNodes
|
||||
{
|
||||
get => _draggableNodes;
|
||||
set => SetProperty(ref _draggableNodes, value);
|
||||
}
|
||||
|
||||
private bool _selectableNodes = true;
|
||||
public bool SelectableNodes
|
||||
{
|
||||
get => _selectableNodes;
|
||||
set => SetProperty(ref _selectableNodes, value);
|
||||
}
|
||||
|
||||
private bool _canSelectMultipleNodes = true;
|
||||
public bool CanSelectMultipleNodes
|
||||
{
|
||||
get => _canSelectMultipleNodes;
|
||||
set => SetProperty(ref _canSelectMultipleNodes, value);
|
||||
}
|
||||
|
||||
private ConnectionStyle _connectionStyle;
|
||||
public ConnectionStyle ConnectionStyle
|
||||
{
|
||||
get => _connectionStyle;
|
||||
set => SetProperty(ref _connectionStyle, value);
|
||||
}
|
||||
|
||||
private string? _connectionText;
|
||||
public string? ConnectionText
|
||||
{
|
||||
get => _connectionText;
|
||||
set => SetProperty(ref _connectionText, value);
|
||||
}
|
||||
|
||||
private double _circuitConnectionAngle = 45;
|
||||
public double CircuitConnectionAngle
|
||||
{
|
||||
get => _circuitConnectionAngle;
|
||||
set => SetProperty(ref _circuitConnectionAngle, value);
|
||||
}
|
||||
|
||||
private double _connectionCornerRadius = 10;
|
||||
public double ConnectionCornerRadius
|
||||
{
|
||||
get => _connectionCornerRadius;
|
||||
set => SetProperty(ref _connectionCornerRadius, value);
|
||||
}
|
||||
|
||||
private double _connectionSpacing = 20;
|
||||
public double ConnectionSpacing
|
||||
{
|
||||
get => _connectionSpacing;
|
||||
set => SetProperty(ref _connectionSpacing, value);
|
||||
}
|
||||
|
||||
private ConnectionOffsetMode _srcConnectionOffsetMode = ConnectionOffsetMode.Static;
|
||||
public ConnectionOffsetMode ConnectionSourceOffsetMode
|
||||
{
|
||||
get => _srcConnectionOffsetMode;
|
||||
set => SetProperty(ref _srcConnectionOffsetMode, value);
|
||||
}
|
||||
|
||||
private ConnectionOffsetMode _targetConnectionOffsetMode = ConnectionOffsetMode.Static;
|
||||
public ConnectionOffsetMode ConnectionTargetOffsetMode
|
||||
{
|
||||
get => _targetConnectionOffsetMode;
|
||||
set => SetProperty(ref _targetConnectionOffsetMode, value);
|
||||
}
|
||||
|
||||
private ArrowHeadEnds _arrowHeadEnds = ArrowHeadEnds.End;
|
||||
public ArrowHeadEnds ArrowHeadEnds
|
||||
{
|
||||
get => _arrowHeadEnds;
|
||||
set => SetProperty(ref _arrowHeadEnds, value);
|
||||
}
|
||||
|
||||
private ArrowHeadShape _arrowHeadShape = ArrowHeadShape.Arrowhead;
|
||||
public ArrowHeadShape ArrowHeadShape
|
||||
{
|
||||
get => _arrowHeadShape;
|
||||
set => SetProperty(ref _arrowHeadShape, value);
|
||||
}
|
||||
|
||||
private PointEditor _connectionSourceOffset = new Size(14, 0);
|
||||
public PointEditor ConnectionSourceOffset
|
||||
{
|
||||
get => _connectionSourceOffset;
|
||||
set => SetProperty(ref _connectionSourceOffset, value);
|
||||
}
|
||||
|
||||
private PointEditor _connectionTargetOffset = new Size(14, 0);
|
||||
public PointEditor ConnectionTargetOffset
|
||||
{
|
||||
get => _connectionTargetOffset;
|
||||
set => SetProperty(ref _connectionTargetOffset, value);
|
||||
}
|
||||
|
||||
private double _connectionStrokeThickness = 3;
|
||||
public double ConnectionStrokeThickness
|
||||
{
|
||||
get => _connectionStrokeThickness;
|
||||
set => SetProperty(ref _connectionStrokeThickness, value);
|
||||
}
|
||||
|
||||
private double _connectionOutlineThickness = 5;
|
||||
public double ConnectionOutlineThickness
|
||||
{
|
||||
get => _connectionOutlineThickness;
|
||||
set => SetProperty(ref _connectionOutlineThickness, value);
|
||||
}
|
||||
|
||||
private double _connectionFocusVisualPadding = 1;
|
||||
public double ConnectionFocusVisualPadding
|
||||
{
|
||||
get => _connectionFocusVisualPadding;
|
||||
set => SetProperty(ref _connectionFocusVisualPadding, value);
|
||||
}
|
||||
|
||||
private uint _directionalArrowsCount = 3;
|
||||
public uint DirectionalArrowsCount
|
||||
{
|
||||
get => _directionalArrowsCount;
|
||||
set => SetProperty(ref _directionalArrowsCount, value);
|
||||
}
|
||||
|
||||
private double _directionalArrowsOffset;
|
||||
public double DirectionalArrowsOffset
|
||||
{
|
||||
get => _directionalArrowsOffset;
|
||||
set => SetProperty(ref _directionalArrowsOffset, value);
|
||||
}
|
||||
|
||||
private bool _isAnimatingConnections;
|
||||
public bool IsAnimatingConnections
|
||||
{
|
||||
get => _isAnimatingConnections;
|
||||
set => SetProperty(ref _isAnimatingConnections, value);
|
||||
}
|
||||
|
||||
private double _directionalArrowsAnimationDuration = 2.0;
|
||||
public double DirectionalArrowsAnimationDuration
|
||||
{
|
||||
get => _directionalArrowsAnimationDuration;
|
||||
set => SetProperty(ref _directionalArrowsAnimationDuration, value);
|
||||
}
|
||||
|
||||
private PointEditor _connectionArrowSize = new Size(8, 8);
|
||||
public PointEditor ConnectionArrowSize
|
||||
{
|
||||
get => _connectionArrowSize;
|
||||
set => SetProperty(ref _connectionArrowSize, value);
|
||||
}
|
||||
|
||||
private bool _displayConnectionsOnTop;
|
||||
public bool DisplayConnectionsOnTop
|
||||
{
|
||||
get => _displayConnectionsOnTop;
|
||||
set => SetProperty(ref _displayConnectionsOnTop, value);
|
||||
}
|
||||
|
||||
private double _bringIntoViewSpeed = 1000;
|
||||
public double BringIntoViewSpeed
|
||||
{
|
||||
get => _bringIntoViewSpeed;
|
||||
set => SetProperty(ref _bringIntoViewSpeed, value);
|
||||
}
|
||||
|
||||
private double _bringIntoViewMaxDuration = 1;
|
||||
public double BringIntoViewMaxDuration
|
||||
{
|
||||
get => _bringIntoViewMaxDuration;
|
||||
set => SetProperty(ref _bringIntoViewMaxDuration, value);
|
||||
}
|
||||
|
||||
private GroupingMovementMode _groupingNodeMovement;
|
||||
public GroupingMovementMode GroupingNodeMovement
|
||||
{
|
||||
get => _groupingNodeMovement;
|
||||
set => SetProperty(ref _groupingNodeMovement, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Advanced settings
|
||||
|
||||
public uint MaxHotKeys
|
||||
{
|
||||
get => PendingConnection.MaxHotKeys;
|
||||
set => PendingConnection.MaxHotKeys = value;
|
||||
}
|
||||
|
||||
public HotKeysDisplayMode HotKeysDisplayMode
|
||||
{
|
||||
get => PendingConnection.HotKeysDisplayMode;
|
||||
set => PendingConnection.HotKeysDisplayMode = value;
|
||||
}
|
||||
|
||||
public bool PreserveSelectionOnRightClick
|
||||
{
|
||||
get => ItemContainer.PreserveSelectionOnRightClick;
|
||||
set => ItemContainer.PreserveSelectionOnRightClick = value;
|
||||
}
|
||||
|
||||
public double MouseActionSuppressionThreshold
|
||||
{
|
||||
get => NodifyEditor.MouseActionSuppressionThreshold;
|
||||
set => NodifyEditor.MouseActionSuppressionThreshold = value;
|
||||
}
|
||||
|
||||
public double AutoPanningTickRate
|
||||
{
|
||||
get => NodifyEditor.AutoPanningTickRate;
|
||||
set => NodifyEditor.AutoPanningTickRate = value;
|
||||
}
|
||||
|
||||
public bool AllowMinimapPanningCancellation
|
||||
{
|
||||
get => Minimap.AllowPanningCancellation;
|
||||
set => Minimap.AllowPanningCancellation = value;
|
||||
}
|
||||
|
||||
public bool AllowCuttingCancellation
|
||||
{
|
||||
get => NodifyEditor.AllowCuttingCancellation;
|
||||
set => NodifyEditor.AllowCuttingCancellation = value;
|
||||
}
|
||||
|
||||
public bool AllowPushItemsCancellation
|
||||
{
|
||||
get => NodifyEditor.AllowPushItemsCancellation;
|
||||
set => NodifyEditor.AllowPushItemsCancellation = value;
|
||||
}
|
||||
|
||||
public bool AllowPanningCancellation
|
||||
{
|
||||
get => NodifyEditor.AllowPanningCancellation;
|
||||
set => NodifyEditor.AllowPanningCancellation = value;
|
||||
}
|
||||
|
||||
public bool AllowSelectionCancellation
|
||||
{
|
||||
get => NodifyEditor.AllowSelectionCancellation;
|
||||
set => NodifyEditor.AllowSelectionCancellation = value;
|
||||
}
|
||||
|
||||
public bool AllowDraggingCancellation
|
||||
{
|
||||
get => NodifyEditor.AllowDraggingCancellation;
|
||||
set => NodifyEditor.AllowDraggingCancellation = value;
|
||||
}
|
||||
|
||||
public bool AllowPendingConnectionCancellation
|
||||
{
|
||||
get => Connector.AllowPendingConnectionCancellation;
|
||||
set => Connector.AllowPendingConnectionCancellation = value;
|
||||
}
|
||||
|
||||
public bool EnableSnappingCorrection
|
||||
{
|
||||
get => NodifyEditor.EnableSnappingCorrection;
|
||||
set => NodifyEditor.EnableSnappingCorrection = value;
|
||||
}
|
||||
|
||||
public bool EnableCuttingLinePreview
|
||||
{
|
||||
get => NodifyEditor.EnableCuttingLinePreview;
|
||||
set => NodifyEditor.EnableCuttingLinePreview = value;
|
||||
}
|
||||
|
||||
public bool EnablePendingConnectionHitTesting
|
||||
{
|
||||
get => PendingConnection.EnableHitTesting;
|
||||
set => PendingConnection.EnableHitTesting = value;
|
||||
}
|
||||
|
||||
public bool EnableConnectorOptimizations
|
||||
{
|
||||
get => Connector.EnableOptimizations;
|
||||
set => Connector.EnableOptimizations = value;
|
||||
}
|
||||
|
||||
public double OptimizeSafeZone
|
||||
{
|
||||
get => Connector.OptimizeSafeZone;
|
||||
set => Connector.OptimizeSafeZone = value;
|
||||
}
|
||||
|
||||
public uint OptimizeMinimumSelectedItems
|
||||
{
|
||||
get => Connector.OptimizeMinimumSelectedItems;
|
||||
set => Connector.OptimizeMinimumSelectedItems = value;
|
||||
}
|
||||
|
||||
public bool EnableRenderingOptimizations
|
||||
{
|
||||
get => NodifyEditor.EnableRenderingContainersOptimizations;
|
||||
set => NodifyEditor.EnableRenderingContainersOptimizations = value;
|
||||
}
|
||||
|
||||
public uint OptimizeRenderingMinimumNodes
|
||||
{
|
||||
get => NodifyEditor.OptimizeRenderingMinimumContainers;
|
||||
set => NodifyEditor.OptimizeRenderingMinimumContainers = value;
|
||||
}
|
||||
|
||||
public double OptimizeRenderingZoomOutPercent
|
||||
{
|
||||
get => NodifyEditor.OptimizeRenderingZoomOutPercent;
|
||||
set => NodifyEditor.OptimizeRenderingZoomOutPercent = value;
|
||||
}
|
||||
|
||||
public double FitToScreenExtentMargin
|
||||
{
|
||||
get => NodifyEditor.FitToScreenExtentMargin;
|
||||
set => NodifyEditor.FitToScreenExtentMargin = value;
|
||||
}
|
||||
|
||||
public bool EnableDraggingOptimizations
|
||||
{
|
||||
get => NodifyEditor.EnableDraggingContainersOptimizations;
|
||||
set => NodifyEditor.EnableDraggingContainersOptimizations = value;
|
||||
}
|
||||
|
||||
public bool EnableStickyConnectors
|
||||
{
|
||||
get => ConnectorState.EnableToggledConnectingMode;
|
||||
set => ConnectorState.EnableToggledConnectingMode = value;
|
||||
}
|
||||
|
||||
public bool EnableToggledPanning
|
||||
{
|
||||
get => EditorState.EnableToggledPanningMode;
|
||||
set => EditorState.EnableToggledPanningMode = value;
|
||||
}
|
||||
|
||||
public bool EnableToggledCutting
|
||||
{
|
||||
get => EditorState.EnableToggledCuttingMode;
|
||||
set => EditorState.EnableToggledCuttingMode = value;
|
||||
}
|
||||
|
||||
public bool EnableToggledPushingItems
|
||||
{
|
||||
get => EditorState.EnableToggledPushingItemsMode;
|
||||
set => EditorState.EnableToggledPushingItemsMode = value;
|
||||
}
|
||||
|
||||
public bool EnableToggledSelecting
|
||||
{
|
||||
get => EditorState.EnableToggledSelectingMode;
|
||||
set => EditorState.EnableToggledSelectingMode = value;
|
||||
}
|
||||
|
||||
public bool EnableToggledDragging
|
||||
{
|
||||
get => ContainerState.EnableToggledDraggingMode;
|
||||
set => ContainerState.EnableToggledDraggingMode = value;
|
||||
}
|
||||
|
||||
public bool EnableMinimapToggledPanning
|
||||
{
|
||||
get => MinimapState.EnableToggledPanningMode;
|
||||
set => MinimapState.EnableToggledPanningMode = value;
|
||||
}
|
||||
|
||||
public bool AllowPanningWhileSelecting
|
||||
{
|
||||
get => EditorState.AllowPanningWhileSelecting;
|
||||
set => EditorState.AllowPanningWhileSelecting = value;
|
||||
}
|
||||
|
||||
public bool AllowPanningWhileCutting
|
||||
{
|
||||
get => EditorState.AllowPanningWhileCutting;
|
||||
set => EditorState.AllowPanningWhileCutting = value;
|
||||
}
|
||||
|
||||
public bool AllowPanningWhilePushingItems
|
||||
{
|
||||
get => EditorState.AllowPanningWhilePushingItems;
|
||||
set => EditorState.AllowPanningWhilePushingItems = value;
|
||||
}
|
||||
|
||||
public bool AllowZoomingWhileSelecting
|
||||
{
|
||||
get => EditorState.AllowZoomingWhileSelecting;
|
||||
set => EditorState.AllowZoomingWhileSelecting = value;
|
||||
}
|
||||
|
||||
public bool AllowZoomingWhileCutting
|
||||
{
|
||||
get => EditorState.AllowZoomingWhileCutting;
|
||||
set => EditorState.AllowZoomingWhileCutting = value;
|
||||
}
|
||||
|
||||
public bool AllowZoomingWhilePushingItems
|
||||
{
|
||||
get => EditorState.AllowZoomingWhilePushingItems;
|
||||
set => EditorState.AllowZoomingWhilePushingItems = value;
|
||||
}
|
||||
|
||||
public bool AllowZoomingWhilePanning
|
||||
{
|
||||
get => EditorState.AllowZoomingWhilePanning;
|
||||
set => EditorState.AllowZoomingWhilePanning = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
47
Examples/Nodify.Playground/EditorSettingsView.xaml
Normal file
47
Examples/Nodify.Playground/EditorSettingsView.xaml
Normal file
@@ -0,0 +1,47 @@
|
||||
<UserControl x:Class="Nodify.Playground.EditorSettingsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Nodify.Playground"
|
||||
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
|
||||
d:Foreground="{DynamicResource ForegroundBrush}"
|
||||
d:Background="{DynamicResource PanelBackgroundBrush}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel>
|
||||
<Border BorderThickness="1"
|
||||
Padding="10"
|
||||
HorizontalAlignment="Stretch">
|
||||
<local:SettingsView Items="{Binding Source={x:Static local:EditorSettings.Instance}, Path=Settings}"/>
|
||||
</Border>
|
||||
<Expander
|
||||
Header="Advanced"
|
||||
Padding="0 5 0 0"
|
||||
BorderThickness="0 0 0 1"
|
||||
IsExpanded="True"
|
||||
BorderBrush="{DynamicResource BackgroundBrush}">
|
||||
<Expander.Style>
|
||||
<Style TargetType="{x:Type Expander}"
|
||||
BasedOn="{StaticResource {x:Type Expander}}">
|
||||
<Setter Property="Tag"
|
||||
Value="{StaticResource ExpandRightIcon}" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsExpanded"
|
||||
Value="True">
|
||||
<Setter Property="Tag"
|
||||
Value="{StaticResource ExpandDownIcon}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Expander.Style>
|
||||
|
||||
<Border BorderThickness="1"
|
||||
Padding="10"
|
||||
HorizontalAlignment="Stretch">
|
||||
<local:SettingsView Items="{Binding Source={x:Static local:EditorSettings.Instance}, Path=AdvancedSettings}"/>
|
||||
</Border>
|
||||
</Expander>
|
||||
</StackPanel>
|
||||
|
||||
</UserControl>
|
||||
26
Examples/Nodify.Playground/EditorSettingsView.xaml.cs
Normal file
26
Examples/Nodify.Playground/EditorSettingsView.xaml.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for EditorSettingsView.xaml
|
||||
/// </summary>
|
||||
public partial class EditorSettingsView : UserControl
|
||||
{
|
||||
public EditorSettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public static class NodeViewModelExtensions
|
||||
{
|
||||
public static Rect GetBoundingBox(this IList<NodeViewModel> nodes, double padding = 0, int gridCellSize = 15)
|
||||
{
|
||||
double minX = double.MaxValue;
|
||||
double minY = double.MaxValue;
|
||||
|
||||
double maxX = double.MinValue;
|
||||
double maxY = double.MinValue;
|
||||
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
var node = nodes[i];
|
||||
var width = 200; //node.Width
|
||||
var height = 200; //node.Height
|
||||
|
||||
if (node.Location.X < minX)
|
||||
{
|
||||
minX = node.Location.X;
|
||||
}
|
||||
|
||||
if (node.Location.Y < minY)
|
||||
{
|
||||
minY = node.Location.Y;
|
||||
}
|
||||
|
||||
var sizeX = node.Location.X + width;
|
||||
if (sizeX > maxX)
|
||||
{
|
||||
maxX = sizeX;
|
||||
}
|
||||
|
||||
var sizeY = node.Location.Y + height;
|
||||
if (sizeY > maxY)
|
||||
{
|
||||
maxY = sizeY;
|
||||
}
|
||||
}
|
||||
|
||||
var result = new Rect(minX - padding, minY - padding, maxX - minX + padding * 2, maxY - minY + padding * 2);
|
||||
result.X = (int)result.X / gridCellSize * gridCellSize;
|
||||
result.Y = (int)result.Y / gridCellSize * gridCellSize;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void AddRange<T>(this ICollection<T> col, IEnumerable<T> items)
|
||||
{
|
||||
if (items is IList<T> itemsCol)
|
||||
{
|
||||
for (int i = 0; i < itemsCol.Count; i++)
|
||||
{
|
||||
col.Add(itemsCol[i]);
|
||||
}
|
||||
}
|
||||
else if (items is T[] itemsArr)
|
||||
{
|
||||
for (int i = 0; i < itemsArr.Length; i++)
|
||||
{
|
||||
col.Add(itemsArr[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
col.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
186
Examples/Nodify.Playground/Helpers/RandomNodesGenerator.cs
Normal file
186
Examples/Nodify.Playground/Helpers/RandomNodesGenerator.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public struct NodesGeneratorSettings
|
||||
{
|
||||
private static readonly Random _rand = new Random();
|
||||
|
||||
public NodesGeneratorSettings(uint count)
|
||||
{
|
||||
GridSnap = 15;
|
||||
MinNodesCount = MaxNodesCount = count;
|
||||
MinInputCount = MinOutputCount = 0;
|
||||
MaxInputCount = MaxOutputCount = 7;
|
||||
|
||||
ConnectorNameGenerator = (s, i) => $"{new string('C', (int)i % 5)} {i}";
|
||||
NodeNameGenerator = (s, i) => $"Node {i}";
|
||||
NodeLocationGenerator = (s, i) =>
|
||||
{
|
||||
static double EaseOut(double percent, double increment, double start, double end, double total)
|
||||
=> -end * (increment /= total) * (increment - 2) + start;
|
||||
|
||||
var xDistanceBetweenNodes = _rand.Next(150, 350);
|
||||
var yDistanceBetweenNodes = _rand.Next(200, 350);
|
||||
var randSignX = _rand.Next(0, 100) > 50 ? 1 : -1;
|
||||
var randSignY = _rand.Next(0, 100) > 50 ? 1 : -1;
|
||||
var gridOffsetX = i * xDistanceBetweenNodes;
|
||||
var gridOffsetY = i * yDistanceBetweenNodes;
|
||||
|
||||
var x = gridOffsetX * Math.Sin(xDistanceBetweenNodes * randSignX / (i + 1));
|
||||
var y = gridOffsetY * Math.Sin(yDistanceBetweenNodes * randSignY / (i + 1));
|
||||
var easeX = x * EaseOut(i / count, i, 1, 0.01, count);
|
||||
var easeY = y * EaseOut(i / count, i, 1, 0.01, count);
|
||||
|
||||
x = s.Snap((int)easeX);
|
||||
y = s.Snap((int)easeY);
|
||||
|
||||
return new Point(x, y);
|
||||
};
|
||||
}
|
||||
|
||||
public uint GridSnap;
|
||||
public uint MinNodesCount;
|
||||
public uint MaxNodesCount;
|
||||
public uint MinInputCount;
|
||||
public uint MaxInputCount;
|
||||
public uint MinOutputCount;
|
||||
public uint MaxOutputCount;
|
||||
|
||||
public Func<NodesGeneratorSettings, uint, string?> ConnectorNameGenerator;
|
||||
public Func<NodesGeneratorSettings, uint, string?> NodeNameGenerator;
|
||||
public Func<NodesGeneratorSettings, uint, Point> NodeLocationGenerator;
|
||||
|
||||
public int Snap(int x)
|
||||
=> x / (int)GridSnap * (int)GridSnap;
|
||||
}
|
||||
|
||||
public static class RandomNodesGenerator
|
||||
{
|
||||
private static readonly Random _rand = new Random();
|
||||
|
||||
public static List<T> GenerateNodes<T>(NodesGeneratorSettings settings)
|
||||
where T : FlowNodeViewModel, new()
|
||||
{
|
||||
var nodes = new List<T>();
|
||||
var count = _rand.Next((int)settings.MinNodesCount, (int)settings.MaxNodesCount + 1);
|
||||
|
||||
for (uint i = 0; i < count; i++)
|
||||
{
|
||||
var node = new T
|
||||
{
|
||||
Title = settings.NodeNameGenerator(settings, i),
|
||||
Location = settings.NodeLocationGenerator(settings, i)
|
||||
};
|
||||
|
||||
nodes.Add(node);
|
||||
node.Input.AddRange(GenerateConnectors(settings, _rand.Next((int)settings.MinInputCount, (int)settings.MaxInputCount + 1)));
|
||||
node.Output.AddRange(GenerateConnectors(settings, _rand.Next((int)settings.MinOutputCount, (int)settings.MaxOutputCount + 1)));
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public static List<ConnectionViewModel> GenerateConnections(IList<NodeViewModel> nodes)
|
||||
{
|
||||
HashSet<NodeViewModel> visited = new HashSet<NodeViewModel>(nodes.Count);
|
||||
List<ConnectionViewModel> connections = new List<ConnectionViewModel>(nodes.Count);
|
||||
|
||||
for (uint i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
var n1 = nodes[_rand.Next(0, nodes.Count)];
|
||||
var n2 = nodes[_rand.Next(0, nodes.Count)];
|
||||
|
||||
if (n1 == n2 && !(visited.Add(n1) && visited.Add(n2)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<ConnectorViewModel> input = n1 is FlowNodeViewModel flow ? flow.Input.ToList() :
|
||||
n1 is KnotNodeViewModel knot ? new List<ConnectorViewModel> { knot.Connector } : new List<ConnectorViewModel>();
|
||||
|
||||
List<ConnectorViewModel> output = n2 is FlowNodeViewModel flow2 ? flow2.Output.ToList() :
|
||||
n2 is KnotNodeViewModel knot2 ? new List<ConnectorViewModel> { knot2.Connector } : new List<ConnectorViewModel>();
|
||||
|
||||
connections.AddRange(ConnectPins(input, output));
|
||||
}
|
||||
|
||||
return connections;
|
||||
}
|
||||
|
||||
public static List<ConnectionViewModel> ConnectPins(IList<ConnectorViewModel> source, IList<ConnectorViewModel> target)
|
||||
{
|
||||
Dictionary<ConnectorViewModel, List<ConnectorViewModel>> connections = new Dictionary<ConnectorViewModel, List<ConnectorViewModel>>();
|
||||
List<ConnectionViewModel> result = new List<ConnectionViewModel>();
|
||||
|
||||
for (int di = 0; di < target.Count; di++)
|
||||
{
|
||||
if (source.Count > 1 && target.Count > 1 && _rand.Next() % 2 == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var outP = target[di];
|
||||
|
||||
if (!connections.TryGetValue(outP, out var outConns))
|
||||
{
|
||||
var newList = new List<ConnectorViewModel>();
|
||||
connections.Add(outP, newList);
|
||||
outConns = newList;
|
||||
}
|
||||
|
||||
var conNum = _rand.Next(0, source.Count + 1);
|
||||
for (uint ci = 0; ci < conNum; ci++)
|
||||
{
|
||||
var inP = source[_rand.Next(0, conNum)];
|
||||
|
||||
if (!connections.TryGetValue(inP, out var inConns))
|
||||
{
|
||||
var newList = new List<ConnectorViewModel>();
|
||||
connections.Add(inP, newList);
|
||||
inConns = newList;
|
||||
}
|
||||
|
||||
if (!connections[inP].Contains(outP) && !connections[outP].Contains(inP))
|
||||
{
|
||||
var isInput = inP.Flow == ConnectorFlow.Input;
|
||||
|
||||
var connection = new ConnectionViewModel
|
||||
{
|
||||
Input = isInput ? inP : outP,
|
||||
Output = isInput ? outP : inP
|
||||
};
|
||||
result.Add(connection);
|
||||
|
||||
inConns.Add(outP);
|
||||
outConns.Add(inP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<ConnectorViewModel> GenerateConnectors(NodesGeneratorSettings settings, int count)
|
||||
{
|
||||
var list = new List<ConnectorViewModel>(count);
|
||||
|
||||
for (uint i = 0; i < count; i++)
|
||||
{
|
||||
int shapeVal = _rand.Next() % 3;
|
||||
var connector = new ConnectorViewModel
|
||||
{
|
||||
Title = settings.ConnectorNameGenerator(settings, i),
|
||||
Shape = PlaygroundSettings.Instance.UseCustomConnectors ? (ConnectorShape)shapeVal : ConnectorShape.Circle
|
||||
};
|
||||
|
||||
list.Add(connector);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Examples/Nodify.Playground/ISettingViewModel.cs
Normal file
24
Examples/Nodify.Playground/ISettingViewModel.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public enum SettingsType
|
||||
{
|
||||
Boolean,
|
||||
Number,
|
||||
Option,
|
||||
Point,
|
||||
Text
|
||||
}
|
||||
|
||||
public interface ISettingViewModel
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Represents the content within the tooltip.
|
||||
/// </summary>
|
||||
string? Description { get; }
|
||||
object? Value { get; set; }
|
||||
|
||||
SettingsType Type { get;}
|
||||
}
|
||||
}
|
||||
275
Examples/Nodify.Playground/MainWindow.xaml
Normal file
275
Examples/Nodify.Playground/MainWindow.xaml
Normal file
@@ -0,0 +1,275 @@
|
||||
<Window x:Class="Nodify.Playground.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Nodify.Playground"
|
||||
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
|
||||
xmlns:nodify="https://miroiu.github.io/nodify"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow"
|
||||
Height="700"
|
||||
Width="1300">
|
||||
|
||||
<Window.Resources>
|
||||
<shared:DebugConverter x:Key="DebugConverter" />
|
||||
<shared:ToStringConverter x:Key="ToStringConverter" />
|
||||
<shared:StringToVisibilityConverter x:Key="StringToVisibilityConverter" />
|
||||
</Window.Resources>
|
||||
|
||||
<Window.DataContext>
|
||||
<local:PlaygroundViewModel />
|
||||
</Window.DataContext>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<local:NodifyEditorView x:Name="EditorView"
|
||||
DataContext="{Binding GraphViewModel}"
|
||||
Grid.RowSpan="3" />
|
||||
|
||||
<!--ACTIONS-->
|
||||
<Border VerticalAlignment="Top"
|
||||
Background="{DynamicResource PanelBackgroundBrush}"
|
||||
Padding="10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Command="{Binding GenerateRandomNodesCommand}"
|
||||
Content="GENERATE NODES"
|
||||
ToolTip="Generate nodes using the specified settings."
|
||||
Style="{StaticResource HollowButton}" />
|
||||
<Button Command="{Binding ToggleConnectionsCommand}"
|
||||
Content="{Binding ConnectNodesText}"
|
||||
ToolTip="Will add new connections if Connect Nodes is checked, otherwise it will disconnect nodes."
|
||||
Style="{StaticResource HollowButton}" />
|
||||
<Button Command="{Binding PerformanceTestCommand}"
|
||||
Content="PERFORMANCE TEST"
|
||||
ToolTip="You will encounter rendering performance issues. Try disabling the connections to see the difference."
|
||||
Style="{StaticResource HollowButton}" />
|
||||
<Button Command="{Binding ResetCommand}"
|
||||
Content="RESET PLAYGROUND"
|
||||
ToolTip="Reset the Location, Zoom, Nodes and Connections."
|
||||
Style="{StaticResource HollowButton}" />
|
||||
<Button Click="BringIntoView_Click"
|
||||
Content="BRING INTO VIEW"
|
||||
ToolTip="Bring a random node into view."
|
||||
Style="{StaticResource HollowButton}" />
|
||||
<Button Command="{x:Static nodify:EditorCommands.FitToScreen}"
|
||||
Content="FIT TO SCREEN"
|
||||
ToolTip="Scales the viewport to fit all nodes if that's possible."
|
||||
CommandTarget="{Binding EditorInstance, ElementName=EditorView}"
|
||||
Style="{StaticResource HollowButton}" />
|
||||
<Button Command="{Binding GraphViewModel.CommentSelectionCommand}"
|
||||
Content="COMMENT SELECTION"
|
||||
ToolTip="Creates a comment node containing the selected nodes."
|
||||
Style="{StaticResource HollowButton}" />
|
||||
<Button Click="AnimateConnections_Click"
|
||||
Content="TOGGLE CONNECTIONS ANIMATION"
|
||||
ToolTip="Starts or stops animating the directional arrows on all connections (see DirectionalArrowsCount)"
|
||||
Style="{StaticResource HollowButton}" />
|
||||
</StackPanel>
|
||||
|
||||
<Button Style="{StaticResource IconButton}"
|
||||
Content="{StaticResource ThemeIcon}"
|
||||
Command="{Binding Source={x:Static shared:ThemeManager.SetNextThemeCommand}}"
|
||||
ToolTip="Change theme"
|
||||
Grid.Column="1" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!--SETTINGS-->
|
||||
<Expander Grid.Row="1"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalContentAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
Background="{DynamicResource PanelBackgroundBrush}"
|
||||
IsExpanded="True"
|
||||
ExpandDirection="Left"
|
||||
Padding="0 1 4 3">
|
||||
<Expander.Style>
|
||||
<Style TargetType="{x:Type Expander}"
|
||||
BasedOn="{StaticResource {x:Type Expander}}">
|
||||
<Setter Property="Tag"
|
||||
Value="{StaticResource ExpandRightIcon}" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsExpanded"
|
||||
Value="True">
|
||||
<Setter Property="Tag"
|
||||
Value="{StaticResource ExpandLeftIcon}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Expander.Style>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Margin="0 0 5 10">
|
||||
<TextBlock Margin="0 0 0 3">Search:</TextBlock>
|
||||
<TextBox Text="{Binding Source={x:Static local:PlaygroundSettings.Instance}, Path=SearchText, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
|
||||
Padding="4" />
|
||||
</StackPanel>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<Grid IsSharedSizeScope="True"
|
||||
Width="330">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Expander Header="Playground Settings"
|
||||
Padding="0 5 0 0"
|
||||
BorderThickness="0 0 0 1"
|
||||
IsExpanded="True"
|
||||
BorderBrush="{DynamicResource BackgroundBrush}">
|
||||
<Expander.Style>
|
||||
<Style TargetType="{x:Type Expander}"
|
||||
BasedOn="{StaticResource {x:Type Expander}}">
|
||||
<Setter Property="Tag"
|
||||
Value="{StaticResource ExpandRightIcon}" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsExpanded"
|
||||
Value="True">
|
||||
<Setter Property="Tag"
|
||||
Value="{StaticResource ExpandDownIcon}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Expander.Style>
|
||||
<Border BorderThickness="1"
|
||||
Padding="10"
|
||||
HorizontalAlignment="Stretch">
|
||||
<local:SettingsView Items="{Binding Source={x:Static local:PlaygroundSettings.Instance}, Path=Settings}" />
|
||||
</Border>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="Editor Settings"
|
||||
Padding="0 5 0 0"
|
||||
BorderThickness="0 0 0 1"
|
||||
IsExpanded="True"
|
||||
BorderBrush="{DynamicResource BackgroundBrush}"
|
||||
Grid.Row="1">
|
||||
<Expander.Style>
|
||||
<Style TargetType="{x:Type Expander}"
|
||||
BasedOn="{StaticResource {x:Type Expander}}">
|
||||
<Setter Property="Tag"
|
||||
Value="{StaticResource ExpandRightIcon}" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsExpanded"
|
||||
Value="True">
|
||||
<Setter Property="Tag"
|
||||
Value="{StaticResource ExpandDownIcon}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Expander.Style>
|
||||
|
||||
<local:EditorSettingsView />
|
||||
</Expander>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Expander>
|
||||
|
||||
<!--INFORMATION-->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource PanelBackgroundBrush}"
|
||||
VerticalAlignment="Bottom"
|
||||
Padding="10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.Resources>
|
||||
<Style TargetType="{x:Type TextBlock}"
|
||||
BasedOn="{StaticResource {x:Type TextBlock}}">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource ForegroundBrush}" />
|
||||
<Setter Property="Margin"
|
||||
Value="0 0 15 0" />
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock ToolTip="The number of selected items.">
|
||||
<TextBlock.Inlines>
|
||||
<Run Text="Selected nodes: " />
|
||||
<Run Foreground="YellowGreen"
|
||||
Text="{Binding GraphViewModel.SelectedNodes.Count, Mode=OneWay}" />
|
||||
<Run Text="/" />
|
||||
<Run Text="{Binding GraphViewModel.Nodes.Count, Mode=OneWay}" />
|
||||
</TextBlock.Inlines>
|
||||
</TextBlock>
|
||||
<TextBlock ToolTip="The number of selected connections.">
|
||||
<TextBlock.Inlines>
|
||||
<Run Text="Selected connections: " />
|
||||
<Run Foreground="YellowGreen"
|
||||
Text="{Binding GraphViewModel.SelectedConnections.Count, Mode=OneWay}" />
|
||||
<Run Text="/" />
|
||||
<Run Text="{Binding GraphViewModel.Connections.Count, Mode=OneWay}" />
|
||||
</TextBlock.Inlines>
|
||||
</TextBlock>
|
||||
|
||||
<Border Visibility="{Binding GraphViewModel.KeyboardNavigationLayer, Converter={StaticResource StringToVisibilityConverter}}"
|
||||
ToolTip="Press CTRL+[ or CTRL+] in the editor to change the keyboard navigation layer."
|
||||
Padding="14 0 0 0"
|
||||
Height="16"
|
||||
CornerRadius="3"
|
||||
Background="{DynamicResource PanelBackgroundBrush}"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderBrush}">
|
||||
<TextBlock>
|
||||
<Run Text="Navigating: " />
|
||||
<Run Foreground="{DynamicResource ForegroundBrush}"
|
||||
Text="{Binding GraphViewModel.KeyboardNavigationLayer, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Right">
|
||||
<TextBlock ToolTip="The viewport's location.">
|
||||
<TextBlock.Inlines>
|
||||
<Run Text="Location: " />
|
||||
<Run Foreground="Orange"
|
||||
Text="{Binding Location.Value, Mode=OneWay, Converter={StaticResource ToStringConverter}, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
</TextBlock.Inlines>
|
||||
</TextBlock>
|
||||
<TextBlock ToolTip="The viewport's size.">
|
||||
<TextBlock.Inlines>
|
||||
<Run Text="Size: " />
|
||||
<Run Foreground="YellowGreen"
|
||||
Text="{Binding GraphViewModel.ViewportSize, Mode=OneWay, Converter={StaticResource ToStringConverter}}" />
|
||||
</TextBlock.Inlines>
|
||||
</TextBlock>
|
||||
<TextBlock ToolTip="The viewport's zoom. Not accurate when trying to zoom outside the MinViewportZoom and MaxViewportZoom because of dependency property coercion not updating the binding with the final result.">
|
||||
<TextBlock.Inlines>
|
||||
<Run Text="Zoom: " />
|
||||
<Run Foreground="DodgerBlue"
|
||||
Text="{Binding Zoom, Mode=OneWay, Converter={StaticResource ToStringConverter}, Source={x:Static local:EditorSettings.Instance}}" />
|
||||
</TextBlock.Inlines>
|
||||
</TextBlock>
|
||||
<TextBlock ToolTip="The estimated frame rate. (my be buggy)">
|
||||
<TextBlock.Inlines>
|
||||
<Run Text="FPS: " />
|
||||
<Run Foreground="LawnGreen"
|
||||
Name="FPSText" />
|
||||
</TextBlock.Inlines>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
95
Examples/Nodify.Playground/MainWindow.xaml.cs
Normal file
95
Examples/Nodify.Playground/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify.Playground
|
||||
{
|
||||
public static class CompositionTargetEx
|
||||
{
|
||||
private static TimeSpan _last = TimeSpan.Zero;
|
||||
private static event Action<double>? FrameUpdating;
|
||||
|
||||
public static event Action<double> Rendering
|
||||
{
|
||||
add
|
||||
{
|
||||
if (FrameUpdating == null)
|
||||
{
|
||||
CompositionTarget.Rendering += OnRendering;
|
||||
}
|
||||
FrameUpdating += value;
|
||||
}
|
||||
remove
|
||||
{
|
||||
FrameUpdating -= value;
|
||||
if (FrameUpdating == null)
|
||||
{
|
||||
CompositionTarget.Rendering -= OnRendering;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnRendering(object? sender, EventArgs e)
|
||||
{
|
||||
RenderingEventArgs args = (RenderingEventArgs)e;
|
||||
var renderingTime = args.RenderingTime;
|
||||
if (renderingTime == _last)
|
||||
return;
|
||||
|
||||
double fps = 1000 / (renderingTime - _last).TotalMilliseconds;
|
||||
_last = renderingTime;
|
||||
FrameUpdating?.Invoke(fps);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly Random _rand = new Random();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
CompositionTargetEx.Rendering += OnRendering;
|
||||
|
||||
EventManager.RegisterClassHandler(
|
||||
typeof(UIElement),
|
||||
Keyboard.PreviewGotKeyboardFocusEvent,
|
||||
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
|
||||
}
|
||||
|
||||
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||
{
|
||||
Title = e.NewFocus.ToString();
|
||||
}
|
||||
|
||||
private void OnRendering(double fps)
|
||||
{
|
||||
FPSText.Text = fps.ToString("0");
|
||||
}
|
||||
|
||||
private void BringIntoView_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is PlaygroundViewModel model)
|
||||
{
|
||||
NodifyObservableCollection<NodeViewModel> nodes = model.GraphViewModel.Nodes;
|
||||
int index = _rand.Next(nodes.Count);
|
||||
|
||||
if (nodes.Count > index)
|
||||
{
|
||||
NodeViewModel node = nodes[index];
|
||||
EditorCommands.BringIntoView.Execute(node.Location, EditorView.Editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AnimateConnections_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
EditorSettings.Instance.IsAnimatingConnections = !EditorSettings.Instance.IsAnimatingConnections;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Examples/Nodify.Playground/Nodify.Playground.csproj
Normal file
17
Examples/Nodify.Playground/Nodify.Playground.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFrameworks>net9-windows;net8-windows;net6-windows;net5-windows;netcoreapp3.1;net48;net472</TargetFrameworks>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(TargetFramework)'=='net472' OR '$(TargetFramework)'=='net48'">
|
||||
<LangVersion>8.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Nodify\Nodify.csproj" />
|
||||
<ProjectReference Include="..\Nodify.Shared\Nodify.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user