Add project files.

This commit is contained in:
Ankitkumar Satapara
2026-04-17 22:31:58 +05:30
commit 21aaef6776
473 changed files with 50152 additions and 0 deletions

48
.editorconfig Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
# These are supported funding model platforms
github: miroiu
custom: ["https://www.buymeacoffee.com/miroiu", "https://paypal.me/miroiuemanuel"]

View 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.

View 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
View 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
View 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
View 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.

View 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
View 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
View 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}}"

View 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
View 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
View 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
View 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

Binary file not shown.

353
CHANGELOG.md Normal file
View 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
View 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
View 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.

Binary file not shown.

View 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);
}
}
}

View 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>

View 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;
}
}
}

View 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>

View 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
{
}
}

View 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);
}
}
}

View 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)
)]

View 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);
}
}
}

View 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 });
}
}
}

View File

@@ -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; }
}
}

View 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;
}
}
}
}

View 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)
});
}
}
}

View 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;
}
}
}

View 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; }
}
}
}

View 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>();
}
}

View 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();
}
}
}

View 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; }
}
}

View 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>

View 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));
}
}
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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
{
}
}
}
}
}

View 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>

View 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();
});
}
}
}

View 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();
}
}

View 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>

View 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();
}
}
}

View 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; }
}
}

View 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;
}
}

View 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;
}
}

View 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>

View 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);
}
}
}

View 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);
}
}
}

View 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; }
}
}

View 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
// {
// }
//}
}
}
}

View 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]);
}
}

View File

@@ -0,0 +1,7 @@
namespace Nodify.Calculator
{
public interface IOperation
{
double Execute(params double[] operands);
}
}

View 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; }
}

View 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; }
}
}

View 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);
}
}

View 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]);
}
}

View 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();
}
}

View 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;
}
}
}

View 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>

View 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);
}
}
}
}

View 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();
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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
}
}
}
}

View 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>

View 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
{
}
}

View 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)
)]

View 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)}.")
};
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View 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;
}
}

View 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);
}
}
}

View 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);
}
}

View 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);
}
}

View 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();
}
}
}

View 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);
}
}
}
}
}

View 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; }
}
}

View 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));
}
}
}

View 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>

View 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";
}
}
}
}

View 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]);
}
}
}
}

View File

@@ -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,
};
}
}
}

View 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;
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace Nodify.Playground
{
public class VerticalNodeViewModel : FlowNodeViewModel
{
public VerticalNodeViewModel()
{
Orientation = Orientation.Vertical;
}
}
}

View 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);
}
}
}

View 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
}
}

View 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>

View 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();
}
}
}

View File

@@ -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);
}
}
}
}
}

View 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;
}
}
}

View 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;}
}
}

View 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>

View 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;
}
}
}

View 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