文家豪 2 weeks ago
parent 70582d4e12
commit f0b72cfb32

13
src/.gitattributes vendored

@ -0,0 +1,13 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Declare files that will always have CRLF line endings on checkout.
*.cs text eol=crlf
*.xaml text eol=crlf
*.resw text eol=crlf
*.csproj text eol=crlf
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: 0x7c13
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: jackieliu
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: paypal.me/jackil

@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug]"
labels: ''
assignees: ''
---
**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.
**Desktop (please complete the following information):**
- OS: [e.g. Windows 10 1809 17763.593]
- Version [e.g. v0.9.3.0]
**Additional context**
Add any other context about the problem here.

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature request]"
labels: ''
assignees: ''
---
**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.

@ -0,0 +1,25 @@
<!-- Add a brief overview here of the feature/bug & fix. -->
## PR Type
What kind of change does this PR introduce?
<!-- Please uncomment one or more that apply to this PR and apply required prefix to the title. -->
<!-- - Bugfix -->
<!-- Prefix title with "fix: " -->
<!-- - Feature -->
<!-- Prefix title with "feat: " -->
<!-- - Translation -->
<!-- Prefix title with "lang: " -->
<!-- - Documentation content changes -->
<!-- Prefix title with "doc: " -->
<!-- - CI/CD pipeline changes -->
<!-- Prefix title with "ci: " -->
<!-- - Other... Please describe: -->
<!-- Prefix title with "other: " or custom label with conventional commit format: https://www.conventionalcommits.org/en/v1.0.0/ -->
## Other information

@ -0,0 +1,13 @@
{
"conventionalCommitsParserOptions": {
"revertPattern": "/^(?:Revert|revert:)\\s\"?([\\s\\S]+?)\"?\\s*This reverts commit (\\w*)\\./i",
"issuePrefixes": [ "#", "OLDARCH-" ]
},
"handleBarsOptions": {
"setupFile": null,
"template": ".github/RELEASE_TEMPLATE/changelog_template.hbs",
"compileOptions": { "noEscape": true }
},
"breakingChangesPattern": "/^breaking\\s+change$/gim",
"hostname": "https://github.com"
}

@ -0,0 +1,43 @@
{{#with release}}
## [{{name}}]({{href}})
{{/with}}
{{#commit-list commits heading='### 💥 Breaking Changes' breaking=true }}
- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
{{/commit-list}}
{{#commit-list commits heading='### ✨ Features' type='feat' excludeBreaking=true}}
- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
{{/commit-list}}
{{#commit-list commits heading='### 🐛 Fixes' type='fix' excludeBreaking=true }}
- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
{{/commit-list}}
{{#commit-list commits heading='### 🔥 Refactorings' type='refactor' excludeBreaking=true }}
- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
{{/commit-list}}
{{#commit-list commits heading='### 🐎 Performance Improvements' type='perf' excludeBreaking=true }}
- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
{{/commit-list}}
{{#commit-list commits heading='### 🛠 Maintenance' types='chore,ci' excludeBreaking=true }}
- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
{{/commit-list}}
{{#commit-list commits heading='### ✅ Tests' type='test' excludeBreaking=true }}
- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
{{/commit-list}}
{{#commit-list commits heading='### 📚 Documentation' type='doc,docs' excludeBreaking=true }}
- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
{{/commit-list}}
{{#commit-list commits heading='### 💄 Style' type='style' excludeBreaking=true }}
- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
{{/commit-list}}
{{#commit-list commits heading='### 📢 Translations' type='lang,trans' excludeBreaking=true }}
- {{#if scope}} **{{scope}}:** {{/if}}{{subject}} ([`{{shorthash}}`]({{html_url}}))
{{/commit-list}}

@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: "github-actions"
# default location of `.github/workflows`
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: 'action-deps: '
- package-ecosystem: "nuget"
# location of package manifests
directory: "/src/Notepads"
schedule:
interval: "daily"
commit-message:
prefix: 'nuget-deps: '
# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)

@ -0,0 +1,4 @@
label-alias:
bug: 'bug'
feature_request: 'enhancement'
question: 'question'

@ -0,0 +1,128 @@
name: Code scanning alerts bulk dismissal
on:
workflow_run:
workflows: [ "Notepads CI/CD Pipeline" ]
types:
- completed
workflow_dispatch:
inputs:
type:
description: Type of filter to use ("path" for using path and "desc" for using description)
required: true
default: 'path'
reason:
description: Reason for dismissal ("fp" for "false positive", "wf" for "won't fix" and "ut" for "used in tests")
required: true
default: 'wf'
jobs:
setup:
runs-on: windows-latest
outputs:
matrix: ${{ steps.set_filter_matrix.outputs.matrix }}
steps:
- name: Setup filter matrix
id: set_filter_matrix
shell: pwsh
run: |
$FILTER_TYPE = $env:FILTER_TYPE
if ( !( $env:FILTER_TYPE -ieq 'path' ) -And !( $env:FILTER_TYPE -ieq 'desc' ) ) {
$FILTER_TYPE = 'path'
}
switch ( $env:REASON ) {
fp {
$REASON = "false positive"
}
wf {
$REASON = "won't fix"
}
ut {
$REASON = "used in tests"
}
default {
$REASON = "won't fix"
}
}
if ( $FILTER_TYPE -ieq 'path' ) {
$MATRIX = @{
include = @(
@{
filter = "*/obj/*"
}
)
}
} elseif ( $FILTER_TYPE -ieq 'desc' ) {
$MATRIX = @{
include = @(
@{
filter = "Calls to unmanaged code"
},
@{
filter = "Unmanaged code"
}
)
}
} else {
throw "Invalid filter type argument"
}
$MATRIX.include | Foreach-Object {
$_.Add('type',"$FILTER_TYPE")
$_.Add('reason',"$REASON")
}
echo "::set-output name=matrix::$($MATRIX | ConvertTo-Json -depth 32 -Compress)"
env:
FILTER_TYPE: ${{ github.event.inputs.type }}
REASON: ${{ github.event.inputs.reason }}
dismiss-alerts:
name: Dismiss alerts
needs: setup
runs-on: windows-latest
strategy:
matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
env:
# Settings
OWNER: ${{ github.repository_owner }} # verbatim from URL
PROJECT_NAME: ${{ github.event.repository.name }} # verbatim from URL
ACCESS_TOKEN: ${{ secrets.CSA_ACCESS_TOKEN }} # requires security_events read/write permissions
DISMISS_REASON: ${{ matrix.reason }} # "false positive", "won't fix" or "used in tests".
ALERTS_PER_PAGE: 100 # maximum is 100
FILTER: ${{ matrix.filter }}
FILTER_TYPE: ${{ matrix.type }}
steps:
- name: Run automation
id: run_automation
shell: pwsh
run: |
$HEADERS = @{
Authorization = 'Basic {0}' -f [System.Convert]::ToBase64String([char[]]"$($env:OWNER):$($env:ACCESS_TOKEN)")
Accept = 'application/vnd.github.v3+json'
}
$page = 1
$FETCH_URL = "https://api.github.com/repos/$env:OWNER/$env:PROJECT_NAME/code-scanning/alerts?state=open&page={0}&per_page=$env:ALERTS_PER_PAGE"
$LIST_OF_ALERTS = Invoke-RestMethod -Method Get -Headers $HEADERS -Uri $($FETCH_URL -f $page)
while ( $LIST_OF_ALERTS -ne $null ) {
if ( $env:FILTER_TYPE -ieq 'path' ) {
$MATCHES += $($LIST_OF_ALERTS | Where-Object { $_.most_recent_instance.location.path -like "$env:FILTER" })
} else {
$MATCHES += $($LIST_OF_ALERTS | Where-Object { $_.rule.description -like "$env:FILTER" })
}
$page += 1
$LIST_OF_ALERTS = Invoke-RestMethod -Method Get -Headers $HEADERS -Uri $($FETCH_URL -f $page)
}
$ALERT_URL = "https://api.github.com/repos/$env:OWNER/$env:PROJECT_NAME/code-scanning/alerts/{0}"
$BODY = @{
state = 'dismissed'
dismissed_reason = "$env:DISMISS_REASON"
} | ConvertTo-Json
foreach ($index in $MATCHES.number) {
Invoke-RestMethod -Method Patch -Headers $HEADERS -Uri $($ALERT_URL -f $index) -Body $BODY
}
# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)

@ -0,0 +1,402 @@
name: Notepads CI/CD Pipeline
on:
push:
#paths-ignore:
#- '**.md'
#- 'ScreenShots/**'
#- '.whitesource'
#- 'azure-pipelines.yml'
#- '.github/**'
#- '!.github/workflows/main.yml'
branches-ignore:
# PRs made by bots trigger both 'push' and 'pull_request' event, ignore 'push' event in that case
- 'dependabot**'
- 'imgbot**'
tags-ignore:
- '**'
pull_request:
paths-ignore:
- '**.md'
- 'ScreenShots/**'
- '.whitesource'
- 'azure-pipelines.yml'
- '.github/**'
- '!.github/workflows/main.yml'
workflow_dispatch:
inputs:
param:
description: Optional parameter for additional actions
# Type '(major|maj) (realease|rel)' or '(minor|min) (realease|rel)' or release for major,miner,patch release respectively
# Or explicitly provide version number to create release with that version
required: false
schedule:
- cron: '0 8 * * *'
jobs:
setup:
runs-on: windows-latest
outputs:
matrix: ${{ steps.set_matrix.outputs.matrix }}
steps:
- name: Setup strategy matrix
id: set_matrix
shell: pwsh
run: |
$MATRIX = @{
include = @( [ordered]@{
configuration= "Debug"
appxBundlePlatforms = "x86|x64"
oldVersion = ""
newVersion = ""
debug = $true
runCodeqlAnalysis = $false
runSonarCloudScan = $false
}, [ordered]@{
configuration= "Release"
appxBundlePlatforms= "x86|x64|ARM64"
oldVersion = ""
newVersion = ""
debug = $true
runCodeqlAnalysis= $false
runSonarCloudScan= $false
}, [ordered]@{
configuration= "Production"
appxBundlePlatforms= "x86|x64|ARM64"
oldVersion = ""
newVersion = ""
debug = $true
runCodeqlAnalysis= $false
runSonarCloudScan= $false
}
)
}
if ( ( $env:GITHUB_EVENT -eq 'pull_request' ) `
-or ( $env:GITHUB_EVENT -eq 'schedule' ) `
-or ( $env:FORK -eq 'true' ) ) {
$MATRIX.include | Foreach-Object { $_.runSonarCloudScan = $false }
}
if ( ( $env:GITHUB_EVENT -ne 'push' ) `
-and ( $env:GITHUB_EVENT -ne 'pull_request' ) ) {
$MATRIX.include = @($MATRIX.include | Where-Object { $_.configuration -eq "$env:RELEASE_CONFIGURATION" })
if ( ( $env:GITHUB_EVENT -eq 'workflow_dispatch' ) `
-and ( $env:GITHUB_REF -eq 'refs/heads/master' ) ) {
$FETCH_URL = "https://api.github.com/repos/$env:GIT_REPOSITORY/tags?per_page=1"
$OLD_VER = [System.Version]::Parse($(Invoke-RestMethod -Method Get -Uri $FETCH_URL).name -replace 'v')
[System.Int32[]]$VER_INPUT = $($env:PARAM -replace '[a-zA-Z]| ').Split('.')
if ( ( $VER_INPUT.Count -gt 1 ) -or ( $VER_INPUT[0] -gt 0 ) ) {
$NEW_VER = [System.Version]::new($VER_INPUT[0],`
(if ( $VER_INPUT.Count -ge 1 ) { $VER_INPUT[1] } else { 0 }),`
(if ( $VER_INPUT.Count -ge 2 ) { $VER_INPUT[2] } else { 0 }),`
(if ( $VER_INPUT.Count -ge 3 ) { $VER_INPUT[3] } else { 0 }))
} elseif ( $env:PARAM -match 'rel' ) {
if ( $env:PARAM -match 'maj' ) {
$NEW_VER = [System.Version]::new($OLD_VER.Major + 1, 0, 0, 0)
} elseif ( $env:PARAM -match 'min' ) {
$NEW_VER = [System.Version]::new($OLD_VER.Major, $OLD_VER.Minor + 1, 0, 0)
} else {
$NEW_VER = [System.Version]::new($OLD_VER.Major, $OLD_VER.Minor, $OLD_VER.Build + 1, 0)
}
}
if ( ![System.String]::IsNullOrEmpty($OLD_VER) `
-and ![System.String]::IsNullOrEmpty($NEW_VER) `
-and ( $NEW_VER -gt $OLD_VER ) ) {
$MATRIX.include | Foreach-Object { $_.oldVersion = $OLD_VER.ToString() }
$MATRIX.include | Foreach-Object { $_.newVersion = $NEW_VER.ToString() }
}
$MATRIX.include | Foreach-Object { $_.runCodeqlAnalysis = $false }
$MATRIX.include | Foreach-Object {
if ( $_.configuration -eq "$env:RELEASE_CONFIGURATION" ) { $_.release = $true }
}
} else {
$MATRIX.include | Foreach-Object { $_.appxBundlePlatforms = 'x64' }
if ( $env:GITHUB_EVENT -ne 'schedule' ) {
$MATRIX.include | Foreach-Object { $_.runCodeqlAnalysis = $false }
}
}
}
echo "::set-output name=matrix::$($MATRIX | ConvertTo-Json -depth 32 -Compress)"
env:
FORK: ${{ github.event.repository.fork }}
PARAM: ${{ github.event.inputs.param }}
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
RELEASE_CONFIGURATION: Production
ci:
needs: setup
runs-on: windows-latest
strategy:
matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
outputs:
old_version: ${{ matrix.oldVersion }}
new_version: ${{ matrix.newVersion }}
env:
SOLUTION_NAME: src\Notepads.sln
CONFIGURATION: ${{ matrix.configuration }}
DEFAULT_DIR: ${{ github.workspace }}
steps:
- if: matrix.runSonarCloudScan
name: Set up JDK 11
id: Setup_JDK
uses: actions/setup-java@v4
with:
java-version: 1.11
- name: Setup MSBuild
id: setup_msbuild
uses: microsoft/setup-msbuild@v2
- name: Setup NuGet
id: setup-nuget
uses: NuGet/setup-nuget@v2.0.1
- name: Checkout repository
id: checkout_repo
uses: actions/checkout@v4
with:
fetch-depth: 50
token: ${{ secrets.GITHUB_TOKEN }}
# Due to the insufficient memory allocated by default, CodeQL sometimes requires more to be manually allocated
- if: matrix.runCodeqlAnalysis
name: Configure Pagefile
id: config_pagefile
uses: al-cheb/configure-pagefile-action@v1.4
with:
minimum-size: 8GB
maximum-size: 10GB
- if: matrix.newVersion != ''
name: Bump GitHub tag and Update manifest
id: tag_manifest_generator
shell: pwsh
run: |
git config --global user.name $env:GIT_USER_NAME
git config --global user.email $env:GIT_USER_EMAIL
git tag -a -m "$env:NEW_VERSION_TAG" $env:NEW_VERSION_TAG
git push --follow-tags
$xml = [xml](Get-Content $env:APPXMANIFEST_PATH)
$xml.Package.Identity.Version = $env:NEW_VERSION
$xml.save($env:APPXMANIFEST_PATH)
env:
GIT_USER_NAME: ${{ secrets.GIT_USER_NAME }}
GIT_USER_EMAIL: ${{ secrets.GIT_USER_EMAIL }}
APPXMANIFEST_PATH: src\Notepads\Package.appxmanifest
NEW_VERSION: ${{ matrix.newVersion }}
NEW_VERSION_TAG: v${{ matrix.newVersion }}
- if: matrix.runSonarCloudScan
name: Cache SonarCloud packages
id: cache_sonar_packages
uses: actions/cache@v4.2.3
with:
path: ~\sonar\cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- if: matrix.runSonarCloudScan
name: Cache SonarCloud scanner
id: cache_sonar_scanner
uses: actions/cache@v4.2.3
with:
path: .\.sonar\scanner
key: ${{ runner.os }}-sonar-scanner
restore-keys: ${{ runner.os }}-sonar-scanner
- if: matrix.runSonarCloudScan && steps.cache_sonar_scanner.outputs.cache-hit != 'true'
name: Install SonarCloud scanner
id: install_sonar_scanner
shell: pwsh
run: |
New-Item -Path .\.sonar\scanner -ItemType Directory
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
- if: matrix.runSonarCloudScan
name: Initialize SonarCloud scanner
id: init_sonar_scanner
shell: pwsh
run: |
$LOWERCASE_REPOSITORY_NAME = "${{ github.event.repository.name }}".ToLower()
.\.sonar\scanner\dotnet-sonarscanner begin `
/k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" `
/o:"$LOWERCASE_REPOSITORY_NAME" `
/d:sonar.login="$env:SONAR_TOKEN" `
/d:sonar.host.url="https://sonarcloud.io"
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- if: matrix.newVersion != ''
name: Create and validate PFX certificate for AppxBundle
id: create_validate_pfx_cert
shell: pwsh
run: |
$TARGET_FILE = "$env:DEFAULT_DIR\cert.pfx"
$FROM_BASE64_STR = [System.Convert]::FromBase64String($env:BASE64_STR)
[System.IO.File]::WriteAllBytes($TARGET_FILE, $FROM_BASE64_STR)
$FILE_STREAM = [System.IO.File]::OpenRead($TARGET_FILE)
$FILE_STREAM.Position = 0
$SHA256 = [System.Security.Cryptography.SHA256]::Create()
$HASH_BUILDER = [System.Text.StringBuilder]::new()
$SHA256.ComputeHash($FILE_STREAM) | ForEach-Object { $HASH_BUILDER.Append($_.ToString("x2")) }
if ( $HASH_BUILDER.ToString() -cne $env:SHA256_HASH ) {
throw [System.Exception]::new("Created certificate hash $($HASH_BUILDER.ToString()) $(
)doesn't match provided hash $($env:SHA256_HASH)")
}
env:
BASE64_STR: ${{ secrets.PACKAGE_CERTIFICATE_BASE64 }}
SHA256_HASH: ${{ secrets.PACKAGE_CERTIFICATE_SHA256 }}
- name: Restore the application
id: restore_application
shell: pwsh
run: |
msbuild $env:SOLUTION_NAME /t:Restore
nuget restore $env:SOLUTION_NAME
- if: matrix.runCodeqlAnalysis
name: Initialize CodeQL
id: init_codeql
uses: github/codeql-action/init@v3
with:
queries: security-and-quality
languages: csharp
- name: Build and generate bundles
id: build_app
shell: pwsh
run: |
msbuild $env:SOLUTION_NAME `
/p:Platform=$env:PLATFORM `
/p:Configuration=$env:CONFIGURATION `
/p:UapAppxPackageBuildMode=$env:UAP_APPX_PACKAGE_BUILD_MODE `
/p:AppxBundle=$env:APPX_BUNDLE `
/p:AppxPackageSigningEnabled=$env:APPX_PACKAGE_SIGNING_ENABLED `
/p:AppxBundlePlatforms=$env:APPX_BUNDLE_PLATFORMS `
/p:AppxPackageDir=$env:ARTIFACTS_DIR `
/p:PackageCertificateKeyFile=$env:PACKAGE_CERTIFICATE_KEYFILE `
/p:PackageCertificatePassword=$env:PACKAGE_CERTIFICATE_PASSWORD
env:
PLATFORM: x64
UAP_APPX_PACKAGE_BUILD_MODE: StoreUpload
APPX_BUNDLE: Always
APPX_PACKAGE_SIGNING_ENABLED: ${{ matrix.newVersion != '' }}
APPX_BUNDLE_PLATFORMS: ${{ matrix.appxBundlePlatforms }}
ARTIFACTS_DIR: ${{ github.workspace }}\Artifacts
PACKAGE_CERTIFICATE_KEYFILE: ${{ github.workspace }}\cert.pfx
PACKAGE_CERTIFICATE_PASSWORD: ${{ secrets.PACKAGE_CERTIFICATE_PWD }}
APP_CENTER_SECRET: ${{ secrets.APP_CENTER_SECRET }}
- if: matrix.debug && !contains( matrix.appxBundlePlatforms, 'arm64' )
name: Test ARM build in debug configuration
id: build_app_arm_debug
shell: pwsh
run: |
msbuild $env:SOLUTION_NAME `
/p:Platform=$env:PLATFORM `
/p:Configuration=$env:CONFIGURATION `
/p:UapAppxPackageBuildMode=$env:UAP_APPX_PACKAGE_BUILD_MODE `
/p:AppxBundle=$env:APPX_BUNDLE `
/p:AppxBundlePlatforms=$env:APPX_BUNDLE_PLATFORMS
env:
PLATFORM: ARM64
UAP_APPX_PACKAGE_BUILD_MODE: StoreUpload
APPX_BUNDLE: Always
APPX_BUNDLE_PLATFORMS: ARM64
- if: matrix.runCodeqlAnalysis
name: Perform CodeQL Analysis
id: analyze_codeql
uses: github/codeql-action/analyze@v3
continue-on-error: true
- if: matrix.runSonarCloudScan
name: Send SonarCloud results
id: send_sonar_results
shell: pwsh
run: |
.\.sonar\scanner\dotnet-sonarscanner end `
/d:sonar.login="$env:SONAR_TOKEN"
env:
GITHUB_TOKEN: ${{ secrets.SONAR_GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- if: matrix.newVersion != ''
name: Upload build artifacts
id: upload_artifacts
uses: actions/upload-artifact@v4
with:
name: Build artifacts
path: Artifacts/
cd:
# This job will execute when the workflow is triggered on a 'workflow_dispatch' event,
# the target branch is 'master' and required parameter provided for release.
if: needs.ci.outputs.new_version != ''
needs: [ setup, ci ]
runs-on: windows-latest
env:
OLD_VERSION: ${{ needs.ci.outputs.old_version }}
NEW_VERSION: ${{ needs.ci.outputs.new_version }}
steps:
- name: Checkout repository
id: checkout_repo
uses: actions/checkout@v4
- name: Download and extract MSIX package
id: dl_package_artifact
uses: actions/download-artifact@v4
with:
name: Build artifacts
path: Artifacts/
- name: Create deployment payload
id: create_notepads_zip
shell: pwsh
run: |
Get-ChildItem -Filter *Production* -Recurse | Rename-Item -NewName { $_.name -replace "_Production|_Test",'' }
Compress-Archive -Path "Notepads_$($env:NEW_VERSION)\*" `
-DestinationPath "Notepads_$($env:NEW_VERSION)\Notepads_$($env:NEW_VERSION)_x86_x64_ARM64.zip"
working-directory: ./Artifacts
- name: Generate changelog
id: generate_changlog
uses: mrchief/universal-changelog-action@v1.3.2
with:
previousReleaseTagNameOrSha: v${{ env.OLD_VERSION }}
nextReleaseTagName: v${{ env.NEW_VERSION }}
nextReleaseName: v${{ env.NEW_VERSION }}
configFilePath: .github/RELEASE_TEMPLATE/changelog_config.json
- name: Create and publish release
id: create_release
uses: ncipollo/release-action@v1.16.0
with:
allowUpdates: true
replacesArtifacts: true
tag: v${{ env.NEW_VERSION }}
name: Notepads v${{ env.NEW_VERSION }}
body: ${{ steps.generate_changlog.outputs.changelog }}
token: ${{ secrets.GITHUB_TOKEN }}
artifacts:
Artifacts/Notepads_${{ env.NEW_VERSION }}/Notepads_${{ env.NEW_VERSION }}_x86_x64_ARM64.msixbundle
Artifacts/Notepads_${{ env.NEW_VERSION }}/Notepads_${{ env.NEW_VERSION }}_x86_x64_ARM64.zip
# - name: Publish to Windows Store
# id: publish_to_store
# uses: isaacrlevin/windows-store-action@1.0
# with:
# tenant-id: ${{ secrets.AZURE_AD_TENANT_ID }}
# client-id: ${{ secrets.AZURE_AD_APPLICATION_CLIENT_ID }}
# client-secret: ${{ secrets.AZURE_AD_APPLICATION_SECRET }}
# app-id: ${{ secrets.STORE_APP_ID }}
# package-path: "${{ github.workspace }}/Artifacts/"
# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)

355
src/.gitignore vendored

@ -0,0 +1,355 @@
## 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
# 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/
# 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
# 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
# JustCode is a .NET coding add-in
.JustCode
# 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
# 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/
_pkginfo.txt
*.appx
# 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
*- Backup*.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/
# JetBrains Rider
.idea/
*.sln.iml
# 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
# Logs and databases #
######################
*.log
*.sql
*.sqlite
# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

@ -0,0 +1,8 @@
{
"checkRunSettings": {
"vulnerableCheckRunConclusionLevel": "failure"
},
"issueSettings": {
"minSeverityLevel": "LOW"
}
}

@ -0,0 +1,419 @@
# Notepads CI/CD documentation
- after merging the PR, the first run of the "Notepads CI/CD Pipeline" workflow will not complete successfully, because it requires specific setup explained in this documentation. The two other workflows "CodeQL Analysis" and "Build", should complete successfully.
## 1. Set up SonarCloud
### SonarCloud is a cloud-based code quality and security service
#### Create SonarCloud project
- Go to https://sonarcloud.io/
- Click the "Log in" button and create a new account or connect with GitHub account (recommended)
- At the top right corner click the "+" sign
- From the dropdown select "Create new Organization"
- Click the "Choose an organization on GitHub" button
- Select an account for the organization setup
- On Repository Access select "Only select repositories" and select the project and click the "Save" button
- On the "Create organization page" don't change the Key and click "Continue"
- Select the Free plan then click the "Create Organization" button to finalize the creation of the Organization
#### Configure SonarCloud project
- At the top right corner click the "+" sign and select "Analyze new project"
- Select the project and click the "Set Up" button in the box on the right
- Under "Choose your analysis method" click "With GitHub Actions" and **keep the following page open**
- [Create a new PAT with **repo_deployment** and **read:packages** permissions](#7-how-to-create-a-pat) and copy the value of the generated token
- In the project's GitHub repository, go to the **Settings** tab -> Secrets
- Click on **New Repository secret** and create a new secret with the name **SONAR_GITHUB_TOKEN** and the token you just copied as the value
- Create another secret with the two values from the SonarCloud page you kept open, which you can close after completing this step
![SonarCloud_1](ScreenShots/CI-CD_DOCUMENTATION/SonarCloud_1.png)
- [Run the "Notepads CI/CD Pipeline" workflow manually](#2-run-workflow-manually)
#### Set Quality Gate
- After the "Notepads CI/CD Pipeline" workflow has executed successfully, go to https://sonarcloud.io/projects and click on the project
- In the alert bar above the results, click the "Set new code definition" button and select "Previous version" (notice the "New Code definition has been updated" alert at the top)
- The Quality Gate will become active as soon as the next SonarCloud scan completes successfully
<br>
<a name="workflow_dispatch"></a>
## 2. Run workflow manually
Once you've set up all the steps above correctly, you should be able to successfully complete a manual execution of the "Notepads CI/CD Pipeline" workflow.
1. Go to the project's GitHub repository and click on the **Actions** tab
2. From the "Workflows" list on the left, click on "Notepads CI/CD Pipeline"
3. On the right, next to the "This workflow has a workflow_dispatch event trigger" label, click on the "Run workflow" dropdown, make sure the default branch is selected (if not manually changed, should be main or master) in the "Use workflow from" dropdown and click the "Run workflow" button
4. You can optionally fill the argument textbox with "release" to trigger [GitHub Release](#github_release) and [Store Upload](#store_upload)
![Actions_workflow_dispatch](ScreenShots/CI-CD_DOCUMENTATION/Actions_workflow_dispatch.png)
5. Once the workflow run has completed successfully, move on to the next step of the documentation
NOTE: **screenshots are only exemplary**
<br>
## 3. Set up Dependabot
Dependabot is a GitHub native security tool that goes through the dependencies in the project and creates alerts, and PRs with updates when a new and/or non-vulnerable version is found.
- for PRs with version updates, this pipeline comes pre-configured for all current dependency sources in the project, so at "Insights" tab -> "Dependency graph" -> "Dependabot", you should be able to see all tracked sources of dependencies, when they have been checked last and view a full log of the last check
![Dependabot_tab](ScreenShots/CI-CD_DOCUMENTATION/Dependabot_tab.png)
![Dependabot_log_page](ScreenShots/CI-CD_DOCUMENTATION/Dependabot_log_page.png)
### Set up security alerts and updates
##### - GitHub, through Dependabot, also natively offers a security check for vulnerable dependencies
1. Go to "Settings" tab of the repo
2. Go to "Security & analysis" section
3. Click "Enable" for both "Dependabot alerts" and "Dependabot security updates"
- By enabling "Dependabot alerts", you would be notified for any vulnerable dependencies in the project. At "Security" tab -> "Dependabot alerts", you can manage all alerts. By clicking on an alert, you would be able to see a detailed explanation of the vulnerability and a viable solution.
![Dependabot_alerts_page](ScreenShots/CI-CD_DOCUMENTATION/Dependabot_alerts_page.png)
![Dependabot_alert_page](ScreenShots/CI-CD_DOCUMENTATION/Dependabot_alert_page.png)
- By enabling "Dependabot security updates", you authorize Dependabot to create PRs specifically for **security updates**
![Dependabot_PRs](ScreenShots/CI-CD_DOCUMENTATION/Dependabot_PRs.png)
### Set up Dependency graph
##### - The "Dependency graph" option should be enabled by default for all public repos, but in case it isn't:
1. Go to "Settings" tab of the repo
2. Go to "Security&Analysis" section
3. Click "Enable" for the "Dependency graph" option
- this option enables the "Insights" tab -> "Dependency graph" section -> "Dependencies" tab, in which all the dependencies for the project are listed, under the different manifests they are included in
![Dependabot_dependency_graph](ScreenShots/CI-CD_DOCUMENTATION/Dependabot_dependency_graph.png)
NOTE: **screenshots are only exemplary**
<br>
## 4. CodeQL
CodeQL is GitHub's own industry-leading semantic code analysis engine. CodeQL requires no setup, because it comes fully pre-configured by us.
To activate it and see its results, only a push commit or a merge of a PR to the default branch of the repository, is required.
We've also configured CodeQL to run on schedule, so every day at 8:00AM UTC, it automatically scans the code.
- you can see the results here at **Security** tab -> **Code scanning alerts** -> **CodeQL**:
![CodeQL_results](ScreenShots/CI-CD_DOCUMENTATION/CodeQL_results.png)
- on the page of each result, you can see an explanation of what the problem is and also one or more solutions:
![CodeQL_alert_page](ScreenShots/CI-CD_DOCUMENTATION/CodeQL_alert_page.png)
### Code scanning alerts bulk dismissal tool
##### - currently, GitHub allows for only 25 code scanning alerts to be dismissed at a time. Sometimes, you might have hundreds you would like to dismiss, so you will have to click many times and wait for a long time to dismiss them. Via the "csa-bulk-dismissal.yml", you can automatically dismiss unnecessary alerts or manually do that with one click.
NOTE: This tool executes automatically when **Notepads CI/CD Pipeline** action completes.
#### 1. Setup
1. In the repository, go to the **Settings** tab -> **Secrets**
![CSA_secrets](ScreenShots/CI-CD_DOCUMENTATION/CSA_secrets.png)
2. Add the following secrets with the name and the corresponding value, by at the upper right of the section, clicking on the **New repository secret** button:
![CSA_new_secret](ScreenShots/CI-CD_DOCUMENTATION/CSA_new_secret.png)
![CSA_secret_add](ScreenShots/CI-CD_DOCUMENTATION/CSA_secret_add.png)
- CSA_ACCESS_TOKEN - [create a PAT with "security_events" permission only](#7-how-to-create-a-pat).
#### 2. Execution
1. This tool is automatically triggered when **Notepads CI/CD Pipeline** task completes, if you want to manually execute this follow next steps
2. In your repo, click on the Actions tab and on the left, in the Workflows list, click on the "Code scanning alerts bulk dismissal"
![CSA_execute_1](ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_1.png)
3. On the right, click on the "Run workflow" dropdown. Under "Use workflow from" choose your default branch (usually main/master), in the **Type of filter to use** field type "path"/"desc" depending upon whether dismiss alerts based on predefined paths or description respectively (default is "path"), in the **Reason for dismissal** type "fp"/"wf"/"ut" for "false positive"/"won't fix"/"used in tests" respectively (default is "wf") and click on the **Run workflow** button
<a name="csa_execute"></a>
![CSA_execute_2](ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_2.png)
NOTE: if any unsupported values are entered default values will be used
4. If everything was set up currently in the "Setup" phase, the "Code scanning alerts bulk dismissal" workflow is going to be executed successfully, which after some time, would result in **all** previously open code scanning alerts, with a certain description be dismissed
![CSA_execute_3](ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_3.png)
![CSA_execute_4](ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_4.png)
![CSA_execute_5](ScreenShots/CI-CD_DOCUMENTATION/CSA_execute_5.png)
NOTE: "closed" refers to "dismissed" alerts
#### 3. Customization
The "setup" job in the pipeline, allows for more precise filtering of alerts to bulk dismiss. It uses the filter type to choose (filter based on path or description) from the alert to determine if it has to be dismissed or not. We've added the following paths and alert descriptions by default:
##### Paths:
- "\*/obj/\*" (if path contains `obj` folder at any position)
##### Descriptions:
- "Calls to unmanaged code"
- "Unmanaged code"
##### To add more paths, follow these steps:
1. In your source code, open ".github/workflows/csa-bulk-dismissal.yml"
2. From line 50 to 56, notice "$MATRIX = **". This is the [powershell hashtable](https://docs.microsoft.com/powershell/scripting/learn/deep-dives/everything-about-hashtable?view=powershell-7.1) of filters that the CSABD (Code scanning alerts bulk dismissal) tool uses to filter through the alerts:
![CSA_custom_3](ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_3.png)
3. To add more paths under **include** element use comma separation and followed from next line add `@{ filter = "New path" }`. Replace "New path" with the path (with or without [wild cards](https://docs.microsoft.com/powershell/module/microsoft.powershell.core/about/about_wildcards?view=powershell-7.1)) you want:
![CSA_custom_4](ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_4.png)
##### To add more descriptions, follow these steps:
1. In your source code, open ".github/workflows/csa-bulk-dismissal.yml"
2. From line 58 to 67, notice "$MATRIX = **". This is the [powershell hashtable](https://docs.microsoft.com/powershell/scripting/learn/deep-dives/everything-about-hashtable?view=powershell-7.1) of filters that the CSABD (Code scanning alerts bulk dismissal) tool uses to filter through the alerts:
![CSA_custom_1](ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_1.png)
3. To add more descriptions under **include** element use comma separation and followed from next line add `@{ filter = "New description" }`. Replace "New description" with the description you want:
![CSA_custom_2](ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_2.png)
##### To change default filter type and dismissal reason, follow these steps:
1. In your source code, open ".github/workflows/csa-bulk-dismissal.yml"
2. To change default filter type change **$FILTER_TYPE** variable in line 31 to something else (default is "path", supported are: "desc" and "path"):
![CSA_custom_5](ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_5.png)
3. To change dismissal reason change **$REASON** variable in line 45 to something else (default is "won't fix", supported are: "false positive", "won't fix" and "used in tests"):
![CSA_custom_6](ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_6.png)
NOTE: changing default filter type and dismissal reason won't change dafault value typed when [manually executing](#csa_execute) tool, change values in line 13 and 17 respectively to reflect the change
![CSA_custom_7](ScreenShots/CI-CD_DOCUMENTATION/CSA_custom_7.png)
<br>
<a name="github_release"></a>
## 5. Automated GitHub release
When triggered bumps up the GitHub tag in the repo and executes the CD job and produces release with changelogs
Note: **not every commit to your master branch are included in changelog**
#### Setup
Add the following secrets by going to the repo **Settings** tab -> **Secrets**:
1. **PACKAGE_CERTIFICATE_BASE64**
- used to dynamically create the PFX file required for the signing of the **msixbundle**
- use the following PowerShell code locally to turn your PFX file into Base64:
```
# read from PFX as binary
$PFX_FILE = [IO.File]::ReadAllBytes('absolute_path_to_PFX')
# convert to Base64 and write in txt
[System.Convert]::ToBase64String($PFX_FILE) | Out-File 'absolute_path\cert.txt'
```
- copy the contents of the **cert.txt** and paste as the value of the secret
2. **PACKAGE_CERTIFICATE_PWD**
- used in the build of the project to authenticate the PFX
- copy and paste the password of your PFX as the value of this secret
NOTE:
- none of those values are visible in the logs of the pipeline, nor are available to anyone outside of the original repository e.g. forks, anonymous clones etc.
- the dynamically created PFX file lives only for the duration of the pipeline execution
#### Execution
[Once you've set up all the steps for manual execution of the "Notepads CI/CD Pipeline" workflow correctly](#workflow_dispatch), you should be able to successfully trigger release with the same workflow.
1. Go to the project's GitHub repository and click on the **Actions** tab
2. From the "Workflows" list on the left, click on "Notepads CI/CD Pipeline"
3. On the right, next to the "This workflow has a workflow_dispatch event trigger" label, click on the "Run workflow" dropdown, make sure the default branch is selected (if not manually changed, should be main or master) in the "Use workflow from" dropdown, type "release" in the argument textbox (By default "test" is typed) and click the "Run workflow" button
![Actions_workflow_dispatch](ScreenShots/CI-CD_DOCUMENTATION/Actions_workflow_dispatch.png)
4. The workflow will produce release assets and calculate version, generate changelogs from valid commits since previous tag.
NOTE: **screenshots are only exemplary**
<br>
#### - follow these instructions for any commit (push or PR merge) to your master branch, you would like to see in changelog and count towards version change.
You would need one of three keywords at the start of your commit title. Each of the three keywords corresponds to a number in your release version i.e. v1.2.3. The release versioning uses the ["Conventional Commits" specification](https://www.conventionalcommits.org/en/v1.0.0/):
- "fix: ..." - this keyword corresponds to the last number v1.2.**3**, also known as PATCH;
- "feat: ..." - this keyword corresponds to the middle number v1.**2**.3, also known as MINOR;
- "perf: ..." - this keyword corresponds to the first number v**1**.2.3, also known as MAJOR. In addition, to trigger a MAJOR release, you would need to write "BREAKING CHANGE: ..." in the description of the commit, with an empty line above it to indicate it is in the <footer> portion of the description;
Note: when making a MAJOR release by committing through a terminal, use the multiple line syntax to add the commit title on one line and then adding an empty line, and then adding the "BREAKING CHANGE: " label
<br><br>
#### Examples
Example(fix/PATCH): <br>
`git commit -a -m "fix: this is a PATCH release triggering commit"`
<br>
`git push origin master`
<br>
<br>
On triggering `Release`:
<br>
Result: v1.2.3 -> **v1.2.4**
<br>
<br>
<br>
Example(feat/MINOR): <br>
`git commit -a -m "feat: this is a MINOR release triggering commit"`
<br>
`git push origin master`
<br>
<br>
On triggering `Release`:
<br>
Result: v1.2.3 -> **v1.3.0**
<br>
<br>
<br>
Example(perf/MAJOR): <br>
`` git commit -a -m "perf: this is a MAJOR release triggering commit ` ``
<br>
&gt;&gt; <br>
&gt;&gt; `BREAKING CHANGE: this is the breaking change"`
<br>
`git push origin master`
<br>
<br>
On triggering `Release`:
<br>
Result: v1.2.3 -> **v2.0.0**
<br>
<br>
Note: in the MAJOR release example, the PowerShell multiline syntax ` (backtick) is used. After writing a backtick, a press of the Enter key should open a new line.
<br>
<a name="store_upload"></a>
## 6. Setup automated publishing to the Windows Store
#### - for the automation to work, at least one submission needs to be already created manually
- [Create an Azure AD tenant](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-create-new-tenant) or use an existing one
- Associate your [Microsoft Partner Center with the Azure AD tenant](https://docs.microsoft.com/en-us/windows/uwp/publish/associate-azure-ad-with-partner-center)
- [Create a new app registration](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml) or use an existing one from the list in your **portal.azure.com** -> **Azure Active Directory** -> **App registrations** section
- Add the [Azure AD application to the Microsoft Partner Center](https://docs.microsoft.com/en-us/partner-center/service-principal) and give it "Manager" permissions
- In the project's GitHub repo, create the following secrets:
1. **AZURE_AD_TENANT_ID** and **AZURE_AD_APPLICATION_CLIENT_ID**
- copy and paste the values shown in the screenshot below to the appropriate secret:
![Publish_to_store_1](ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_1.png)
Note: screenshot is taken from **portal.azure.com** -> **Azure AD** -> **App registrations** -> **app-name** page
2. **AZURE_AD_APPLICATION_SECRET**
- copy and paste the value you get on the page following from **Account settings** -> **User management** -> **Azure AD applications** -> click on the added application:
![Publish_to_store_2](ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_2.png)
3. **STORE_APP_ID**
- copy and paste the highlighted code as the value of this secret:
![Publish_to_store_3](ScreenShots/CI-CD_DOCUMENTATION/Publish_to_store_3.png)
- If everything was setup correctly, on your next push commit to the `master` branch with a new `Identity.Version` in the `Package.appxmanifest`, a new submission in the Microsoft Partner Center with the new `*.msixupload` package should appear and be automatically submitted if all verifications pass
<br>
## 7. How to create a PAT
- In a new tab open GitHub, at the top right corner, click on your profile picture and click on **Settings** from the dropdown.
![CSA_new_pat_1](ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_1.png)
- Go to Developer Settings -> Personal access tokens.
![CSA_new_pat_2](ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_2.png)
![CSA_new_pat_3](ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_3.png)
- Click the **Generate new token** button and enter password if prompted.
![CSA_new_pat_4](ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_4.png)
- Name the token, from the permissions list choose the ones needed and at the bottom click on the **Generate token** button.
![CSA_new_pat_5](ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_5.png)
- Copy the token value and paste it wherever its needed
![CSA_new_pat_6](ScreenShots/CI-CD_DOCUMENTATION/CSA_new_pat_6.png)
NOTE: once you close or refresh the page, you won't be able to copy the value of the PAT again!
#
Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)

@ -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 NotepadsApp@outlook.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

@ -0,0 +1,35 @@
# How to Contribute:
You can contribute to Notepads project by:
- Report issues and bugs [here](https://github.com/0x7c13/Notepads/issues)
- Submit feature requests [here](https://github.com/0x7c13/Notepads/issues)
- Create a pull request to help me (Let me know before you do so):
* Fix an existing bug, prefix title with `fix: `.
* Implement new features, prefix title with `feat: `.
* Fix grammar errors or improve my documentations, prefix title with `doc: `.
* Improve CI/CD pipeline, prefix title with `ci: `.
* Cleanup code and code refactoring or anything else you want to change in the project not listed above, prefix title with `other: ` or assign a custom prefix with the same format (`label: `).
- Internationalization and localization:
* My only inputs for the work here is to recommend you guys to use existing phrases that you found in win32 notepad.exe or vs code or notepad++ as much as possible. It makes your translations more consistent and easier to understand by end users.
* Since Notepads is still in early beta. I might change texts and add texts now and then for the upcoming months. Whenever that happens, I will notify you in [Notepads Discord Server](https://discord.gg/VqetCub) (Please join it if possible) and in [GitHub Discussions](https://github.com/0x7c13/Notepads/discussions/818) (Subscribe to notifications). If someday you lose the passion, feel free to let me know so I can assign your language to others.
* OK, here are the steps you need to follow if you want to contribute:
1. Make sure you can build and run Notepads project on your machine so that you can test it after your work.
2. Click [here](https://github.com/0x7c13/Notepads/discussions/818) and provide your information.
3. Do your work and test it on your machine and check your work to make sure it is not breaking any existing layout.
4. Finish your work and create a PR, prefix PR title with `lang: ` (Example: https://github.com/0x7c13/Notepads/pull/30)
5. Let me know and I will merge it if it looks good to me.
Notes: You should use the language code as your folder name listed here: https://docs.microsoft.com/en-us/windows/uwp/publish/supported-languages
Note: This repository follows [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), format your pull request title according to specifications.
# How to Build and Run Notepads from source:
* Make sure your machine is running on Windows 10 1903+.
* Make sure you have Visual Studio 2019 16.2 or newer installed.
* Make sure you have "Universal Windows Platform development" component installed for Visual Studio.
* Make sure you installed "Windows 10 SDK (10.0.17763.0 + 10.0.19041.0)" as well.
* Open src/Notepads.sln with Visual Studio and set Solution Platform to x64(amd64).
* Once opened, right click on the solution and click on "Restore NuGet Packages".
* Now you should be able to build and run Notepads on your machine. If it fails, try close the solution and reopen it again.
# TL;DR:
This is my first UWP project and I learn as I go. As a result, the code base is not well organized, and it is not well written. The philosophy here is to create a text editor that is easy to use, lightweight and yet stylish instead of creating another Notepad++ or VS Code in anyway. If you are looking for a code/programming editor, you might want to use VS Code instead. If you are looking for a lightweight text editor, you come to the right place. Notepads is here to help you do small things quicker and you should always install and use other editors that suit your need.

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-2024 Jackie (Jiaqi) Liu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1 +0,0 @@
Subproject commit 39a2159ca24d2c04889b651c41889c72a7a9db43

@ -0,0 +1,24 @@
Privacy Policy
----------------
### Introduction
Our privacy policy will help you understand what information we collect by *[Notepads](https://github.com/0x7c13/Notepads)* app, how *[Notepads](https://github.com/0x7c13/Notepads)* app uses it, and what choices you have.
*[0x7c13](https://github.com/0x7c13)* with the help of the Github Notepads App community built the *[Notepads](https://github.com/0x7c13/Notepads)* app as a free app. This APP is provided by *[0x7c13](https://github.com/0x7c13)* at no cost and is intended for use as is.
If you choose to use this app, then you agree to the collection and use of information in relation with this policy. The data that we collect are used for providing and improving the app service. We will not use or share your information or usage data with anyone except as described in this Privacy Policy.
### Information Collection and Use
For a better experience while using this app, certain usage data and errors are collected for identifying issues or improving the user experience of the app. *[Visual Studio AppCenter](https://visualstudio.microsoft.com/app-center/)* analytics service is used in this app to collect basic usage data plus some minimum telemetry to help debug runtime errors. See thread [#334](https://github.com/0x7c13/Notepads/issues/334) for more details.
The app does NOT use third party services that may collect information used to identify you.
Note: Visual Studio App Center is scheduled for retirement on March 31, 2025. Notepads v1.5.6.0+ starts to use Microsoft Store Services SDK to log non-privacy usage data and errors.
### Data Security
We value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its absolute security.
### Changes to This Privacy Policy
We may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately, after they are posted on this page.
### Contact Us
If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.
Contact Information:
Email: notepadsapp@outlook.com

@ -0,0 +1,145 @@
<p align="center">
<img width="128" align="center" src="src/Notepads/Assets/appicon_ws.gif">
</p>
<h1 align="center">
Notepads
</h1>
<p align="center">
A modern, lightweight text editor with a minimalist design.
</p>
<p align="center">
<a style="text-decoration:none" href="https://www.microsoft.com/store/apps/9nhl4nsc67wm">
<img src="https://img.shields.io/badge/Microsoft%20Store-Download-orange.svg?style=flat-square" alt="Store link" />
</a>
<a style="text-decoration:none" href="https://github.com/0x7c13/Notepads/releases">
<img src="https://img.shields.io/github/release/0x7c13/notepads.svg?label=latest%20version&style=flat-square" alt="Releases" />
</a>
<a style="text-decoration:none">
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20UWP-yellow.svg?style=flat-square" alt="Platform" />
</a>
<a style="text-decoration:none" href="https://discord.gg/VqetCub">
<img src="https://img.shields.io/discord/588473626651787274.svg?style=flat-square" alt="Discord" />
</a>
</p>
## What is Notepads and why do I care?
I have been waiting long enough for a modern Windows 10 notepad app to come before I decided to create one myself. Dont get me wrong, Notepad++, VS Code, and Sublime are great text editors. I have used them all and I will continue to use them in the future. However, they are either too heavy or look less appealing. There are times that I just wanted to use Windows notepad for things like writing notes or editing config files. So I decided to create a win32 notepad replacement here and try to give it a modern look and feel. Most importantly, it has to be blazingly fast and appeals to everyone.
So here comes the “Notepads” 🎉 (s stands for Sets).
* Fluent design with a built-in tab system.
* Blazingly fast and lightweight.
* Launch from the command line or PowerShell by typing: `notepads` or `notepads %FilePath%`.
* Multi-line handwriting support.
* Built-in Markdown live preview.
* Built-in diff viewer (preview your changes).
* Session snapshot and multi-instances.
![Screenshot Dark](ScreenShots/1.png?raw=true "Dark")
![Screenshot Markdown](ScreenShots/2.png?raw=true "Markdown")
![Screenshot DiffViewer](ScreenShots/3.png?raw=true "DiffViewer")
![Screenshot Light](ScreenShots/4.png?raw=true "Light")
## Shortcuts:
* Ctrl+N/T to create new tab.
* Ctrl+(Shift)+Tab to switch between tabs.
* Ctrl+Num(1-9) to quickly switch to specified tab.
* Ctrl+"+"/"-" for zooming. Ctrl+"0" to reset zooming to default.
* Ctrl+L/R to change text flow direction. (LTR/RTL)
* Alt+P to toggle preview split view for Markdown file.
* Alt+D to toggle side-by-side diff viewer.
## Platform limitations (UWP):
* You won't be able to save files to system folders due to UWP restriction (windows, system32, etc.).
* You cannot associate potentially harmful file types (.cmd, .bat etc.) with Notepads.
* Notepads does not work well with large files; the file size limit is set to 1MB for now. I will add large file support later.
## Downloads:
Notepads is available in the Microsoft Store. You can get the latest version of Notepads here for free: [Microsoft Store Link](https://www.microsoft.com/store/apps/9nhl4nsc67wm).
You can also use the Windows Package Manager to install notepads:
```cmd
winget install "Notepads App"
```
## Changelog:
* [Notepads Releases](https://github.com/0x7c13/Notepads/releases)
## Disclaimer and Privacy statement:
To be 100% transparent:
* Notepads does not and will never collect user information in terms of user privacy.
* I will not track your IP.
* I will not record your typings or read any of your files created in Notepads including file name and file path.
* No typings or files will be sent to me or third parties.
I am using analytics service "AppCenter" to collect basic usage data plus some minimum telemetry to help me debug runtime errors. Here is the thread I made clear on this topic: https://github.com/0x7c13/Notepads/issues/334
Feel free to review the source code or build your own version of Notepads since it is 100% open sourced.
#### More to read here: [[Privacy Policy](PRIVACY.md)]
TL;DR: You might notice that I work for Microsoft but Notepads is my personal project that I accomplish during free time (to empower every person and every organization on the planet to achieve more😃). I do not work for the Windows team, nor do I work for a Microsoft UX/App team. I am not expert on creating Windows apps either. I learned how to code UWP as soon as I started this project, so dont put too much hope on me or treat it as a project sponsored by Microsoft.
## Contributing:
* [How to contribute?](CONTRIBUTING.md)
* Notepads is free and open source, if you like my work, please consider:
* Star this project on GitHub
* Leave me a review [here](https://www.microsoft.com/store/apps/9nhl4nsc67wm)
* [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/D1D6Y3C6)
## Dependencies and References:
* [Windows Community Toolkit](https://github.com/windows-toolkit/WindowsCommunityToolkit)
* [XAML Controls Gallery](https://github.com/microsoft/Xaml-Controls-Gallery)
* [Windows UI Library](https://github.com/Microsoft/microsoft-ui-xaml)
* [ColorCode Universal](https://github.com/WilliamABradley/ColorCode-Universal)
* [UTF Unknown](https://github.com/CharsetDetector/UTF-unknown)
* [DiffPlex](https://github.com/mmanela/diffplex)
* [Win2D](https://github.com/microsoft/Win2D)
## Special Thanks:
* [Yi Zhou](http://zhouyiwork.com/) - App icon designer, Notepads App Icon (old) is greatly inspired by the new icon for Windows Terminal.
* [Mahmoud Qurashy](https://github.com/mah-qurashy) - App icon and file icon(s) designer, creator of the new Notepads App Icon.
* Alexandru Sterpu - App Tester, who helped me a lot during preview/beta testing.
* Code Contributors: [DanverZ](https://github.com/chenghanzou), [BernhardWebstudio](https://github.com/BernhardWebstudio), [Csányi István](https://github.com/AmionSky), [Pavel Erokhin](https://github.com/MairwunNx), [Sergio Pedri](https://github.com/Sergio0694), [Lucas Pinho B. Santos](https://github.com/pinholucas), [Soumya Ranjan Mahunt](https://github.com/soumyamahunt), [Belleve Invis](https://github.com/be5invis), [Maickonn Richard](https://github.com/Maickonn), [Xam](https://github.com/XamDR)
* Documentation Contributors: [Craig S.](https://github.com/sercraig)
* Localization Contributors:
* [fr-FR][French (France)]: [François Rousselet](https://github.com/frousselet), [François-Joseph du Fou](https://github.com/FJduFou), [Armand Delessert](https://github.com/ArmandDelessert)
* [es-ES][Spanish (Spain)]: [Jose Pinilla](https://github.com/joseppinilla)
* [zh-CN][Chinese (S)]: [lindexi](https://github.com/lindexi), [walterlv](https://github.com/walterlv), [0x7c13](https://github.com/0x7c13)
* [hu-HU][Hungarian (Hungary)]: [Csányi István](https://github.com/AmionSky), [Kristóf Kékesi](https://github.com/KristofKekesi)
* [tr-TR][Turkish (Turkey)]: [Mert Can Demir](https://github.com/validatedev), [Emirhakan Tanhan](https://github.com/EmirhakanTanhan)
* [ja-JP][Japanese (Japan)]: [Mamoru Satoh](https://github.com/pnp0a03)
* [de-DE][German (Germany)]/[de-CH][German (Switzerland)]: [Walter Wolf](https://github.com/WalterWolf49)
* [ru-RU][Russian (Russia)]: [Pavel Erokhin](https://github.com/MairwunNx), [krlvm](https://github.com/krlvm)
* [fi-FI][Finnish (Finland)]: [Esa Elo](https://github.com/sauihdik)
* [uk-UA][Ukrainian (Ukraine)]: [Taras Fomin aka Tarik02](https://github.com/Tarik02)
* [it-IT][Italian (Italy)]: [Andrea Guarinoni](https://github.com/guari), [Bunz](https://github.com/66Bunz)
* [cs-CZ][Czech (Czech Republic)]: [Jan Rajnoha](https://github.com/JanRajnoha)
* [pt-BR][Portuguese (Brazil)]: [Lucas Pinho B. Santos](https://github.com/pinholucas)
* [ko-KR][Korean (Korea)]: [Donghyeok Tak](https://github.com/tdh8316)
* [hi-IN][Hindi (India)]/[or-IN][Odia (India)]: [Soumya Ranjan Mahunt](https://github.com/soumyamahunt)
* [pl-PL][Polish (Poland)]: [Daxxxis](https://github.com/Daxxxis)
* [ka-GE][Georgian (Georgia)]: [guram mazanashvili](https://github.com/gmaza)
* [hr-HR][Croatian (Croatia)]: [milotype](https://github.com/milotype)
* [zh-TW][Chinese (T)]: [Tony Yao](https://github.com/SeaBao)
* [pt-PT][Portuguese (Portugal)]: [O.Leitão](https://github.com/oleitao)
* [sr-Latn][Serbian (Latin)]: [bzzrak](https://github.com/bzzrak)
* [sr-cyrl][Serbian (Cyrillic)]: [bzzrak](https://github.com/bzzrak)
* [nl-NL][Dutch (Netherlands)]: [Stephan Paternotte](https://github.com/Stephan-P)
* Notepads CI/CD pipeline: Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)
[![](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/images/0)](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/0)[![](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/images/1)](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/1)[![](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/images/2)](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/2)[![](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/images/3)](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/3)[![](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/images/4)](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/4)[![](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/images/5)](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/5)[![](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/images/6)](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/6)[![](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/images/7)](https://sourcerer.io/fame/0x7c13/0x7c13/Notepads/links/7)
## Stay tuned 📢:
* [Notepads Discord Server](https://discord.gg/VqetCub)

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

@ -0,0 +1,39 @@
# Universal Windows Platform build definition
trigger:
paths:
exclude:
- '*.md'
- 'ScreenShots/'
- '.whitesource'
- '.github/'
tags:
exclude:
- '*'
pool:
vmImage: 'windows-latest'
variables:
solution: '**/*.sln'
buildPlatform: 'x86|x64|arm64'
buildConfiguration: 'Production'
appxPackageDir: '$(build.artifactStagingDirectory)\AppxPackages\\'
steps:
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: '$(solution)'
- task: VSBuild@1
inputs:
platform: 'x64'
solution: '$(solution)'
configuration: '$(buildConfiguration)'
msbuildArgs: '/p:AppxBundlePlatforms="$(buildPlatform)"
/p:AppxPackageDir="$(appxPackageDir)"
/p:AppxBundle=Always
/p:UapAppxPackageBuildMode=StoreUpload
/p:AppxPackageSigningEnabled=false'

@ -0,0 +1,323 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# this. and Me. preferences
dotnet_style_qualification_for_event = false:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_property = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
# Expression-level preferences
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_inlined_variable_declaration = true:silent
csharp_style_throw_expression = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
# Field preferences
dotnet_style_readonly_field = true:suggestion
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = true:silent
csharp_style_var_for_built_in_types = true:silent
csharp_style_var_when_type_is_apparent = true:silent
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = true:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:silent
csharp_style_pattern_matching_over_is_with_cast_check = true:silent
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
# Code-block preferences
csharp_prefer_braces = false:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
# Naming Symbols
# constant_fields - Define constant fields
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
# non_private_readonly_fields - Define public, internal and protected readonly fields
dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, internal, protected
dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
# static_readonly_fields - Define static and readonly fields
dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly
# private_readonly_fields - Define private readonly fields
dotnet_naming_symbols.private_readonly_fields.applicable_accessibilities = private
dotnet_naming_symbols.private_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.private_readonly_fields.required_modifiers = readonly
# public_internal_fields - Define public and internal fields
dotnet_naming_symbols.public_internal_protected_fields.applicable_accessibilities = public, internal, protected
dotnet_naming_symbols.public_internal_protected_fields.applicable_kinds = field
# private_protected_fields - Define private and protected fields
dotnet_naming_symbols.private_protected_fields.applicable_accessibilities = private, protected
dotnet_naming_symbols.private_protected_fields.applicable_kinds = field
# public_symbols - Define any public symbol
dotnet_naming_symbols.public_symbols.applicable_accessibilities = public, internal, protected, protected_internal
dotnet_naming_symbols.public_symbols.applicable_kinds = method, property, event, delegate
# parameters - Defines any parameter
dotnet_naming_symbols.parameters.applicable_kinds = parameter
# non_interface_types - Defines class, struct, enum and delegate types
dotnet_naming_symbols.non_interface_types.applicable_kinds = class, struct, enum, delegate
# interface_types - Defines interfaces
dotnet_naming_symbols.interface_types.applicable_kinds = interface
# Naming Styles
# camel_case - Define the camelCase style
dotnet_naming_style.camel_case.capitalization = camel_case
# pascal_case - Define the Pascal_case style
dotnet_naming_style.pascal_case.capitalization = pascal_case
# first_upper - The first character must start with an upper-case character
dotnet_naming_style.first_upper.capitalization = first_word_upper
# prefix_interface_interface_with_i - Interfaces must be PascalCase and the first character of an interface must be an 'I'
dotnet_naming_style.prefix_interface_interface_with_i.capitalization = pascal_case
dotnet_naming_style.prefix_interface_interface_with_i.required_prefix = I
# Naming Rules
# Async
dotnet_naming_rule.async_methods_end_in_async.severity = silent
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
dotnet_naming_symbols.any_async_methods.applicable_kinds = method
dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
dotnet_naming_symbols.any_async_methods.required_modifiers = async
dotnet_naming_style.end_in_async.required_suffix = Async
dotnet_naming_style.end_in_async.capitalization = pascal_case
# Constant fields must be PascalCase
dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = silent
dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case
# Public, internal and protected readonly fields must be PascalCase
dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.severity = silent
dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.symbols = non_private_readonly_fields
dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pascal_case
# Static readonly fields must be PascalCase
dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = silent
dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields
dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case
# Private readonly fields must be camelCase
dotnet_naming_rule.private_readonly_fields_must_be_camel_case.severity = silent
dotnet_naming_rule.private_readonly_fields_must_be_camel_case.symbols = private_readonly_fields
dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = camel_case
# Public and internal fields must be PascalCase
dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.severity = silent
dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.symbols = public_internal_protected_fields
dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.style = pascal_case
# Private and protected fields must be camelCase
dotnet_naming_rule.private_fields_must_be_camel_case.severity = silent
dotnet_naming_rule.private_fields_must_be_camel_case.symbols = private_protected_fields
dotnet_naming_rule.private_fields_must_be_camel_case.style = prefix_private_field_with_underscore
# Public members must be capitalized
dotnet_naming_rule.public_members_must_be_capitalized.severity = silent
dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols
dotnet_naming_rule.public_members_must_be_capitalized.style = first_upper
# Parameters must be camelCase
dotnet_naming_rule.parameters_must_be_camel_case.severity = silent
dotnet_naming_rule.parameters_must_be_camel_case.symbols = parameters
dotnet_naming_rule.parameters_must_be_camel_case.style = camel_case
# Class, struct, enum and delegates must be PascalCase
dotnet_naming_rule.non_interface_types_must_be_pascal_case.severity = silent
dotnet_naming_rule.non_interface_types_must_be_pascal_case.symbols = non_interface_types
dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case
# Interfaces must be PascalCase and start with an 'I'
dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = silent
dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types
dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i
# prefix_private_field_with_underscore - Private fields must be prefixed with _
dotnet_naming_style.prefix_private_field_with_underscore.capitalization = camel_case
dotnet_naming_style.prefix_private_field_with_underscore.required_prefix = _
# Code files
[*.{cs,vb}]
# Migrate back from old Toolkit.ruleset
dotnet_diagnostic.CA1001.severity = warning
dotnet_diagnostic.CA1009.severity = warning
dotnet_diagnostic.CA1016.severity = warning
dotnet_diagnostic.CA1033.severity = warning
dotnet_diagnostic.CA1049.severity = warning
dotnet_diagnostic.CA1060.severity = warning
dotnet_diagnostic.CA1061.severity = warning
dotnet_diagnostic.CA1063.severity = warning
dotnet_diagnostic.CA1065.severity = warning
dotnet_diagnostic.CA1301.severity = warning
dotnet_diagnostic.CA1400.severity = warning
dotnet_diagnostic.CA1401.severity = warning
dotnet_diagnostic.CA1403.severity = warning
dotnet_diagnostic.CA1404.severity = warning
dotnet_diagnostic.CA1405.severity = warning
dotnet_diagnostic.CA1410.severity = warning
dotnet_diagnostic.CA1415.severity = warning
dotnet_diagnostic.CA1821.severity = warning
dotnet_diagnostic.CA1900.severity = warning
dotnet_diagnostic.CA1901.severity = warning
dotnet_diagnostic.CA2002.severity = warning
dotnet_diagnostic.CA2100.severity = warning
dotnet_diagnostic.CA2101.severity = warning
dotnet_diagnostic.CA2108.severity = warning
dotnet_diagnostic.CA2111.severity = warning
dotnet_diagnostic.CA2112.severity = warning
dotnet_diagnostic.CA2114.severity = warning
dotnet_diagnostic.CA2116.severity = warning
dotnet_diagnostic.CA2117.severity = warning
dotnet_diagnostic.CA2122.severity = warning
dotnet_diagnostic.CA2123.severity = warning
dotnet_diagnostic.CA2124.severity = warning
dotnet_diagnostic.CA2126.severity = warning
dotnet_diagnostic.CA2131.severity = warning
dotnet_diagnostic.CA2132.severity = warning
dotnet_diagnostic.CA2133.severity = warning
dotnet_diagnostic.CA2134.severity = warning
dotnet_diagnostic.CA2137.severity = warning
dotnet_diagnostic.CA2138.severity = warning
dotnet_diagnostic.CA2140.severity = warning
dotnet_diagnostic.CA2141.severity = warning
dotnet_diagnostic.CA2146.severity = warning
dotnet_diagnostic.CA2147.severity = warning
dotnet_diagnostic.CA2149.severity = warning
dotnet_diagnostic.CA2200.severity = warning
dotnet_diagnostic.CA2202.severity = warning
dotnet_diagnostic.CA2207.severity = warning
dotnet_diagnostic.CA2212.severity = warning
dotnet_diagnostic.CA2213.severity = warning
dotnet_diagnostic.CA2214.severity = warning
dotnet_diagnostic.CA2216.severity = warning
dotnet_diagnostic.CA2220.severity = warning
dotnet_diagnostic.CA2229.severity = warning
dotnet_diagnostic.CA2231.severity = warning
dotnet_diagnostic.CA2232.severity = warning
dotnet_diagnostic.CA2235.severity = warning
dotnet_diagnostic.CA2236.severity = warning
dotnet_diagnostic.CA2237.severity = warning
dotnet_diagnostic.CA2238.severity = warning
dotnet_diagnostic.CA2240.severity = warning
dotnet_diagnostic.CA2241.severity = warning
dotnet_diagnostic.CA2242.severity = warning
dotnet_diagnostic.CA1031.severity = none # Disable https://docs.microsoft.com/en-us/visualstudio/code-quality/ca1031?view=vs-2019
dotnet_diagnostic.SA1011.severity = none
dotnet_diagnostic.SA1101.severity = none
dotnet_diagnostic.SA1118.severity = none
dotnet_diagnostic.SA1200.severity = none
dotnet_diagnostic.SA1201.severity = none
dotnet_diagnostic.SA1202.severity = none
dotnet_diagnostic.SA1309.severity = none
dotnet_diagnostic.SA1310.severity = none
dotnet_diagnostic.SA1600.severity = none
dotnet_diagnostic.SA1602.severity = none
dotnet_diagnostic.SA1611.severity = none
dotnet_diagnostic.SA1633.severity = none
dotnet_diagnostic.SA1634.severity = none
dotnet_diagnostic.SA1652.severity = none
dotnet_diagnostic.SA1629.severity = none # DocumentationTextMustEndWithAPeriod: Let's enable this rule back when we shift to WinUI3 (v8.x). If we do it now, it would mean more than 400 file changes.
dotnet_diagnostic.SA1413.severity = none # UseTrailingCommasInMultiLineInitializers: This would also mean a lot of changes at the end of all multiline intializers. It's also debatable if we want this or not.
dotnet_diagnostic.SA1314.severity = none # TypeParameterNamesMustBeginWithT: We do have a few templates that don't start with T. We need to double check that changing this is not a breaking change. If not, we can re-enable this.

@ -0,0 +1,204 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/DropShadowPanel
namespace Notepads.Controls
{
using Windows.UI;
using Windows.UI.Composition;
using Windows.UI.Xaml;
/// <summary>
/// The <see cref="DropShadowPanel"/> control allows the creation of a DropShadow for any Xaml FrameworkElement in markup
/// making it easier to add shadows to Xaml without having to directly drop down to Windows.UI.Composition APIs.
/// </summary>
public partial class DropShadowPanel
{
/// <summary>
/// Identifies the <see cref="BlurRadius"/> dependency property.
/// </summary>
public static readonly DependencyProperty BlurRadiusProperty =
DependencyProperty.Register(nameof(BlurRadius), typeof(double), typeof(DropShadowPanel), new PropertyMetadata(9.0, OnBlurRadiusChanged));
/// <summary>
/// Identifies the <see cref="Color"/> dependency property.
/// </summary>
public static readonly DependencyProperty ColorProperty =
DependencyProperty.Register(nameof(Color), typeof(Color), typeof(DropShadowPanel), new PropertyMetadata(Colors.Black, OnColorChanged));
/// <summary>
/// Identifies the <see cref="OffsetX"/> dependency property.
/// </summary>
public static readonly DependencyProperty OffsetXProperty =
DependencyProperty.Register(nameof(OffsetX), typeof(double), typeof(DropShadowPanel), new PropertyMetadata(0.0, OnOffsetXChanged));
/// <summary>
/// Identifies the <see cref="OffsetY"/> dependency property.
/// </summary>
public static readonly DependencyProperty OffsetYProperty =
DependencyProperty.Register(nameof(OffsetY), typeof(double), typeof(DropShadowPanel), new PropertyMetadata(0.0, OnOffsetYChanged));
/// <summary>
/// Identifies the <see cref="OffsetZ"/> dependency property.
/// </summary>
public static readonly DependencyProperty OffsetZProperty =
DependencyProperty.Register(nameof(OffsetZ), typeof(double), typeof(DropShadowPanel), new PropertyMetadata(0.0, OnOffsetZChanged));
/// <summary>
/// Identifies the <see cref="ShadowOpacity"/> dependency property.
/// </summary>
public static readonly DependencyProperty ShadowOpacityProperty =
DependencyProperty.Register(nameof(ShadowOpacity), typeof(double), typeof(DropShadowPanel), new PropertyMetadata(1.0, OnShadowOpacityChanged));
/// <summary>
/// Identifies the <see cref="IsMasked"/> dependency property.
/// </summary>
public static readonly DependencyProperty IsMaskedProperty =
DependencyProperty.Register(nameof(IsMasked), typeof(bool), typeof(DropShadowPanel), new PropertyMetadata(true, OnIsMaskedChanged));
/// <summary>
/// Gets DropShadow. Exposes the underlying composition object to allow custom Windows.UI.Composition animations.
/// </summary>
public DropShadow DropShadow => _dropShadow;
/// <summary>
/// Gets or sets the mask of the underlying <see cref="Windows.UI.Composition.DropShadow"/>.
/// Allows for a custom <see cref="Windows.UI.Composition.CompositionBrush"/> to be set.
/// </summary>
public CompositionBrush Mask
{
get => _dropShadow?.Mask;
set
{
if (_dropShadow != null)
{
_dropShadow.Mask = value;
}
}
}
/// <summary>
/// Gets or sets the blur radius of the drop shadow.
/// </summary>
public double BlurRadius
{
get => (double)GetValue(BlurRadiusProperty);
set => SetValue(BlurRadiusProperty, value);
}
/// <summary>
/// Gets or sets the color of the drop shadow.
/// </summary>
public Color Color
{
get => (Color)GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
/// <summary>
/// Gets or sets the x offset of the drop shadow.
/// </summary>
public double OffsetX
{
get => (double)GetValue(OffsetXProperty);
set => SetValue(OffsetXProperty, value);
}
/// <summary>
/// Gets or sets the y offset of the drop shadow.
/// </summary>
public double OffsetY
{
get => (double)GetValue(OffsetYProperty);
set => SetValue(OffsetYProperty, value);
}
/// <summary>
/// Gets or sets the z offset of the drop shadow.
/// </summary>
public double OffsetZ
{
get => (double)GetValue(OffsetZProperty);
set => SetValue(OffsetZProperty, value);
}
/// <summary>
/// Gets or sets the opacity of the drop shadow.
/// </summary>
public double ShadowOpacity
{
get => (double)GetValue(ShadowOpacityProperty);
set => SetValue(ShadowOpacityProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the panel uses an alpha mask to create a more precise shadow vs. a quicker rectangle shape.
/// </summary>
/// <remarks>
/// Turn this off to lose fidelity and gain performance of the panel.
/// </remarks>
public bool IsMasked
{
get => (bool)GetValue(IsMaskedProperty);
set => SetValue(IsMaskedProperty, value);
}
private static void OnBlurRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DropShadowPanel panel)
{
panel.OnBlurRadiusChanged((double)e.NewValue);
}
}
private static void OnColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DropShadowPanel panel)
{
panel.OnColorChanged((Color)e.NewValue);
}
}
private static void OnOffsetXChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DropShadowPanel panel)
{
panel.OnOffsetXChanged((double)e.NewValue);
}
}
private static void OnOffsetYChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DropShadowPanel panel)
{
panel.OnOffsetYChanged((double)e.NewValue);
}
}
private static void OnOffsetZChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DropShadowPanel panel)
{
panel.OnOffsetZChanged((double)e.NewValue);
}
}
private static void OnShadowOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DropShadowPanel panel)
{
panel.OnShadowOpacityChanged((double)e.NewValue);
}
}
private static void OnIsMaskedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DropShadowPanel panel)
{
panel.UpdateShadowMask();
}
}
}
}

@ -0,0 +1,191 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/DropShadowPanel
namespace Notepads.Controls
{
using System.Numerics;
using Windows.UI;
using Windows.UI.Composition;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Hosting;
using Windows.UI.Xaml.Shapes;
/// <summary>
/// The <see cref="DropShadowPanel"/> control allows the creation of a DropShadow for any Xaml FrameworkElement in markup
/// making it easier to add shadows to Xaml without having to directly drop down to Windows.UI.Composition APIs.
/// </summary>
[TemplatePart(Name = PartShadow, Type = typeof(Border))]
public partial class DropShadowPanel : ContentControl
{
private const string PartShadow = "ShadowElement";
private readonly DropShadow _dropShadow;
private readonly SpriteVisual _shadowVisual;
private Border _border;
/// <summary>
/// Initializes a new instance of the <see cref="DropShadowPanel"/> class.
/// </summary>
public DropShadowPanel()
{
DefaultStyleKey = typeof(DropShadowPanel);
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
_shadowVisual = compositor.CreateSpriteVisual();
_dropShadow = compositor.CreateDropShadow();
_shadowVisual.Shadow = _dropShadow;
}
/// <summary>
/// Update the visual state of the control when its template is changed.
/// </summary>
protected override void OnApplyTemplate()
{
_border = GetTemplateChild(PartShadow) as Border;
if (_border != null)
{
ElementCompositionPreview.SetElementChildVisual(_border, _shadowVisual);
}
ConfigureShadowVisualForCastingElement();
base.OnApplyTemplate();
}
/// <inheritdoc/>
protected override void OnContentChanged(object oldContent, object newContent)
{
if (oldContent != null)
{
if (oldContent is FrameworkElement oldElement)
{
oldElement.SizeChanged -= OnSizeChanged;
}
}
if (newContent != null)
{
if (newContent is FrameworkElement newElement)
{
newElement.SizeChanged += OnSizeChanged;
}
}
base.OnContentChanged(oldContent, newContent);
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateShadowSize();
}
private void ConfigureShadowVisualForCastingElement()
{
UpdateShadowMask();
UpdateShadowSize();
}
private void OnBlurRadiusChanged(double newValue)
{
if (_dropShadow != null)
{
_dropShadow.BlurRadius = (float)newValue;
}
}
private void OnColorChanged(Color newValue)
{
if (_dropShadow != null)
{
_dropShadow.Color = newValue;
}
}
private void OnOffsetXChanged(double newValue)
{
if (_dropShadow != null)
{
UpdateShadowOffset((float)newValue, _dropShadow.Offset.Y, _dropShadow.Offset.Z);
}
}
private void OnOffsetYChanged(double newValue)
{
if (_dropShadow != null)
{
UpdateShadowOffset(_dropShadow.Offset.X, (float)newValue, _dropShadow.Offset.Z);
}
}
private void OnOffsetZChanged(double newValue)
{
if (_dropShadow != null)
{
UpdateShadowOffset(_dropShadow.Offset.X, _dropShadow.Offset.Y, (float)newValue);
}
}
private void OnShadowOpacityChanged(double newValue)
{
if (_dropShadow != null)
{
_dropShadow.Opacity = (float)newValue;
}
}
private void UpdateShadowMask()
{
if (Content != null && IsMasked)
{
CompositionBrush mask = null;
if (Content is Image image)
{
mask = image.GetAlphaMask();
}
else if (Content is Shape shape)
{
mask = shape.GetAlphaMask();
}
else if (Content is TextBlock textBlock)
{
mask = textBlock.GetAlphaMask();
}
_dropShadow.Mask = mask;
}
else
{
_dropShadow.Mask = null;
}
}
private void UpdateShadowOffset(float x, float y, float z)
{
if (_dropShadow != null)
{
_dropShadow.Offset = new Vector3(x, y, z);
}
}
private void UpdateShadowSize()
{
if (_shadowVisual != null)
{
Vector2 newSize = new Vector2(0, 0);
if (Content is FrameworkElement content)
{
newSize = new Vector2((float)content.ActualWidth, (float)content.ActualHeight);
}
_shadowVisual.Size = newSize;
}
}
}
}

@ -0,0 +1,30 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Notepads.Controls">
<Style TargetType="controls:DropShadowPanel">
<Setter Property="IsTabStop"
Value="False" />
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:DropShadowPanel">
<Grid Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}">
<Border x:Name="ShadowElement"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

@ -0,0 +1,158 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
namespace Notepads.Controls
{
/// <summary>
/// Represents the control that redistributes space between columns or rows of a Grid control.
/// </summary>
public partial class GridSplitter
{
/// <summary>
/// Enum to indicate whether GridSplitter resizes Columns or Rows
/// </summary>
public enum GridResizeDirection
{
/// <summary>
/// Determines whether to resize rows or columns based on its Alignment and
/// width compared to height
/// </summary>
Auto,
/// <summary>
/// Resize columns when dragging Splitter.
/// </summary>
Columns,
/// <summary>
/// Resize rows when dragging Splitter.
/// </summary>
Rows
}
/// <summary>
/// Enum to indicate what Columns or Rows the GridSplitter resizes
/// </summary>
public enum GridResizeBehavior
{
/// <summary>
/// Determine which columns or rows to resize based on its Alignment.
/// </summary>
BasedOnAlignment,
/// <summary>
/// Resize the current and next Columns or Rows.
/// </summary>
CurrentAndNext,
/// <summary>
/// Resize the previous and current Columns or Rows.
/// </summary>
PreviousAndCurrent,
/// <summary>
/// Resize the previous and next Columns or Rows.
/// </summary>
PreviousAndNext
}
/// <summary>
/// Enum to indicate the supported gripper cursor types.
/// </summary>
public enum GripperCursorType
{
/// <summary>
/// Change the cursor based on the splitter direction
/// </summary>
Default = -1,
/// <summary>
/// Standard Arrow cursor
/// </summary>
Arrow,
/// <summary>
/// Standard Cross cursor
/// </summary>
Cross,
/// <summary>
/// Standard Custom cursor
/// </summary>
Custom,
/// <summary>
/// Standard Hand cursor
/// </summary>
Hand,
/// <summary>
/// Standard Help cursor
/// </summary>
Help,
/// <summary>
/// Standard IBeam cursor
/// </summary>
IBeam,
/// <summary>
/// Standard SizeAll cursor
/// </summary>
SizeAll,
/// <summary>
/// Standard SizeNortheastSouthwest cursor
/// </summary>
SizeNortheastSouthwest,
/// <summary>
/// Standard SizeNorthSouth cursor
/// </summary>
SizeNorthSouth,
/// <summary>
/// Standard SizeNorthwestSoutheast cursor
/// </summary>
SizeNorthwestSoutheast,
/// <summary>
/// Standard SizeWestEast cursor
/// </summary>
SizeWestEast,
/// <summary>
/// Standard UniversalNo cursor
/// </summary>
UniversalNo,
/// <summary>
/// Standard UpArrow cursor
/// </summary>
UpArrow,
/// <summary>
/// Standard Wait cursor
/// </summary>
Wait
}
/// <summary>
/// Enum to indicate the behavior of window cursor on grid splitter hover
/// </summary>
public enum SplitterCursorBehavior
{
/// <summary>
/// Update window cursor on Grid Splitter hover
/// </summary>
ChangeOnSplitterHover,
/// <summary>
/// Update window cursor on Grid Splitter Gripper hover
/// </summary>
ChangeOnGripperHover
}
}
}

@ -0,0 +1,302 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
namespace Notepads.Controls
{
using Windows.System;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
/// <summary>
/// Represents the control that redistributes space between columns or rows of a Grid control.
/// </summary>
public partial class GridSplitter
{
// Symbols for GripperBar in Segoe MDL2 Assets
private const string GripperBarVertical = "\xE784";
private const string GripperBarHorizontal = "\xE76F";
private const string GripperDisplayFont = "Segoe MDL2 Assets";
private void GridSplitter_Loaded(object sender, RoutedEventArgs e)
{
_resizeDirection = GetResizeDirection();
_resizeBehavior = GetResizeBehavior();
// Adding Grip to Grid Splitter
if (Element == default(UIElement))
{
CreateGripperDisplay();
Element = _gripperDisplay;
}
if (_hoverWrapper == null)
{
var hoverWrapper = new GripperHoverWrapper(
CursorBehavior == SplitterCursorBehavior.ChangeOnSplitterHover
? this
: Element,
_resizeDirection,
GripperCursor,
GripperCustomCursorResource);
ManipulationStarted += hoverWrapper.SplitterManipulationStarted;
ManipulationCompleted += hoverWrapper.SplitterManipulationCompleted;
_hoverWrapper = hoverWrapper;
}
}
private void CreateGripperDisplay()
{
if (_gripperDisplay == null)
{
_gripperDisplay = new TextBlock
{
FontFamily = new FontFamily(GripperDisplayFont),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Foreground = GripperForeground,
Text = _resizeDirection == GridResizeDirection.Columns ? GripperBarVertical : GripperBarHorizontal
};
}
}
/// <inheritdoc />
protected override void OnKeyDown(KeyRoutedEventArgs e)
{
var step = 1;
var ctrl = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control);
if (ctrl.HasFlag(CoreVirtualKeyStates.Down))
{
step = 5;
}
if (_resizeDirection == GridResizeDirection.Columns)
{
if (e.Key == VirtualKey.Left)
{
HorizontalMove(-step);
}
else if (e.Key == VirtualKey.Right)
{
HorizontalMove(step);
}
else
{
return;
}
e.Handled = true;
return;
}
if (_resizeDirection == GridResizeDirection.Rows)
{
if (e.Key == VirtualKey.Up)
{
VerticalMove(-step);
}
else if (e.Key == VirtualKey.Down)
{
VerticalMove(step);
}
else
{
return;
}
e.Handled = true;
}
base.OnKeyDown(e);
}
/// <inheritdoc />
protected override void OnManipulationStarted(ManipulationStartedRoutedEventArgs e)
{
// saving the previous state
PreviousCursor = Window.Current.CoreWindow.PointerCursor;
_resizeDirection = GetResizeDirection();
_resizeBehavior = GetResizeBehavior();
if (_resizeDirection == GridResizeDirection.Columns)
{
Window.Current.CoreWindow.PointerCursor = ColumnsSplitterCursor;
}
else if (_resizeDirection == GridResizeDirection.Rows)
{
Window.Current.CoreWindow.PointerCursor = RowSplitterCursor;
}
base.OnManipulationStarted(e);
}
/// <inheritdoc />
protected override void OnManipulationCompleted(ManipulationCompletedRoutedEventArgs e)
{
Window.Current.CoreWindow.PointerCursor = PreviousCursor;
base.OnManipulationCompleted(e);
}
/// <inheritdoc />
protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
{
var horizontalChange = e.Delta.Translation.X;
var verticalChange = e.Delta.Translation.Y;
if (_resizeDirection == GridResizeDirection.Columns)
{
if (HorizontalMove(horizontalChange))
{
return;
}
}
else if (_resizeDirection == GridResizeDirection.Rows)
{
if (VerticalMove(verticalChange))
{
return;
}
}
base.OnManipulationDelta(e);
}
private bool VerticalMove(double verticalChange)
{
if (CurrentRow == null || SiblingRow == null)
{
return true;
}
// if current row has fixed height then resize it
if (!IsStarRow(CurrentRow))
{
// No need to check for the row Min height because it is automatically respected
if (!SetRowHeight(CurrentRow, verticalChange, GridUnitType.Pixel))
{
return true;
}
}
// if sibling row has fixed width then resize it
else if (!IsStarRow(SiblingRow))
{
// Would adding to this column make the current column violate the MinWidth?
if (IsValidRowHeight(CurrentRow, verticalChange) == false)
{
return false;
}
if (!SetRowHeight(SiblingRow, verticalChange * -1, GridUnitType.Pixel))
{
return true;
}
}
// if both row haven't fixed height (auto *)
else
{
// change current row height to the new height with respecting the auto
// change sibling row height to the new height relative to current row
// respect the other star row height by setting it's height to it's actual height with stars
// We need to validate current and sibling height to not cause any un expected behavior
if (!IsValidRowHeight(CurrentRow, verticalChange) ||
!IsValidRowHeight(SiblingRow, verticalChange * -1))
{
return true;
}
foreach (var rowDefinition in Resizable.RowDefinitions)
{
if (rowDefinition == CurrentRow)
{
SetRowHeight(CurrentRow, verticalChange, GridUnitType.Star);
}
else if (rowDefinition == SiblingRow)
{
SetRowHeight(SiblingRow, verticalChange * -1, GridUnitType.Star);
}
else if (IsStarRow(rowDefinition))
{
rowDefinition.Height = new GridLength(rowDefinition.ActualHeight, GridUnitType.Star);
}
}
}
return false;
}
private bool HorizontalMove(double horizontalChange)
{
if (CurrentColumn == null || SiblingColumn == null)
{
return true;
}
// if current column has fixed width then resize it
if (!IsStarColumn(CurrentColumn))
{
// No need to check for the Column Min width because it is automatically respected
if (!SetColumnWidth(CurrentColumn, horizontalChange, GridUnitType.Pixel))
{
return true;
}
}
// if sibling column has fixed width then resize it
else if (!IsStarColumn(SiblingColumn))
{
// Would adding to this column make the current column violate the MinWidth?
if (IsValidColumnWidth(CurrentColumn, horizontalChange) == false)
{
return false;
}
if (!SetColumnWidth(SiblingColumn, horizontalChange * -1, GridUnitType.Pixel))
{
return true;
}
}
// if both column haven't fixed width (auto *)
else
{
// change current column width to the new width with respecting the auto
// change sibling column width to the new width relative to current column
// respect the other star column width by setting it's width to it's actual width with stars
// We need to validate current and sibling width to not cause any un expected behavior
if (!IsValidColumnWidth(CurrentColumn, horizontalChange) ||
!IsValidColumnWidth(SiblingColumn, horizontalChange * -1))
{
return true;
}
foreach (var columnDefinition in Resizable.ColumnDefinitions)
{
if (columnDefinition == CurrentColumn)
{
SetColumnWidth(CurrentColumn, horizontalChange, GridUnitType.Star);
}
else if (columnDefinition == SiblingColumn)
{
SetColumnWidth(SiblingColumn, horizontalChange * -1, GridUnitType.Star);
}
else if (IsStarColumn(columnDefinition))
{
columnDefinition.Width = new GridLength(columnDefinition.ActualWidth, GridUnitType.Star);
}
}
}
return false;
}
}
}

@ -0,0 +1,261 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
namespace Notepads.Controls
{
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
/// <summary>
/// Represents the control that redistributes space between columns or rows of a Grid control.
/// </summary>
public partial class GridSplitter
{
private static bool IsStarColumn(ColumnDefinition definition)
{
return ((GridLength)definition.GetValue(ColumnDefinition.WidthProperty)).IsStar;
}
private static bool IsStarRow(RowDefinition definition)
{
return ((GridLength)definition.GetValue(RowDefinition.HeightProperty)).IsStar;
}
private bool SetColumnWidth(ColumnDefinition columnDefinition, double horizontalChange, GridUnitType unitType)
{
var newWidth = columnDefinition.ActualWidth + horizontalChange;
var minWidth = columnDefinition.MinWidth;
if (!double.IsNaN(minWidth) && newWidth < minWidth)
{
newWidth = minWidth;
}
var maxWidth = columnDefinition.MaxWidth;
if (!double.IsNaN(maxWidth) && newWidth > maxWidth)
{
newWidth = maxWidth;
}
if (newWidth > ActualWidth)
{
columnDefinition.Width = new GridLength(newWidth, unitType);
return true;
}
return false;
}
private bool IsValidColumnWidth(ColumnDefinition columnDefinition, double horizontalChange)
{
var newWidth = columnDefinition.ActualWidth + horizontalChange;
var minWidth = columnDefinition.MinWidth;
if (!double.IsNaN(minWidth) && newWidth < minWidth)
{
return false;
}
var maxWidth = columnDefinition.MaxWidth;
if (!double.IsNaN(maxWidth) && newWidth > maxWidth)
{
return false;
}
if (newWidth <= ActualWidth)
{
return false;
}
return true;
}
private bool SetRowHeight(RowDefinition rowDefinition, double verticalChange, GridUnitType unitType)
{
var newHeight = rowDefinition.ActualHeight + verticalChange;
var minHeight = rowDefinition.MinHeight;
if (!double.IsNaN(minHeight) && newHeight < minHeight)
{
newHeight = minHeight;
}
var maxWidth = rowDefinition.MaxHeight;
if (!double.IsNaN(maxWidth) && newHeight > maxWidth)
{
newHeight = maxWidth;
}
if (newHeight > ActualHeight)
{
rowDefinition.Height = new GridLength(newHeight, unitType);
return true;
}
return false;
}
private bool IsValidRowHeight(RowDefinition rowDefinition, double verticalChange)
{
var newHeight = rowDefinition.ActualHeight + verticalChange;
var minHeight = rowDefinition.MinHeight;
if (!double.IsNaN(minHeight) && newHeight < minHeight)
{
return false;
}
var maxHeight = rowDefinition.MaxHeight;
if (!double.IsNaN(maxHeight) && newHeight > maxHeight)
{
return false;
}
if (newHeight <= ActualHeight)
{
return false;
}
return true;
}
// Return the targeted Column based on the resize behavior
private int GetTargetedColumn()
{
var currentIndex = Grid.GetColumn(TargetControl);
return GetTargetIndex(currentIndex);
}
// Return the sibling Row based on the resize behavior
private int GetTargetedRow()
{
var currentIndex = Grid.GetRow(TargetControl);
return GetTargetIndex(currentIndex);
}
// Return the sibling Column based on the resize behavior
private int GetSiblingColumn()
{
var currentIndex = Grid.GetColumn(TargetControl);
return GetSiblingIndex(currentIndex);
}
// Return the sibling Row based on the resize behavior
private int GetSiblingRow()
{
var currentIndex = Grid.GetRow(TargetControl);
return GetSiblingIndex(currentIndex);
}
// Gets index based on resize behavior for first targeted row/column
private int GetTargetIndex(int currentIndex)
{
switch (_resizeBehavior)
{
case GridResizeBehavior.CurrentAndNext:
return currentIndex;
case GridResizeBehavior.PreviousAndNext:
return currentIndex - 1;
case GridResizeBehavior.PreviousAndCurrent:
return currentIndex - 1;
default:
return -1;
}
}
// Gets index based on resize behavior for second targeted row/column
private int GetSiblingIndex(int currentIndex)
{
switch (_resizeBehavior)
{
case GridResizeBehavior.CurrentAndNext:
return currentIndex + 1;
case GridResizeBehavior.PreviousAndNext:
return currentIndex + 1;
case GridResizeBehavior.PreviousAndCurrent:
return currentIndex;
default:
return -1;
}
}
// Checks the control alignment and Width/Height to detect the control resize direction columns/rows
private GridResizeDirection GetResizeDirection()
{
GridResizeDirection direction = ResizeDirection;
if (direction == GridResizeDirection.Auto)
{
// When HorizontalAlignment is Left, Right or Center, resize Columns
if (HorizontalAlignment != HorizontalAlignment.Stretch)
{
direction = GridResizeDirection.Columns;
}
// When VerticalAlignment is Top, Bottom or Center, resize Rows
else if (VerticalAlignment != VerticalAlignment.Stretch)
{
direction = GridResizeDirection.Rows;
}
// Check Width vs Height
else if (ActualWidth <= ActualHeight)
{
direction = GridResizeDirection.Columns;
}
else
{
direction = GridResizeDirection.Rows;
}
}
return direction;
}
// Get the resize behavior (Which columns/rows should be resized) based on alignment and Direction
private GridResizeBehavior GetResizeBehavior()
{
GridResizeBehavior resizeBehavior = ResizeBehavior;
if (resizeBehavior == GridResizeBehavior.BasedOnAlignment)
{
if (_resizeDirection == GridResizeDirection.Columns)
{
switch (HorizontalAlignment)
{
case HorizontalAlignment.Left:
resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
break;
case HorizontalAlignment.Right:
resizeBehavior = GridResizeBehavior.CurrentAndNext;
break;
default:
resizeBehavior = GridResizeBehavior.PreviousAndNext;
break;
}
}
// resize direction is vertical
else
{
switch (VerticalAlignment)
{
case VerticalAlignment.Top:
resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
break;
case VerticalAlignment.Bottom:
resizeBehavior = GridResizeBehavior.CurrentAndNext;
break;
default:
resizeBehavior = GridResizeBehavior.PreviousAndNext;
break;
}
}
}
return resizeBehavior;
}
}
}

@ -0,0 +1,225 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
namespace Notepads.Controls
{
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
/// <summary>
/// Represents the control that redistributes space between columns or rows of a Grid control.
/// </summary>
public partial class GridSplitter
{
/// <summary>
/// Identifies the <see cref="Element"/> dependency property.
/// </summary>
public static readonly DependencyProperty ElementProperty
= DependencyProperty.Register(
nameof(Element),
typeof(UIElement),
typeof(GridSplitter),
new PropertyMetadata(default(UIElement), OnElementPropertyChanged));
/// <summary>
/// Identifies the <see cref="ResizeDirection"/> dependency property.
/// </summary>
public static readonly DependencyProperty ResizeDirectionProperty
= DependencyProperty.Register(
nameof(ResizeDirection),
typeof(GridResizeDirection),
typeof(GridSplitter),
new PropertyMetadata(GridResizeDirection.Auto));
/// <summary>
/// Identifies the <see cref="ResizeBehavior"/> dependency property.
/// </summary>
public static readonly DependencyProperty ResizeBehaviorProperty
= DependencyProperty.Register(
nameof(ResizeBehavior),
typeof(GridResizeBehavior),
typeof(GridSplitter),
new PropertyMetadata(GridResizeBehavior.BasedOnAlignment));
/// <summary>
/// Identifies the <see cref="GripperForeground"/> dependency property.
/// </summary>
public static readonly DependencyProperty GripperForegroundProperty
= DependencyProperty.Register(
nameof(GripperForeground),
typeof(Brush),
typeof(GridSplitter),
new PropertyMetadata(default(Brush), OnGripperForegroundPropertyChanged));
/// <summary>
/// Identifies the <see cref="ParentLevel"/> dependency property.
/// </summary>
public static readonly DependencyProperty ParentLevelProperty
= DependencyProperty.Register(
nameof(ParentLevel),
typeof(int),
typeof(GridSplitter),
new PropertyMetadata(default(int)));
/// <summary>
/// Identifies the <see cref="GripperCursor"/> dependency property.
/// </summary>
public static readonly DependencyProperty GripperCursorProperty =
DependencyProperty.RegisterAttached(
nameof(GripperCursor),
typeof(CoreCursorType?),
typeof(GridSplitter),
new PropertyMetadata(GripperCursorType.Default, OnGripperCursorPropertyChanged));
/// <summary>
/// Identifies the <see cref="GripperCustomCursorResource"/> dependency property.
/// </summary>
public static readonly DependencyProperty GripperCustomCursorResourceProperty =
DependencyProperty.RegisterAttached(
nameof(GripperCustomCursorResource),
typeof(uint),
typeof(GridSplitter),
new PropertyMetadata(GripperCustomCursorDefaultResource, GripperCustomCursorResourcePropertyChanged));
/// <summary>
/// Identifies the <see cref="CursorBehavior"/> dependency property.
/// </summary>
public static readonly DependencyProperty CursorBehaviorProperty =
DependencyProperty.RegisterAttached(
nameof(CursorBehavior),
typeof(SplitterCursorBehavior),
typeof(GridSplitter),
new PropertyMetadata(SplitterCursorBehavior.ChangeOnSplitterHover, CursorBehaviorPropertyChanged));
/// <summary>
/// Gets or sets the visual content of this Grid Splitter
/// </summary>
public UIElement Element
{
get => (UIElement)GetValue(ElementProperty);
set => SetValue(ElementProperty, value);
}
/// <summary>
/// Gets or sets whether the Splitter resizes the Columns, Rows, or Both.
/// </summary>
public GridResizeDirection ResizeDirection
{
get => (GridResizeDirection)GetValue(ResizeDirectionProperty);
set => SetValue(ResizeDirectionProperty, value);
}
/// <summary>
/// Gets or sets which Columns or Rows the Splitter resizes.
/// </summary>
public GridResizeBehavior ResizeBehavior
{
get => (GridResizeBehavior)GetValue(ResizeBehaviorProperty);
set => SetValue(ResizeBehaviorProperty, value);
}
/// <summary>
/// Gets or sets the foreground color of grid splitter grip
/// </summary>
public Brush GripperForeground
{
get => (Brush)GetValue(GripperForegroundProperty);
set => SetValue(GripperForegroundProperty, value);
}
/// <summary>
/// Gets or sets the level of the parent grid to resize
/// </summary>
public int ParentLevel
{
get => (int)GetValue(ParentLevelProperty);
set => SetValue(ParentLevelProperty, value);
}
/// <summary>
/// Gets or sets the gripper Cursor type
/// </summary>
public GripperCursorType GripperCursor
{
get => (GripperCursorType)GetValue(GripperCursorProperty);
set => SetValue(GripperCursorProperty, value);
}
/// <summary>
/// Gets or sets the gripper Custom Cursor resource number
/// </summary>
public int GripperCustomCursorResource
{
get => (int)GetValue(GripperCustomCursorResourceProperty);
set => SetValue(GripperCustomCursorResourceProperty, value);
}
/// <summary>
/// Gets or sets splitter cursor on hover behavior
/// </summary>
public SplitterCursorBehavior CursorBehavior
{
get => (SplitterCursorBehavior)GetValue(CursorBehaviorProperty);
set => SetValue(CursorBehaviorProperty, value);
}
private static void OnGripperForegroundPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gridSplitter = (GridSplitter)d;
if (gridSplitter._gripperDisplay == null)
{
return;
}
gridSplitter._gripperDisplay.Foreground = gridSplitter.GripperForeground;
}
private static void OnGripperCursorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gridSplitter = (GridSplitter)d;
if (gridSplitter._hoverWrapper == null)
{
return;
}
gridSplitter._hoverWrapper.GripperCursor = gridSplitter.GripperCursor;
}
private static void GripperCustomCursorResourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gridSplitter = (GridSplitter)d;
if (gridSplitter._hoverWrapper == null)
{
return;
}
gridSplitter._hoverWrapper.GripperCustomCursorResource = gridSplitter.GripperCustomCursorResource;
}
private static void CursorBehaviorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gridSplitter = (GridSplitter)d;
gridSplitter._hoverWrapper?.UpdateHoverElement(gridSplitter.CursorBehavior ==
SplitterCursorBehavior.ChangeOnSplitterHover
? gridSplitter
: gridSplitter.Element);
}
private static void OnElementPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gridSplitter = (GridSplitter)d;
gridSplitter._hoverWrapper?.UpdateHoverElement(gridSplitter.CursorBehavior ==
SplitterCursorBehavior.ChangeOnSplitterHover
? gridSplitter
: gridSplitter.Element);
}
}
}

@ -0,0 +1,246 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
namespace Notepads.Controls
{
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Automation;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
/// <summary>
/// Represents the control that redistributes space between columns or rows of a Grid control.
/// </summary>
public partial class GridSplitter : Control
{
internal const int GripperCustomCursorDefaultResource = -1;
internal static readonly CoreCursor ColumnsSplitterCursor = new CoreCursor(CoreCursorType.SizeWestEast, 1);
internal static readonly CoreCursor RowSplitterCursor = new CoreCursor(CoreCursorType.SizeNorthSouth, 1);
internal CoreCursor PreviousCursor { get; set; }
private GridResizeDirection _resizeDirection;
private GridResizeBehavior _resizeBehavior;
private GripperHoverWrapper _hoverWrapper;
private TextBlock _gripperDisplay;
private bool _pressed = false;
private bool _dragging = false;
private bool _pointerEntered = false;
/// <summary>
/// Gets the target parent grid from level
/// </summary>
private FrameworkElement TargetControl
{
get
{
if (ParentLevel == 0)
{
return this;
}
var parent = Parent;
for (int i = 2; i < ParentLevel; i++)
{
if (parent is FrameworkElement frameworkElement)
{
parent = frameworkElement.Parent;
}
}
return parent as FrameworkElement;
}
}
/// <summary>
/// Gets GridSplitter Container Grid
/// </summary>
private Grid Resizable => TargetControl?.Parent as Grid;
/// <summary>
/// Gets the current Column definition of the parent Grid
/// </summary>
private ColumnDefinition CurrentColumn
{
get
{
if (Resizable == null)
{
return null;
}
var gridSplitterTargetedColumnIndex = GetTargetedColumn();
if ((gridSplitterTargetedColumnIndex >= 0)
&& (gridSplitterTargetedColumnIndex < Resizable.ColumnDefinitions.Count))
{
return Resizable.ColumnDefinitions[gridSplitterTargetedColumnIndex];
}
return null;
}
}
/// <summary>
/// Gets the Sibling Column definition of the parent Grid
/// </summary>
private ColumnDefinition SiblingColumn
{
get
{
if (Resizable == null)
{
return null;
}
var gridSplitterSiblingColumnIndex = GetSiblingColumn();
if ((gridSplitterSiblingColumnIndex >= 0)
&& (gridSplitterSiblingColumnIndex < Resizable.ColumnDefinitions.Count))
{
return Resizable.ColumnDefinitions[gridSplitterSiblingColumnIndex];
}
return null;
}
}
/// <summary>
/// Gets the current Row definition of the parent Grid
/// </summary>
private RowDefinition CurrentRow
{
get
{
if (Resizable == null)
{
return null;
}
var gridSplitterTargetedRowIndex = GetTargetedRow();
if ((gridSplitterTargetedRowIndex >= 0)
&& (gridSplitterTargetedRowIndex < Resizable.RowDefinitions.Count))
{
return Resizable.RowDefinitions[gridSplitterTargetedRowIndex];
}
return null;
}
}
/// <summary>
/// Gets the Sibling Row definition of the parent Grid
/// </summary>
private RowDefinition SiblingRow
{
get
{
if (Resizable == null)
{
return null;
}
var gridSplitterSiblingRowIndex = GetSiblingRow();
if ((gridSplitterSiblingRowIndex >= 0)
&& (gridSplitterSiblingRowIndex < Resizable.RowDefinitions.Count))
{
return Resizable.RowDefinitions[gridSplitterSiblingRowIndex];
}
return null;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="GridSplitter"/> class.
/// </summary>
public GridSplitter()
{
DefaultStyleKey = typeof(GridSplitter);
Loaded += GridSplitter_Loaded;
string automationName = "GridSpliter";
AutomationProperties.SetName(this, automationName);
}
/// <inheritdoc />
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Unhook registered events
Loaded -= GridSplitter_Loaded;
PointerEntered -= GridSplitter_PointerEntered;
PointerExited -= GridSplitter_PointerExited;
PointerPressed -= GridSplitter_PointerPressed;
PointerReleased -= GridSplitter_PointerReleased;
ManipulationStarted -= GridSplitter_ManipulationStarted;
ManipulationCompleted -= GridSplitter_ManipulationCompleted;
_hoverWrapper?.UnhookEvents();
// Register Events
Loaded += GridSplitter_Loaded;
PointerEntered += GridSplitter_PointerEntered;
PointerExited += GridSplitter_PointerExited;
PointerPressed += GridSplitter_PointerPressed;
PointerReleased += GridSplitter_PointerReleased;
ManipulationStarted += GridSplitter_ManipulationStarted;
ManipulationCompleted += GridSplitter_ManipulationCompleted;
_hoverWrapper?.UpdateHoverElement(Element);
ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY;
}
private void GridSplitter_PointerReleased(object sender, PointerRoutedEventArgs e)
{
_pressed = false;
VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true);
}
private void GridSplitter_PointerPressed(object sender, PointerRoutedEventArgs e)
{
_pressed = true;
VisualStateManager.GoToState(this, "Pressed", true);
}
private void GridSplitter_PointerExited(object sender, PointerRoutedEventArgs e)
{
_pointerEntered = false;
if (!_pressed && !_dragging)
{
VisualStateManager.GoToState(this, "Normal", true);
}
}
private void GridSplitter_PointerEntered(object sender, PointerRoutedEventArgs e)
{
_pointerEntered = true;
if (!_pressed && !_dragging)
{
VisualStateManager.GoToState(this, "PointerOver", true);
}
}
private void GridSplitter_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
_dragging = false;
_pressed = false;
VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true);
}
private void GridSplitter_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
{
_dragging = true;
VisualStateManager.GoToState(this, "Pressed", true);
}
}
}

@ -0,0 +1,52 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Notepads.Controls">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="SystemControlSplitterPointerOver" Color="{ThemeResource SystemBaseLowColor}" />
<SolidColorBrush x:Key="SystemControlSplitterPressed" Color="{ThemeResource SystemBaseHighColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="SystemControlSplitterPointerOver" Color="{ThemeResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="SystemControlSplitterPressed" Color="{ThemeResource SystemColorHighlightColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Style TargetType="local:GridSplitter">
<Setter Property="IsTabStop" Value="True"></Setter>
<Setter Property="UseSystemFocusVisuals" Value="True"></Setter>
<Setter Property="HorizontalAlignment" Value="Stretch"></Setter>
<Setter Property="VerticalAlignment" Value="Stretch"></Setter>
<Setter Property="IsFocusEngagementEnabled" Value="True"></Setter>
<Setter Property="MinWidth" Value="16"></Setter>
<Setter Property="MinHeight" Value="16"></Setter>
<Setter Property="Background" Value="{ThemeResource SystemControlHighlightChromeHighBrush}"></Setter>
<Setter Property="GripperForeground" Value="{ThemeResource SystemControlForegroundAltHighBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:GridSplitter">
<Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="GridSplitterStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource SystemControlSplitterPointerOver}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource SystemControlSplitterPressed}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter Content="{TemplateBinding Element}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

@ -0,0 +1,154 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/GridSplitter
namespace Notepads.Controls
{
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Input;
internal class GripperHoverWrapper
{
private readonly GridSplitter.GridResizeDirection _gridSplitterDirection;
private CoreCursor _splitterPreviousPointer;
private CoreCursor _previousCursor;
private GridSplitter.GripperCursorType _gripperCursor;
private int _gripperCustomCursorResource;
private bool _isDragging;
private UIElement _element;
internal GridSplitter.GripperCursorType GripperCursor
{
get => _gripperCursor;
set => _gripperCursor = value;
}
internal int GripperCustomCursorResource
{
get => _gripperCustomCursorResource;
set => _gripperCustomCursorResource = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="GripperHoverWrapper"/> class that add cursor change on hover functionality for GridSplitter.
/// </summary>
/// <param name="element">UI element to apply cursor change on hover</param>
/// <param name="gridSplitterDirection">GridSplitter resize direction</param>
/// <param name="gripperCursor">GridSplitter gripper on hover cursor type</param>
/// <param name="gripperCustomCursorResource">GridSplitter gripper custom cursor resource number</param>
internal GripperHoverWrapper(UIElement element, GridSplitter.GridResizeDirection gridSplitterDirection, GridSplitter.GripperCursorType gripperCursor, int gripperCustomCursorResource)
{
_gridSplitterDirection = gridSplitterDirection;
_gripperCursor = gripperCursor;
_gripperCustomCursorResource = gripperCustomCursorResource;
_element = element;
UnhookEvents();
_element.PointerEntered += Element_PointerEntered;
_element.PointerExited += Element_PointerExited;
}
internal void UpdateHoverElement(UIElement element)
{
UnhookEvents();
_element = element;
_element.PointerEntered += Element_PointerEntered;
_element.PointerExited += Element_PointerExited;
}
private void Element_PointerExited(object sender, PointerRoutedEventArgs e)
{
if (_isDragging)
{
// if dragging don't update the curser just update the splitter cursor with the last window cursor,
// because the splitter is still using the arrow cursor and will revert to original case when drag completes
_splitterPreviousPointer = _previousCursor;
}
else
{
Window.Current.CoreWindow.PointerCursor = _previousCursor;
}
}
private void Element_PointerEntered(object sender, PointerRoutedEventArgs e)
{
// if not dragging
if (!_isDragging)
{
_previousCursor = _splitterPreviousPointer = Window.Current.CoreWindow.PointerCursor;
UpdateDisplayCursor();
}
// if dragging
else
{
_previousCursor = _splitterPreviousPointer;
}
}
private void UpdateDisplayCursor()
{
if (_gripperCursor == GridSplitter.GripperCursorType.Default)
{
if (_gridSplitterDirection == GridSplitter.GridResizeDirection.Columns)
{
Window.Current.CoreWindow.PointerCursor = GridSplitter.ColumnsSplitterCursor;
}
else if (_gridSplitterDirection == GridSplitter.GridResizeDirection.Rows)
{
Window.Current.CoreWindow.PointerCursor = GridSplitter.RowSplitterCursor;
}
}
else
{
var coreCursor = (CoreCursorType)((int)_gripperCursor);
if (_gripperCursor == GridSplitter.GripperCursorType.Custom)
{
if (_gripperCustomCursorResource > GridSplitter.GripperCustomCursorDefaultResource)
{
Window.Current.CoreWindow.PointerCursor = new CoreCursor(coreCursor, (uint)_gripperCustomCursorResource);
}
}
else
{
Window.Current.CoreWindow.PointerCursor = new CoreCursor(coreCursor, 1);
}
}
}
internal void SplitterManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
{
if (!(sender is GridSplitter splitter))
{
return;
}
_splitterPreviousPointer = splitter.PreviousCursor;
_isDragging = true;
}
internal void SplitterManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
if (!(sender is GridSplitter splitter))
{
return;
}
Window.Current.CoreWindow.PointerCursor = splitter.PreviousCursor = _splitterPreviousPointer;
_isDragging = false;
}
internal void UnhookEvents()
{
if (_element == null)
{
return;
}
_element.PointerEntered -= Element_PointerEntered;
_element.PointerExited -= Element_PointerExited;
}
}
}

@ -0,0 +1,250 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/8464f8e5263686c1484732bdea86ebba3f30a075/Microsoft.Toolkit.Uwp/Helpers/DispatcherQueueHelper.cs
namespace Notepads.Controls.Helpers
{
using System;
using System.Threading.Tasks;
using Windows.System;
/// <summary>
/// This class provides static methods helper for executing code in a DispatcherQueue.
/// </summary>
public static class DispatcherQueueHelper
{
/// <summary>
/// Extension method for <see cref="DispatcherQueue"/>. Offering an actual awaitable <see cref="Task"/> with optional result that will be executed on the given dispatcher.
/// </summary>
/// <param name="dispatcher">DispatcherQueue of a thread to run <paramref name="function"/>.</param>
/// <param name="function"> Function to be executed on the given dispatcher.</param>
/// <param name="priority">DispatcherQueue execution priority, default is normal.</param>
/// <returns>An awaitable <see cref="Task"/> for the operation.</returns>
/// <remarks>If the current thread has UI access, <paramref name="function"/> will be invoked directly.</remarks>
public static Task ExecuteOnUIThreadAsync(this DispatcherQueue dispatcher, Action function, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal)
{
if (function is null)
{
throw new ArgumentNullException(nameof(function));
}
/* Run the function directly when we have thread access.
* Also reuse Task.CompletedTask in case of success,
* to skip an unnecessary heap allocation for every invocation. */
// Ignoring for now, but need to map the CurrentThreadID for all dispatcher queue code we have
/*
if (dispatcher.HasThreadAccess)
{
try
{
function();
return Task.CompletedTask;
}
catch (Exception e)
{
return Task.FromException(e);
}
}
*/
var taskCompletionSource = new TaskCompletionSource<object>();
_ = dispatcher?.TryEnqueue(priority, () =>
{
try
{
function();
taskCompletionSource.SetResult(null);
}
catch (Exception e)
{
taskCompletionSource.SetException(e);
}
});
return taskCompletionSource.Task;
}
/// <summary>
/// Extension method for <see cref="DispatcherQueue"/>. Offering an actual awaitable <see cref="Task{T}"/> with optional result that will be executed on the given dispatcher.
/// </summary>
/// <typeparam name="T">Returned data type of the function.</typeparam>
/// <param name="dispatcher">DispatcherQueue of a thread to run <paramref name="function"/>.</param>
/// <param name="function"> Function to be executed on the given dispatcher.</param>
/// <param name="priority">DispatcherQueue execution priority, default is normal.</param>
/// <returns>An awaitable <see cref="Task{T}"/> for the operation.</returns>
/// <remarks>If the current thread has UI access, <paramref name="function"/> will be invoked directly.</remarks>
public static Task<T> ExecuteOnUIThreadAsync<T>(this DispatcherQueue dispatcher, Func<T> function, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal)
{
if (function is null)
{
throw new ArgumentNullException(nameof(function));
}
// Skip the dispatch, if possible
// Ignoring for now, but need to map the CurrentThreadID for all dispatcher queue code we have
/*
if (dispatcher.HasThreadAccess)
{
try
{
return Task.FromResult(function());
}
catch (Exception e)
{
return Task.FromException<T>(e);
}
}
*/
var taskCompletionSource = new TaskCompletionSource<T>();
_ = dispatcher?.TryEnqueue(priority, () =>
{
try
{
taskCompletionSource.SetResult(function());
}
catch (Exception e)
{
taskCompletionSource.SetException(e);
}
});
return taskCompletionSource.Task;
}
/// <summary>
/// Extension method for <see cref="DispatcherQueue"/>. Offering an actual awaitable <see cref="Task"/> with optional result that will be executed on the given dispatcher.
/// </summary>
/// <param name="dispatcher">DispatcherQueue of a thread to run <paramref name="function"/>.</param>
/// <param name="function">Asynchronous function to be executed on the given dispatcher.</param>
/// <param name="priority">DispatcherQueue execution priority, default is normal.</param>
/// <returns>An awaitable <see cref="Task"/> for the operation.</returns>
/// <remarks>If the current thread has UI access, <paramref name="function"/> will be invoked directly.</remarks>
public static Task ExecuteOnUIThreadAsync(this DispatcherQueue dispatcher, Func<Task> function, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal)
{
if (function is null)
{
throw new ArgumentNullException(nameof(function));
}
/* If we have thread access, we can retrieve the task directly.
* We don't use ConfigureAwait(false) in this case, in order
* to let the caller continue its execution on the same thread
* after awaiting the task returned by this function. */
// Ignoring for now, but need to map the CurrentThreadID for all dispatcher queue code we have
/*
if (dispatcher.HasThreadAccess)
{
try
{
if (function() is Task awaitableResult)
{
return awaitableResult;
}
return Task.FromException(new InvalidOperationException("The Task returned by function cannot be null."));
}
catch (Exception e)
{
return Task.FromException(e);
}
}
*/
var taskCompletionSource = new TaskCompletionSource<object>();
_ = dispatcher?.TryEnqueue(priority, async () =>
{
try
{
if (function() is Task awaitableResult)
{
await awaitableResult.ConfigureAwait(false);
taskCompletionSource.SetResult(null);
}
else
{
taskCompletionSource.SetException(new InvalidOperationException("The Task returned by function cannot be null."));
}
}
catch (Exception e)
{
taskCompletionSource.SetException(e);
}
});
return taskCompletionSource.Task;
}
/// <summary>
/// Extension method for <see cref="DispatcherQueue"/>. Offering an actual awaitable <see cref="Task{T}"/> with optional result that will be executed on the given dispatcher.
/// </summary>
/// <typeparam name="T">Returned data type of the function.</typeparam>
/// <param name="dispatcher">DispatcherQueue of a thread to run <paramref name="function"/>.</param>
/// <param name="function">Asynchronous function to be executed asynchronously on the given dispatcher.</param>
/// <param name="priority">DispatcherQueue execution priority, default is normal.</param>
/// <returns>An awaitable <see cref="Task{T}"/> for the operation.</returns>
/// <remarks>If the current thread has UI access, <paramref name="function"/> will be invoked directly.</remarks>
public static Task<T> ExecuteOnUIThreadAsync<T>(this DispatcherQueue dispatcher, Func<Task<T>> function, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal)
{
if (function is null)
{
throw new ArgumentNullException(nameof(function));
}
// Skip the dispatch, if possible
// Ignoring for now, but need to map the CurrentThreadID for all dispatcher queue code we have
/*
if (dispatcher.HasThreadAccess)
{
try
{
if (function() is Task<T> awaitableResult)
{
return awaitableResult;
}
return Task.FromException<T>(new InvalidOperationException("The Task returned by function cannot be null."));
}
catch (Exception e)
{
return Task.FromException<T>(e);
}
}
*/
var taskCompletionSource = new TaskCompletionSource<T>();
_ = dispatcher?.TryEnqueue(priority, async () =>
{
try
{
if (function() is Task<T> awaitableResult)
{
var result = await awaitableResult.ConfigureAwait(false);
taskCompletionSource.SetResult(result);
}
else
{
taskCompletionSource.SetException(new InvalidOperationException("The Task returned by function cannot be null."));
}
}
catch (Exception e)
{
taskCompletionSource.SetException(e);
}
});
return taskCompletionSource.Task;
}
}
}

@ -0,0 +1,144 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/8464f8e5263686c1484732bdea86ebba3f30a075/Microsoft.Toolkit.Uwp.UI/Helpers/ThemeListener.cs
namespace Notepads.Controls.Helpers
{
using System;
using System.Threading.Tasks;
using Windows.Foundation.Metadata;
using Windows.System;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
/// <summary>
/// The Delegate for a ThemeChanged Event.
/// </summary>
/// <param name="sender">Sender ThemeListener</param>
public delegate void ThemeChangedEvent(ThemeListener sender);
/// <summary>
/// Class which listens for changes to Application Theme or High Contrast Modes
/// and Signals an Event when they occur.
/// </summary>
[AllowForWeb]
public sealed class ThemeListener : IDisposable
{
/// <summary>
/// Gets the Name of the Current Theme.
/// </summary>
public string CurrentThemeName => CurrentTheme.ToString();
/// <summary>
/// Gets or sets the Current Theme.
/// </summary>
public ApplicationTheme CurrentTheme { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the current theme is high contrast.
/// </summary>
public bool IsHighContrast { get; set; }
/// <summary>
/// Gets or sets which DispatcherQueue is used to dispatch UI updates.
/// </summary>
public DispatcherQueue DispatcherQueue { get; set; }
/// <summary>
/// An event that fires if the Theme changes.
/// </summary>
public event ThemeChangedEvent ThemeChanged;
private readonly AccessibilitySettings _accessible = new AccessibilitySettings();
private readonly UISettings _settings = new UISettings();
/// <summary>
/// Initializes a new instance of the <see cref="ThemeListener"/> class.
/// </summary>
/// <param name="dispatcherQueue">The DispatcherQueue that should be used to dispatch UI updates, or null if this is being called from the UI thread.</param>
public ThemeListener(DispatcherQueue dispatcherQueue = null)
{
CurrentTheme = Application.Current.RequestedTheme;
IsHighContrast = _accessible.HighContrast;
DispatcherQueue = dispatcherQueue ?? DispatcherQueue.GetForCurrentThread();
_accessible.HighContrastChanged += Accessible_HighContrastChanged;
_settings.ColorValuesChanged += Settings_ColorValuesChanged;
// Fallback in case either of the above fail, we'll check when we get activated next.
if (Window.Current != null)
{
Window.Current.CoreWindow.Activated += CoreWindow_Activated;
}
}
private async void Accessible_HighContrastChanged(AccessibilitySettings sender, object args)
{
await DispatcherQueue.ExecuteOnUIThreadAsync(UpdateProperties, DispatcherQueuePriority.Normal);
}
// Note: This can get called multiple times during HighContrast switch, do we care?
private async void Settings_ColorValuesChanged(UISettings sender, object args)
{
await OnColorValuesChanged();
}
internal Task OnColorValuesChanged()
{
// Getting called off thread, so we need to dispatch to request value.
return DispatcherQueue.ExecuteOnUIThreadAsync(
() =>
{
// TODO: This doesn't stop the multiple calls if we're in our faked 'White' HighContrast Mode below.
if (CurrentTheme != Application.Current.RequestedTheme ||
IsHighContrast != _accessible.HighContrast)
{
UpdateProperties();
}
}, DispatcherQueuePriority.Normal);
}
private void CoreWindow_Activated(Windows.UI.Core.CoreWindow sender, Windows.UI.Core.WindowActivatedEventArgs args)
{
if (CurrentTheme != Application.Current.RequestedTheme ||
IsHighContrast != _accessible.HighContrast)
{
UpdateProperties();
}
}
/// <summary>
/// Set our current properties and fire a change notification.
/// </summary>
private void UpdateProperties()
{
// TODO: Not sure if HighContrastScheme names are localized?
if (_accessible.HighContrast && _accessible.HighContrastScheme.IndexOf("white", StringComparison.OrdinalIgnoreCase) != -1)
{
IsHighContrast = false;
CurrentTheme = ApplicationTheme.Light;
}
else
{
// Otherwise, we just set to what's in the system as we'd expect.
IsHighContrast = _accessible.HighContrast;
CurrentTheme = Application.Current.RequestedTheme;
}
ThemeChanged?.Invoke(this);
}
/// <inheritdoc/>
public void Dispose()
{
_accessible.HighContrastChanged -= Accessible_HighContrastChanged;
_settings.ColorValuesChanged -= Settings_ColorValuesChanged;
if (Window.Current != null)
{
Window.Current.CoreWindow.Activated -= CoreWindow_Activated;
}
}
}
}

@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Animation;
/// <summary>
/// In App Notification defines a control to show local notification in the app.
/// </summary>
public partial class InAppNotification
{
/// <summary>
/// Gets the value of the KeyFrameDuration attached Property
/// </summary>
/// <param name="obj">the KeyFrame where the duration is set</param>
/// <returns>Value of KeyFrameDuration</returns>
public static TimeSpan GetKeyFrameDuration(DependencyObject obj)
{
return (TimeSpan)obj.GetValue(KeyFrameDurationProperty);
}
/// <summary>
/// Sets the value of the KeyFrameDuration attached property
/// </summary>
/// <param name="obj">The KeyFrame object where the property is attached</param>
/// <param name="value">The TimeSpan value to be set as duration</param>
public static void SetKeyFrameDuration(DependencyObject obj, TimeSpan value)
{
obj.SetValue(KeyFrameDurationProperty, value);
}
/// <summary>
/// Using a DependencyProperty as the backing store for KeyFrameDuration. This enables animation, styling, binding, etc
/// </summary>
public static readonly DependencyProperty KeyFrameDurationProperty =
DependencyProperty.RegisterAttached("KeyFrameDuration", typeof(TimeSpan), typeof(InAppNotification), new PropertyMetadata(0, OnKeyFrameAnimationChanged));
private static void OnKeyFrameAnimationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is TimeSpan ts)
{
if (d is DoubleKeyFrame dkf)
{
dkf.KeyTime = KeyTime.FromTimeSpan(ts);
}
else if (d is ObjectKeyFrame okf)
{
okf.KeyTime = KeyTime.FromTimeSpan(ts);
}
}
}
}
}

@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
/// <summary>
/// In App Notification defines a control to show local notification in the app.
/// </summary>
public partial class InAppNotification
{
/// <summary>
/// Key of the VisualStateGroup that show/dismiss content
/// </summary>
private const string GroupContent = "State";
/// <summary>
/// Key of the VisualState when content is showed
/// </summary>
private const string StateContentVisible = "Visible";
/// <summary>
/// Key of the VisualState when content is dismissed
/// </summary>
private const string StateContentCollapsed = "Collapsed";
/// <summary>
/// Key of the UI Element that dismiss the control
/// </summary>
private const string DismissButtonPart = "PART_DismissButton";
}
}

@ -0,0 +1,87 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Automation;
using Windows.UI.Xaml.Automation.Peers;
/// <summary>
/// In App Notification defines a control to show local notification in the app.
/// </summary>
public partial class InAppNotification
{
/// <summary>
/// Event raised when the notification is opening
/// </summary>
public event InAppNotificationOpeningEventHandler Opening;
/// <summary>
/// Event raised when the notification is opened
/// </summary>
public event EventHandler Opened;
/// <summary>
/// Event raised when the notification is closing
/// </summary>
public event InAppNotificationClosingEventHandler Closing;
/// <summary>
/// Event raised when the notification is closed
/// </summary>
public event InAppNotificationClosedEventHandler Closed;
private AutomationPeer peer;
private void DismissButton_Click(object sender, RoutedEventArgs e)
{
Dismiss(InAppNotificationDismissKind.User);
}
private void DismissTimer_Tick(object sender, object e)
{
Dismiss(InAppNotificationDismissKind.Timeout);
}
private void OpenAnimationTimer_Tick(object sender, object e)
{
lock (_openAnimationTimer)
{
_openAnimationTimer.Stop();
Opened?.Invoke(this, EventArgs.Empty);
SetValue(AutomationProperties.NameProperty, "Notification");
peer = FrameworkElementAutomationPeer.CreatePeerForElement(ContentTemplateRoot);
if (Content?.GetType() == typeof(string))
{
AutomateTextNotification(Content.ToString());
}
}
}
private void AutomateTextNotification(string message)
{
if (peer != null)
{
peer.SetFocus();
peer.RaiseNotificationEvent(
AutomationNotificationKind.Other,
AutomationNotificationProcessing.ImportantMostRecent,
"New notification" + message,
Guid.NewGuid().ToString());
}
}
private void ClosingAnimationTimer_Tick(object sender, object e)
{
lock (_closingAnimationTimer)
{
_closingAnimationTimer.Stop();
Closed?.Invoke(this, new InAppNotificationClosedEventArgs(_lastDismissKind));
}
}
}
}

@ -0,0 +1,102 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
using System;
using Windows.UI.Xaml;
/// <summary>
/// In App Notification defines a control to show local notification in the app.
/// </summary>
public partial class InAppNotification
{
/// <summary>
/// Identifies the <see cref="ShowDismissButton"/> dependency property.
/// </summary>
public static readonly DependencyProperty ShowDismissButtonProperty =
DependencyProperty.Register(nameof(ShowDismissButton), typeof(bool), typeof(InAppNotification), new PropertyMetadata(true, OnShowDismissButtonChanged));
/// <summary>
/// Identifies the <see cref="AnimationDuration"/> dependency property.
/// </summary>
public static readonly DependencyProperty AnimationDurationProperty =
DependencyProperty.Register(nameof(AnimationDuration), typeof(TimeSpan), typeof(InAppNotification), new PropertyMetadata(TimeSpan.FromMilliseconds(100)));
/// <summary>
/// Identifies the <see cref="VerticalOffset"/> dependency property.
/// </summary>
public static readonly DependencyProperty VerticalOffsetProperty =
DependencyProperty.Register(nameof(VerticalOffset), typeof(double), typeof(InAppNotification), new PropertyMetadata(100));
/// <summary>
/// Identifies the <see cref="HorizontalOffset"/> dependency property.
/// </summary>
public static readonly DependencyProperty HorizontalOffsetProperty =
DependencyProperty.Register(nameof(HorizontalOffset), typeof(double), typeof(InAppNotification), new PropertyMetadata(0));
/// <summary>
/// Identifies the <see cref="StackMode"/> dependency property.
/// </summary>
public static readonly DependencyProperty StackModeProperty =
DependencyProperty.Register(nameof(StackMode), typeof(StackMode), typeof(InAppNotification), new PropertyMetadata(StackMode.Replace));
/// <summary>
/// Gets or sets a value indicating whether to show the Dismiss button of the control.
/// </summary>
public bool ShowDismissButton
{
get => (bool)GetValue(ShowDismissButtonProperty);
set => SetValue(ShowDismissButtonProperty, value);
}
/// <summary>
/// Gets or sets a value indicating the duration of the popup animation (in milliseconds).
/// </summary>
public TimeSpan AnimationDuration
{
get => (TimeSpan)GetValue(AnimationDurationProperty);
set => SetValue(AnimationDurationProperty, value);
}
/// <summary>
/// Gets or sets a value indicating the vertical offset of the popup animation.
/// </summary>
public double VerticalOffset
{
get => (double)GetValue(VerticalOffsetProperty);
set => SetValue(VerticalOffsetProperty, value);
}
/// <summary>
/// Gets or sets a value indicating the horizontal offset of the popup animation.
/// </summary>
public double HorizontalOffset
{
get => (double)GetValue(HorizontalOffsetProperty);
set => SetValue(HorizontalOffsetProperty, value);
}
/// <summary>
/// Gets or sets a value indicating the stack mode of the notifications.
/// </summary>
public StackMode StackMode
{
get => (StackMode)GetValue(StackModeProperty);
set => SetValue(StackModeProperty, value);
}
private static void OnShowDismissButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var inApNotification = d as InAppNotification;
if (inApNotification._dismissButton != null)
{
bool showDismissButton = (bool)e.NewValue;
inApNotification._dismissButton.Visibility = showDismissButton ? Visibility.Visible : Visibility.Collapsed;
}
}
}
}

@ -0,0 +1,274 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
using System;
using System.Collections.Generic;
using System.Linq;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
/// <summary>
/// In App Notification defines a control to show local notification in the app.
/// </summary>
[TemplateVisualState(Name = StateContentVisible, GroupName = GroupContent)]
[TemplateVisualState(Name = StateContentCollapsed, GroupName = GroupContent)]
[TemplatePart(Name = DismissButtonPart, Type = typeof(Button))]
public partial class InAppNotification : ContentControl
{
private InAppNotificationDismissKind _lastDismissKind;
private readonly DispatcherTimer _openAnimationTimer = new DispatcherTimer();
private readonly DispatcherTimer _closingAnimationTimer = new DispatcherTimer();
private readonly DispatcherTimer _dismissTimer = new DispatcherTimer();
private Button _dismissButton;
//private VisualStateGroup _visualStateGroup;
private readonly List<NotificationOptions> _stackedNotificationOptions = new List<NotificationOptions>();
/// <summary>
/// Initializes a new instance of the <see cref="InAppNotification"/> class.
/// </summary>
public InAppNotification()
{
DefaultStyleKey = typeof(InAppNotification);
_dismissTimer.Tick += DismissTimer_Tick;
_openAnimationTimer.Tick += OpenAnimationTimer_Tick;
_closingAnimationTimer.Tick += ClosingAnimationTimer_Tick;
}
/// <inheritdoc />
protected override void OnApplyTemplate()
{
if (_dismissButton != null)
{
_dismissButton.Click -= DismissButton_Click;
}
_dismissButton = (Button)GetTemplateChild(DismissButtonPart);
//_visualStateGroup = (VisualStateGroup)GetTemplateChild(GroupContent);
if (_dismissButton != null)
{
_dismissButton.Visibility = ShowDismissButton ? Visibility.Visible : Visibility.Collapsed;
_dismissButton.Click += DismissButton_Click;
}
if (Visibility == Visibility.Visible)
{
VisualStateManager.GoToState(this, StateContentVisible, true);
}
else
{
VisualStateManager.GoToState(this, StateContentCollapsed, true);
}
base.OnApplyTemplate();
}
/// <summary>
/// Show notification using the current template
/// </summary>
/// <param name="duration">Displayed duration of the notification in ms (less or equal 0 means infinite duration)</param>
public void Show(int duration = 0)
{
lock (_openAnimationTimer)
lock (_closingAnimationTimer)
lock (_dismissTimer)
{
_openAnimationTimer.Stop();
_closingAnimationTimer.Stop();
_dismissTimer.Stop();
var eventArgs = new InAppNotificationOpeningEventArgs();
Opening?.Invoke(this, eventArgs);
if (eventArgs.Cancel)
{
return;
}
Visibility = Visibility.Visible;
VisualStateManager.GoToState(this, StateContentVisible, true);
_openAnimationTimer.Interval = AnimationDuration;
_openAnimationTimer.Start();
if (duration > 0)
{
_dismissTimer.Interval = TimeSpan.FromMilliseconds(duration);
_dismissTimer.Start();
}
}
}
/// <summary>
/// Show notification using text as the content of the notification
/// </summary>
/// <param name="text">Text used as the content of the notification</param>
/// <param name="duration">Displayed duration of the notification in ms (less or equal 0 means infinite duration)</param>
public void Show(string text, int duration = 0)
{
var notificationOptions = new NotificationOptions
{
Duration = duration,
Content = text
};
Show(notificationOptions);
}
/// <summary>
/// Show notification using UIElement as the content of the notification
/// </summary>
/// <param name="element">UIElement used as the content of the notification</param>
/// <param name="duration">Displayed duration of the notification in ms (less or equal 0 means infinite duration)</param>
public void Show(UIElement element, int duration = 0)
{
var notificationOptions = new NotificationOptions
{
Duration = duration,
Content = element
};
Show(notificationOptions);
}
/// <summary>
/// Show notification using DataTemplate as the content of the notification
/// </summary>
/// <param name="dataTemplate">DataTemplate used as the content of the notification</param>
/// <param name="duration">Displayed duration of the notification in ms (less or equal 0 means infinite duration)</param>
public void Show(DataTemplate dataTemplate, int duration = 0)
{
var notificationOptions = new NotificationOptions
{
Duration = duration,
Content = dataTemplate
};
Show(notificationOptions);
}
/// <summary>
/// Dismiss the notification
/// </summary>
public void Dismiss()
{
Dismiss(InAppNotificationDismissKind.Programmatic);
}
/// <summary>
/// Dismiss the notification
/// </summary>
/// <param name="dismissKind">Kind of action that triggered dismiss event</param>
private void Dismiss(InAppNotificationDismissKind dismissKind)
{
lock (_openAnimationTimer)
lock (_closingAnimationTimer)
lock (_dismissTimer)
{
if (Visibility == Visibility.Visible)
{
_dismissTimer.Stop();
// Continue to display notification if on remaining stacked notification
if (_stackedNotificationOptions.Any())
{
_stackedNotificationOptions.RemoveAt(0);
if (_stackedNotificationOptions.Any())
{
var notificationOptions = _stackedNotificationOptions[0];
UpdateContent(notificationOptions);
if (notificationOptions.Duration > 0)
{
_dismissTimer.Interval = TimeSpan.FromMilliseconds(notificationOptions.Duration);
_dismissTimer.Start();
}
return;
}
}
_openAnimationTimer.Stop();
_closingAnimationTimer.Stop();
var closingEventArgs = new InAppNotificationClosingEventArgs(dismissKind);
Closing?.Invoke(this, closingEventArgs);
if (closingEventArgs.Cancel)
{
return;
}
VisualStateManager.GoToState(this, StateContentCollapsed, true);
_lastDismissKind = dismissKind;
_closingAnimationTimer.Interval = AnimationDuration;
_closingAnimationTimer.Start();
}
}
}
/// <summary>
/// Informs if the notification should be displayed immediately (based on the StackMode)
/// </summary>
/// <returns>True if notification should be displayed immediately</returns>
private bool ShouldDisplayImmediately()
{
return StackMode != StackMode.QueueBehind ||
(StackMode == StackMode.QueueBehind && _stackedNotificationOptions.Count == 0);
}
/// <summary>
/// Update the Content of the notification
/// </summary>
/// <param name="notificationOptions">Information about the notification to display</param>
private void UpdateContent(NotificationOptions notificationOptions)
{
switch (notificationOptions.Content)
{
case string text:
ContentTemplate = null;
Content = text;
break;
case UIElement element:
ContentTemplate = null;
Content = element;
break;
case DataTemplate dataTemplate:
ContentTemplate = dataTemplate;
Content = null;
break;
}
}
/// <summary>
/// Handle the display of the notification based on the current StackMode
/// </summary>
/// <param name="notificationOptions">Information about the notification to display</param>
private void Show(NotificationOptions notificationOptions)
{
bool shouldDisplayImmediately = ShouldDisplayImmediately();
if (StackMode == StackMode.QueueBehind)
{
_stackedNotificationOptions.Add(notificationOptions);
}
if (StackMode == StackMode.StackInFront)
{
_stackedNotificationOptions.Insert(0, notificationOptions);
}
if (shouldDisplayImmediately)
{
UpdateContent(notificationOptions);
Show(notificationOptions.Duration);
}
}
}
}

@ -0,0 +1,35 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Notepads.Controls">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles/MSEdgeNotificationStyle.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="local:InAppNotification" x:Key="BaseInAppNotificationsStyle">
<Setter Property="Background" Value="{ThemeResource SystemControlBackgroundChromeMediumBrush}" />
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource SystemControlForegroundBaseLowBrush}" />
<Setter Property="Visibility" Value="Collapsed" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Bottom" />
<Setter Property="MinHeight" Value="55" />
<Setter Property="FontSize" Value="16" />
<Setter Property="RenderTransformOrigin" Value="0.5,1" />
<Setter Property="Margin" Value="24,12" />
<Setter Property="Padding" Value="24,12" />
<Setter Property="MaxWidth" Value="960" />
<Setter Property="MinWidth" Value="132" />
<Setter Property="AnimationDuration" Value="0:0:0.100" />
<Setter Property="VerticalOffset" Value="100" />
<Setter Property="HorizontalOffset" Value="0" />
<Setter Property="Template" Value="{StaticResource MSEdgeNotificationTemplate}" />
</Style>
<Style TargetType="local:InAppNotification" BasedOn="{StaticResource BaseInAppNotificationsStyle}">
<Setter Property="Background" Value="{ThemeResource SystemControlAcrylicElementBrush}" />
</Style>
</ResourceDictionary>

@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
using System;
/// <summary>
/// A delegate for <see cref="InAppNotification"/> dismissing.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void InAppNotificationClosedEventHandler(object sender, InAppNotificationClosedEventArgs e);
/// <summary>
/// Provides data for the <see cref="InAppNotification"/> Dismissing event.
/// </summary>
public class InAppNotificationClosedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="InAppNotificationClosedEventArgs"/> class.
/// </summary>
/// <param name="dismissKind">Dismiss kind that triggered the closing event</param>
public InAppNotificationClosedEventArgs(InAppNotificationDismissKind dismissKind)
{
DismissKind = dismissKind;
}
/// <summary>
/// Gets the kind of action for the closing event.
/// </summary>
public InAppNotificationDismissKind DismissKind { get; private set; }
}
}

@ -0,0 +1,41 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
using System;
/// <summary>
/// A delegate for <see cref="InAppNotification"/> dismissing.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void InAppNotificationClosingEventHandler(object sender, InAppNotificationClosingEventArgs e);
/// <summary>
/// Provides data for the <see cref="InAppNotification"/> Dismissing event.
/// </summary>
public class InAppNotificationClosingEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="InAppNotificationClosingEventArgs"/> class.
/// </summary>
/// <param name="dismissKind">Dismiss kind that triggered the closing event</param>
public InAppNotificationClosingEventArgs(InAppNotificationDismissKind dismissKind)
{
DismissKind = dismissKind;
}
/// <summary>
/// Gets the kind of action for the closing event.
/// </summary>
public InAppNotificationDismissKind DismissKind { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the notification should be closed.
/// </summary>
public bool Cancel { get; set; }
}
}

@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
/// <summary>
/// Enumeration to describe how an InAppNotification was dismissed
/// </summary>
public enum InAppNotificationDismissKind
{
/// <summary>
/// When the system dismissed the notification.
/// </summary>
Programmatic,
/// <summary>
/// When user explicitly dismissed the notification.
/// </summary>
User,
/// <summary>
/// When the system dismissed the notification after timeout.
/// </summary>
Timeout
}
}

@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
using System;
/// <summary>
/// A delegate for <see cref="InAppNotification"/> opening.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void InAppNotificationOpeningEventHandler(object sender, InAppNotificationOpeningEventArgs e);
/// <summary>
/// Provides data for the <see cref="InAppNotification"/> Dismissing event.
/// </summary>
public class InAppNotificationOpeningEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="InAppNotificationOpeningEventArgs"/> class.
/// </summary>
public InAppNotificationOpeningEventArgs()
{
}
/// <summary>
/// Gets or sets a value indicating whether the notification should be opened.
/// </summary>
public bool Cancel { get; set; }
}
}

@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
using Windows.UI.Xaml;
/// <summary>
/// Base class that contains options of notification
/// </summary>
internal class NotificationOptions
{
/// <summary>
/// Gets or sets duration of the stacked notification
/// </summary>
public int Duration { get; set; }
/// <summary>
/// Gets or sets Content of the notification
/// Could be either a <see cref="string"/> or a <see cref="UIElement"/> or a <see cref="DataTemplate"/>
/// </summary>
public object Content { get; set; }
}
}

@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/InAppNotification
namespace Notepads.Controls
{
/// <summary>
/// The Stack mode of an in-app notification.
/// </summary>
public enum StackMode
{
/// <summary>
/// Each notification will replace the previous one
/// </summary>
Replace,
/// <summary>
/// Opening a notification will display it immediately, remaining notifications will appear when a notification is dismissed
/// </summary>
StackInFront,
/// <summary>
/// Dismissing a notification will show the next one in the queue
/// </summary>
QueueBehind
}
}

@ -0,0 +1,200 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Notepads.Controls">
<ResourceDictionary.ThemeDictionaries>
<!-- Default is a fallback if a more precise theme isn't called
out below -->
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="SystemControlMSEdgeNotificationPointerOverChromeBrush" Color="Transparent" />
<SolidColorBrush x:Key="SystemControlMSEdgeNotificationPointerOverForegroundBrush" Color="{ThemeResource SystemColorButtonTextColor}"/>
<SolidColorBrush x:Key="SystemControlMSEdgeNotificationButtonBorderBrush" Color="Transparent" />
</ResourceDictionary>
<!-- HighContrast is used in all high contrast themes -->
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="SystemControlMSEdgeNotificationPointerOverChromeBrush" Color="{ThemeResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="SystemControlMSEdgeNotificationPointerOverForegroundBrush" Color="{ThemeResource SystemColorHighlightTextColor}"/>
<SolidColorBrush x:Key="SystemControlMSEdgeNotificationButtonBorderBrush" Color="{ThemeResource SystemColorButtonTextColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Style x:Key="DismissTextBlockButtonStyle" TargetType="ButtonBase">
<Setter Property="Background" Value="{ThemeResource HyperlinkButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}" />
<Setter Property="Width" Value="40" />
<Setter Property="Height" Value="40" />
<Setter Property="UseSystemFocusVisuals" Value="True" />
<Setter Property="HighContrastAdjustment" Value="None" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ButtonBase">
<Grid x:Name="RootGrid" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
<Border x:Name="TextBorder" BorderThickness="2" BorderBrush="{ThemeResource SystemControlMSEdgeNotificationButtonBorderBrush}">
<ContentPresenter x:Name="Text"
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlMSEdgeNotificationPointerOverForegroundBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlMSEdgeNotificationPointerOverChromeBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlMSEdgeNotificationPointerOverForegroundBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlMSEdgeNotificationPointerOverChromeBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBorderBrushDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="MSEdgeNotificationTemplate"
TargetType="local:InAppNotification">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="State">
<VisualState x:Name="Collapsed">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="RootGrid"
Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)">
<EasingDoubleKeyFrame KeyTime="0" Value="0" />
<EasingDoubleKeyFrame local:InAppNotification.KeyFrameDuration="{Binding AnimationDuration, RelativeSource={RelativeSource TemplatedParent}}"
Value="{Binding HorizontalOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="RootGrid"
Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)">
<EasingDoubleKeyFrame KeyTime="0" Value="0" />
<EasingDoubleKeyFrame local:InAppNotification.KeyFrameDuration="{Binding AnimationDuration, RelativeSource={RelativeSource TemplatedParent}}"
Value="{Binding VerticalOffset, RelativeSource={RelativeSource TemplatedParent}}"/>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
<DiscreteObjectKeyFrame local:InAppNotification.KeyFrameDuration="{Binding AnimationDuration, RelativeSource={RelativeSource TemplatedParent}}">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Visible">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="RootGrid"
Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)">
<EasingDoubleKeyFrame KeyTime="0" Value="{Binding HorizontalOffset, RelativeSource={RelativeSource TemplatedParent}}" />
<EasingDoubleKeyFrame local:InAppNotification.KeyFrameDuration="{Binding AnimationDuration, RelativeSource={RelativeSource TemplatedParent}}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="RootGrid"
Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)">
<EasingDoubleKeyFrame KeyTime="0" Value="{Binding VerticalOffset, RelativeSource={RelativeSource TemplatedParent}}" />
<EasingDoubleKeyFrame local:InAppNotification.KeyFrameDuration="{Binding AnimationDuration, RelativeSource={RelativeSource TemplatedParent}}" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="RootGrid"
RenderTransformOrigin="{TemplateBinding RenderTransformOrigin}"
Margin="{TemplateBinding Margin}"
Padding="{TemplateBinding Padding}"
MaxWidth="{TemplateBinding MaxWidth}"
Visibility="{TemplateBinding Visibility}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid.RenderTransform>
<CompositeTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
HorizontalContentAlignment="Stretch"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
VerticalContentAlignment="Center"
TextWrapping="WrapWholeWords" />
<Button x:Name="PART_DismissButton"
Grid.Column="1"
Margin="24,0,0,0"
FontSize="16"
Style="{StaticResource DismissTextBlockButtonStyle}"
Content="&#xE894;"
FontFamily="Segoe MDL2 Assets"
AutomationProperties.Name="Dismiss">
<Button.RenderTransform>
<TranslateTransform x:Name="DismissButtonTransform" X="18" />
</Button.RenderTransform>
</Button>
</Grid>
</Grid>
</ControlTemplate>
<Style x:Key="MSEdgeNotificationStyle" TargetType="local:InAppNotification">
<Setter Property="Background" Value="{ThemeResource SystemControlBackgroundChromeMediumBrush}" />
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource SystemControlBackgroundBaseLowBrush}" />
<Setter Property="Visibility" Value="Collapsed" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Bottom" />
<Setter Property="MinHeight" Value="55" />
<Setter Property="FontSize" Value="16" />
<Setter Property="RenderTransformOrigin" Value="0.5,1" />
<Setter Property="Margin" Value="24,12" />
<Setter Property="Padding" Value="24,12" />
<Setter Property="MaxWidth" Value="960" />
<Setter Property="MinWidth" Value="132" />
<Setter Property="AnimationDuration" Value="0:0:0.100" />
<Setter Property="VerticalOffset" Value="100" />
<Setter Property="HorizontalOffset" Value="0" />
<Setter Property="Template" Value="{StaticResource MSEdgeNotificationTemplate}" />
</Style>
</ResourceDictionary>

@ -0,0 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
namespace Notepads.Controls
{
using System;
using Windows.UI.Xaml.Documents;
/// <summary>
/// Arguments for the <see cref="MarkdownTextBlock.CodeBlockResolving"/> event when a Code Block is being rendered.
/// </summary>
public class CodeBlockResolvingEventArgs : EventArgs
{
internal CodeBlockResolvingEventArgs(InlineCollection inlineCollection, string text, string codeLanguage)
{
InlineCollection = inlineCollection;
Text = text;
CodeLanguage = codeLanguage;
}
/// <summary>
/// Gets the language of the Code Block, as specified by ```{Language} on the first line of the block,
/// e.g. <para/>
/// ```C# <para/>
/// public void Method();<para/>
/// ```<para/>
/// </summary>
public string CodeLanguage { get; }
/// <summary>
/// Gets the raw code block text
/// </summary>
public string Text { get; }
/// <summary>
/// Gets Collection to add formatted Text to.
/// </summary>
public InlineCollection InlineCollection { get; }
/// <summary>
/// Gets or sets a value indicating whether this event was handled successfully.
/// </summary>
public bool Handled { get; set; } = false;
}
}

@ -0,0 +1,73 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
namespace Notepads.Controls
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI.Xaml.Media;
/// <summary>
/// Arguments for the <see cref="MarkdownTextBlock.ImageResolving"/> event which is called when a url needs to be resolved to a <see cref="ImageSource"/>.
/// </summary>
public class ImageResolvingEventArgs : EventArgs
{
private readonly IList<TaskCompletionSource<object>> _deferrals;
internal ImageResolvingEventArgs(string url, string tooltip)
{
_deferrals = new List<TaskCompletionSource<object>>();
Url = url;
Tooltip = tooltip;
}
/// <summary>
/// Gets the url of the image in the markdown document.
/// </summary>
public string Url { get; }
/// <summary>
/// Gets the tooltip of the image in the markdown document.
/// </summary>
public string Tooltip { get; }
/// <summary>
/// Gets or sets a value indicating whether this event was handled successfully.
/// </summary>
public bool Handled { get; set; }
/// <summary>
/// Gets or sets the image to display in the <see cref="MarkdownTextBlock"/>.
/// </summary>
public ImageSource Image { get; set; }
/// <summary>
/// Informs the <see cref="MarkdownTextBlock"/> that the event handler might run asynchronously.
/// </summary>
/// <returns>Deferral</returns>
public Deferral GetDeferral()
{
var task = new TaskCompletionSource<object>();
_deferrals.Add(task);
return new Deferral(() =>
{
task.SetResult(null);
});
}
/// <summary>
/// Returns a <see cref="Task"/> that completes when all <see cref="Deferral"/>s have completed.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
internal Task WaitForDeferrals()
{
return Task.WhenAll(_deferrals.Select(f => f.Task));
}
}
}

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlock
namespace Notepads.Controls
{
using System;
/// <summary>
/// Arguments for the OnLinkClicked event which is fired then the user presses a link.
/// </summary>
public class LinkClickedEventArgs : EventArgs
{
internal LinkClickedEventArgs(string link)
{
Link = link;
}
/// <summary>
/// Gets the link that was tapped.
/// </summary>
public string Link { get; }
}
}

@ -0,0 +1,196 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
namespace Notepads.Controls.Markdown
{
using System.Text;
/// <summary>
/// Represents a block of text that is displayed in a fixed-width font. Inline elements and
/// escape sequences are ignored inside the code block.
/// </summary>
public class CodeBlock : MarkdownBlock
{
/// <summary>
/// Initializes a new instance of the <see cref="CodeBlock"/> class.
/// </summary>
public CodeBlock()
: base(MarkdownBlockType.Code)
{
}
/// <summary>
/// Gets or sets the source code to display.
/// </summary>
public string Text { get; set; }
/// <summary>
/// Gets or sets the Language specified in prefix, e.g. ```c# (Github Style Parsing).<para/>
/// This does not guarantee that the Code Block has a language, or no language, some valid code might not have been prefixed, and this will still return null. <para/>
/// To ensure all Code is Highlighted (If desired), you might have to determine the language from the provided string, such as looking for key words.
/// </summary>
public string CodeLanguage { get; set; }
/// <summary>
/// Parses a code block.
/// </summary>
/// <param name="markdown"> The markdown text. </param>
/// <param name="start"> The location of the first character in the block. </param>
/// <param name="maxEnd"> The location to stop parsing. </param>
/// <param name="quoteDepth"> The current nesting level for block quoting. </param>
/// <param name="actualEnd"> Set to the end of the block when the return value is non-null. </param>
/// <returns> A parsed code block, or <c>null</c> if this is not a code block. </returns>
internal static CodeBlock Parse(string markdown, int start, int maxEnd, int quoteDepth, out int actualEnd)
{
StringBuilder code = null;
actualEnd = start;
bool insideCodeBlock = false;
string codeLanguage = string.Empty;
/*
Two options here:
Either every line starts with a tab character or at least 4 spaces
Or the code block starts and ends with ```
*/
foreach (var lineInfo in Common.ParseLines(markdown, start, maxEnd, quoteDepth))
{
int pos = lineInfo.StartOfLine;
if (pos < maxEnd && markdown[pos] == '`')
{
var backTickCount = 0;
while (pos < maxEnd && backTickCount < 3)
{
if (markdown[pos] == '`')
{
backTickCount++;
}
else
{
break;
}
pos++;
}
if (backTickCount == 3)
{
insideCodeBlock = !insideCodeBlock;
if (!insideCodeBlock)
{
actualEnd = lineInfo.StartOfNextLine;
break;
}
else
{
// Collects the Programming Language from the end of the starting ticks.
while (pos < lineInfo.EndOfLine)
{
codeLanguage += markdown[pos];
pos++;
}
}
}
}
if (!insideCodeBlock)
{
// Add every line that starts with a tab character or at least 4 spaces.
if (pos < maxEnd && markdown[pos] == '\t')
{
pos++;
}
else
{
int spaceCount = 0;
while (pos < maxEnd && spaceCount < 4)
{
if (markdown[pos] == ' ')
{
spaceCount++;
}
else if (markdown[pos] == '\t')
{
spaceCount += 4;
}
else
{
break;
}
pos++;
}
if (spaceCount < 4)
{
// We found a line that doesn't start with a tab or 4 spaces.
// But don't end the code block until we find a non-blank line.
if (lineInfo.IsLineBlank == false)
{
break;
}
}
}
}
// Separate each line of the code text.
if (code == null)
{
code = new StringBuilder();
}
else
{
code.AppendLine();
}
if (lineInfo.IsLineBlank == false)
{
// Append the code text, excluding the first tab/4 spaces, and convert tab characters into spaces.
string lineText = markdown.Substring(pos, lineInfo.EndOfLine - pos);
int startOfLinePos = code.Length;
for (int i = 0; i < lineText.Length; i++)
{
char c = lineText[i];
if (c == '\t')
{
code.Append(' ', 4 - ((code.Length - startOfLinePos) % 4));
}
else
{
code.Append(c);
}
}
}
// Update the end position.
actualEnd = lineInfo.StartOfNextLine;
}
if (code == null)
{
// Not a valid code block.
actualEnd = start;
return null;
}
// Blank lines should be trimmed from the start and end.
return new CodeBlock()
{
Text = code.ToString().Trim('\r', '\n'),
CodeLanguage = !string.IsNullOrWhiteSpace(codeLanguage) ? codeLanguage.Trim() : null
};
}
/// <summary>
/// Converts the object into it's textual representation.
/// </summary>
/// <returns> The textual representation of this object. </returns>
public override string ToString()
{
return Text;
}
}
}

@ -0,0 +1,166 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
namespace Notepads.Controls.Markdown
{
using System;
using System.Collections.Generic;
/// <summary>
/// Represents a heading.
/// <seealso href="https://spec.commonmark.org/0.29/#atx-headings">Single-Line Header CommonMark Spec</seealso>
/// <seealso href="https://spec.commonmark.org/0.29/#setext-headings">Two-Line Header CommonMark Spec</seealso>
/// </summary>
public class HeaderBlock : MarkdownBlock
{
/// <summary>
/// Initializes a new instance of the <see cref="HeaderBlock"/> class.
/// </summary>
public HeaderBlock()
: base(MarkdownBlockType.Header)
{
}
private int _headerLevel;
/// <summary>
/// Gets or sets the header level (1-6). 1 is the most important header, 6 is the least important.
/// </summary>
public int HeaderLevel
{
get => _headerLevel;
set
{
if (value < 1 || value > 6)
{
throw new ArgumentOutOfRangeException("HeaderLevel", "The header level must be between 1 and 6 (inclusive).");
}
_headerLevel = value;
}
}
/// <summary>
/// Gets or sets the contents of the block.
/// </summary>
public IList<MarkdownInline> Inlines { get; set; }
/// <summary>
/// Parses a header that starts with a hash.
/// </summary>
/// <param name="markdown"> The markdown text. </param>
/// <param name="start"> The location of the first hash character. </param>
/// <param name="end"> The location of the end of the line. </param>
/// <returns> A parsed header block, or <c>null</c> if this is not a header. </returns>
internal static HeaderBlock ParseHashPrefixedHeader(string markdown, int start, int end)
{
// This type of header starts with one or more '#' characters, followed by the header
// text, optionally followed by any number of hash characters.
var result = new HeaderBlock();
// Figure out how many consecutive hash characters there are.
int pos = start;
while (pos < end && markdown[pos] == '#' && pos - start < 6)
{
pos++;
}
result.HeaderLevel = pos - start;
if (result.HeaderLevel == 0)
{
return null;
}
// Ignore any hashes at the end of the line.
while (pos < end && markdown[end - 1] == '#')
{
end--;
}
// Parse the inline content.
result.Inlines = Common.ParseInlineChildren(markdown, pos, end);
return result;
}
/// <summary>
/// Parses a two-line header.
/// </summary>
/// <param name="markdown"> The markdown text. </param>
/// <param name="firstLineStart"> The location of the start of the first line. </param>
/// <param name="firstLineEnd"> The location of the end of the first line. </param>
/// <param name="secondLineStart"> The location of the start of the second line. </param>
/// <param name="secondLineEnd"> The location of the end of the second line. </param>
/// <returns> A parsed header block, or <c>null</c> if this is not a header. </returns>
internal static HeaderBlock ParseUnderlineStyleHeader(string markdown, int firstLineStart, int firstLineEnd, int secondLineStart, int secondLineEnd)
{
// This type of header starts with some text on the first line, followed by one or more
// underline characters ('=' or '-') on the second line.
// The second line can have whitespace after the underline characters, but not before
// or between each character.
// Check the second line is valid.
if (secondLineEnd <= secondLineStart)
{
return null;
}
// Figure out what the underline character is ('=' or '-').
char underlineChar = markdown[secondLineStart];
if (underlineChar != '=' && underlineChar != '-')
{
return null;
}
// Read past consecutive underline characters.
int pos = secondLineStart + 1;
for (; pos < secondLineEnd; pos++)
{
char c = markdown[pos];
if (c != underlineChar)
{
break;
}
pos++;
}
// The rest of the line must be whitespace.
for (; pos < secondLineEnd; pos++)
{
char c = markdown[pos];
if (c != ' ' && c != '\t')
{
return null;
}
pos++;
}
var result = new HeaderBlock
{
HeaderLevel = underlineChar == '=' ? 1 : 2,
// Parse the inline content.
Inlines = Common.ParseInlineChildren(markdown, firstLineStart, firstLineEnd)
};
return result;
}
/// <summary>
/// Converts the object into it's textual representation.
/// </summary>
/// <returns> The textual representation of this object. </returns>
public override string ToString()
{
if (Inlines == null)
{
return base.ToString();
}
return string.Join(string.Empty, Inlines);
}
}
}

@ -0,0 +1,73 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
namespace Notepads.Controls.Markdown
{
/// <summary>
/// Represents a horizontal line.
/// </summary>
public class HorizontalRuleBlock : MarkdownBlock
{
/// <summary>
/// Initializes a new instance of the <see cref="HorizontalRuleBlock"/> class.
/// </summary>
public HorizontalRuleBlock()
: base(MarkdownBlockType.HorizontalRule)
{
}
/// <summary>
/// Parses a horizontal rule.
/// </summary>
/// <param name="markdown"> The markdown text. </param>
/// <param name="start"> The location of the start of the line. </param>
/// <param name="end"> The location of the end of the line. </param>
/// <returns> A parsed horizontal rule block, or <c>null</c> if this is not a horizontal rule. </returns>
internal static HorizontalRuleBlock Parse(string markdown, int start, int end)
{
// A horizontal rule is a line with at least 3 stars, optionally separated by spaces
// OR a line with at least 3 dashes, optionally separated by spaces
// OR a line with at least 3 underscores, optionally separated by spaces.
char hrChar = '\0';
int hrCharCount = 0;
int pos = start;
while (pos < end)
{
char c = markdown[pos++];
if (c == '*' || c == '-' || c == '_')
{
// All of the non-whitespace characters on the line must match.
if (hrCharCount > 0 && c != hrChar)
{
return null;
}
hrChar = c;
hrCharCount++;
}
else if (c == '\n')
{
break;
}
else if (!ParseHelpers.IsMarkdownWhiteSpace(c))
{
return null;
}
}
// Hopefully there were at least 3 stars/dashes/underscores.
return hrCharCount >= 3 ? new HorizontalRuleBlock() : null;
}
/// <summary>
/// Converts the object into it's textual representation.
/// </summary>
/// <returns> The textual representation of this object. </returns>
public override string ToString()
{
return "---";
}
}
}

@ -0,0 +1,170 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks
namespace Notepads.Controls.Markdown
{
/// <summary>
/// Represents the target of a reference ([ref][]).
/// </summary>
public class LinkReferenceBlock : MarkdownBlock
{
/// <summary>
/// Initializes a new instance of the <see cref="LinkReferenceBlock"/> class.
/// </summary>
public LinkReferenceBlock()
: base(MarkdownBlockType.LinkReference)
{
}
/// <summary>
/// Gets or sets the reference ID.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets the link URL.
/// </summary>
public string Url { get; set; }
/// <summary>
/// Gets or sets a tooltip to display on hover.
/// </summary>
public string Tooltip { get; set; }
/// <summary>
/// Attempts to parse a reference e.g. "[example]: http://www.reddit.com 'title'".
/// </summary>
/// <param name="markdown"> The markdown text. </param>
/// <param name="start"> The location to start parsing. </param>
/// <param name="end"> The location to stop parsing. </param>
/// <returns> A parsed markdown link, or <c>null</c> if this is not a markdown link. </returns>
internal static LinkReferenceBlock Parse(string markdown, int start, int end)
{
// Expect a '[' character.
if (start >= end || markdown[start] != '[')
{
return null;
}
// Find the ']' character
int pos = start + 1;
while (pos < end)
{
if (markdown[pos] == ']')
{
break;
}
pos++;
}
if (pos == end)
{
return null;
}
// Extract the ID.
string id = markdown.Substring(start + 1, pos - (start + 1));
// Expect the ':' character.
pos++;
if (pos == end || markdown[pos] != ':')
{
return null;
}
// Skip whitespace
pos++;
while (pos < end && ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]))
{
pos++;
}
if (pos == end)
{
return null;
}
// Extract the URL.
int urlStart = pos;
while (pos < end && !ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]))
{
pos++;
}
string url = TextRunInline.ResolveEscapeSequences(markdown, urlStart, pos);
// Ignore leading '<' and trailing '>'.
url = url.TrimStart('<').TrimEnd('>');
// Skip whitespace.
pos++;
while (pos < end && ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]))
{
pos++;
}
string tooltip = null;
if (pos < end)
{
// Extract the tooltip.
char tooltipEndChar;
switch (markdown[pos])
{
case '(':
tooltipEndChar = ')';
break;
case '"':
case '\'':
tooltipEndChar = markdown[pos];
break;
default:
return null;
}
pos++;
int tooltipStart = pos;
while (pos < end && markdown[pos] != tooltipEndChar)
{
pos++;
}
if (pos == end)
{
return null; // No end character.
}
tooltip = markdown.Substring(tooltipStart, pos - tooltipStart);
// Check there isn't any trailing text.
pos++;
while (pos < end && ParseHelpers.IsMarkdownWhiteSpace(markdown[pos]))
{
pos++;
}
if (pos < end)
{
return null;
}
}
// We found something!
var result = new LinkReferenceBlock { Id = id, Url = url, Tooltip = tooltip };
return result;
}
/// <summary>
/// Converts the object into it's textual representation.
/// </summary>
/// <returns> The textual representation of this object. </returns>
public override string ToString()
{
return $"[{Id}]: {Url} {Tooltip}";
}
}
}

@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks/List
namespace Notepads.Controls.Markdown
{
using System.Collections.Generic;
/// <summary>
/// This specifies the Content of the List element.
/// </summary>
public class ListItemBlock
{
/// <summary>
/// Gets or sets the contents of the list item.
/// </summary>
public IList<MarkdownBlock> Blocks { get; set; }
internal ListItemBlock()
{
}
}
}

@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks/List
namespace Notepads.Controls.Markdown
{
using System.Text;
internal class ListItemBuilder : MarkdownBlock
{
public StringBuilder Builder { get; } = new StringBuilder();
public ListItemBuilder()
: base(MarkdownBlockType.ListItemBuilder)
{
}
}
}

@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks/List
namespace Notepads.Controls.Markdown
{
internal class ListItemPreamble
{
public ListStyle Style { get; set; }
public int ContentStartPos { get; set; }
}
}

@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Source: https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown/Blocks/List
namespace Notepads.Controls.Markdown
{
internal class NestedListInfo
{
public ListBlock List { get; set; }
// The number of spaces at the start of the line the list first appeared.
public int SpaceCount { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save