diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000000..4e34d4b132
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: Bug report
+about: Create a report to help us improve Avalonia
+title: ''
+labels: bug
+assignees: ''
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**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, Mac, Linux (State distribution)]
+- Version [e.g. 0.10.0-rc1 or 0.9.12]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..687355d825
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Questions, Discussions, Ideas
+ url: https://github.com/AvaloniaUI/Avalonia/discussions/new
+ about: Please ask and answer questions here.
+ - name: Avalonia Community Support on Gitter
+ url: https://gitter.im/AvaloniaUI/Avalonia
+ about: Please ask and answer questions here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000000..5f0a04cee3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,19 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+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.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index acff8cc117..46e8665945 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -18,11 +18,13 @@
- [ ] Added unit tests (if possible)?
- [ ] Added XML documentation to any related classes?
-- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Avaloniaui.net with user documentation
+- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Documentation with user documentation
## Breaking changes
+## Obsoletions / Deprecations
+
## Fixed issues
-
+
diff --git a/build/ApiDiff.props b/build/ApiDiff.props
index fb65ef6e87..666417addf 100644
--- a/build/ApiDiff.props
+++ b/build/ApiDiff.props
@@ -1,6 +1,6 @@
- 0.10.0-preview6
+ 0.10.0
$(PackageId)
Avalonia
diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props
index e636461ad9..13419eb173 100644
--- a/build/HarfBuzzSharp.props
+++ b/build/HarfBuzzSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props
index f74ab07e31..c3b136d41d 100644
--- a/build/ReactiveUI.props
+++ b/build/ReactiveUI.props
@@ -1,5 +1,5 @@
-
+
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index 43ec995ed9..75bada4bfc 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -3,7 +3,7 @@
Avalonia
0.10.999
- Copyright 2020 © The AvaloniaUI Project
+ Copyright 2021 © The AvaloniaUI Project
https://avaloniaui.net
https://github.com/AvaloniaUI/Avalonia/
true
@@ -21,6 +21,6 @@
-
+
diff --git a/build/SourceLink.props b/build/SourceLink.props
index e27727c9e8..1e007e01eb 100644
--- a/build/SourceLink.props
+++ b/build/SourceLink.props
@@ -1,5 +1,26 @@
+
+ true
+ false
+ true
+ embedded
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+
+
+ true
+
+
+
+ true
+
+
-
+
+
+
+
+
+
diff --git a/native/Avalonia.Native/inc/key.h b/native/Avalonia.Native/inc/key.h
deleted file mode 100644
index 12d283cc17..0000000000
--- a/native/Avalonia.Native/inc/key.h
+++ /dev/null
@@ -1,1020 +0,0 @@
-#ifndef _KEY_H_
-#define _KEY_H_
-
-///
-/// Defines the keys available on a keyboard.
-///
-enum AvnKey
-{
- ///
- /// No key pressed.
- ///
- AvnKeyNone = 0,
-
- ///
- /// The Cancel key.
- ///
- AvnKeyCancel = 1,
-
- ///
- /// The Back key.
- ///
- AvnKeyBack = 2,
-
- ///
- /// The Tab key.
- ///
- AvnKeyTab = 3,
-
- ///
- /// The Linefeed key.
- ///
- AvnKeyLineFeed = 4,
-
- ///
- /// The Clear key.
- ///
- AvnKeyClear = 5,
-
- ///
- /// The Return key.
- ///
- AvnKeyReturn = 6,
-
- ///
- /// The Enter key.
- ///
- AvnKeyEnter = 6,
-
- ///
- /// The Pause key.
- ///
- AvnKeyPause = 7,
-
- ///
- /// The Caps Lock key.
- ///
- AvnKeyCapsLock = 8,
-
- ///
- /// The Caps Lock key.
- ///
- AvnKeyCapital = 8,
-
- ///
- /// The IME Hangul mode key.
- ///
- AvnKeyHangulMode = 9,
-
- ///
- /// The IME Kana mode key.
- ///
- AvnKeyKanaMode = 9,
-
- ///
- /// The IME Junja mode key.
- ///
- AvnKeyJunjaMode = 10,
-
- ///
- /// The IME Final mode key.
- ///
- AvnKeyFinalMode = 11,
-
- ///
- /// The IME Kanji mode key.
- ///
- AvnKeyKanjiMode = 12,
-
- ///
- /// The IME Hanja mode key.
- ///
- HanjaMode = 12,
-
- ///
- /// The Escape key.
- ///
- Escape = 13,
-
- ///
- /// The IME Convert key.
- ///
- ImeConvert = 14,
-
- ///
- /// The IME NonConvert key.
- ///
- ImeNonConvert = 15,
-
- ///
- /// The IME Accept key.
- ///
- ImeAccept = 16,
-
- ///
- /// The IME Mode change key.
- ///
- ImeModeChange = 17,
-
- ///
- /// The space bar.
- ///
- Space = 18,
-
- ///
- /// The Page Up key.
- ///
- PageUp = 19,
-
- ///
- /// The Page Up key.
- ///
- Prior = 19,
-
- ///
- /// The Page Down key.
- ///
- PageDown = 20,
-
- ///
- /// The Page Down key.
- ///
- Next = 20,
-
- ///
- /// The End key.
- ///
- End = 21,
-
- ///
- /// The Home key.
- ///
- Home = 22,
-
- ///
- /// The Left arrow key.
- ///
- Left = 23,
-
- ///
- /// The Up arrow key.
- ///
- Up = 24,
-
- ///
- /// The Right arrow key.
- ///
- Right = 25,
-
- ///
- /// The Down arrow key.
- ///
- Down = 26,
-
- ///
- /// The Select key.
- ///
- Select = 27,
-
- ///
- /// The Print key.
- ///
- Print = 28,
-
- ///
- /// The Execute key.
- ///
- Execute = 29,
-
- ///
- /// The Print Screen key.
- ///
- Snapshot = 30,
-
- ///
- /// The Print Screen key.
- ///
- PrintScreen = 30,
-
- ///
- /// The Insert key.
- ///
- Insert = 31,
-
- ///
- /// The Delete key.
- ///
- Delete = 32,
-
- ///
- /// The Help key.
- ///
- Help = 33,
-
- ///
- /// The 0 key.
- ///
- D0 = 34,
-
- ///
- /// The 1 key.
- ///
- D1 = 35,
-
- ///
- /// The 2 key.
- ///
- D2 = 36,
-
- ///
- /// The 3 key.
- ///
- D3 = 37,
-
- ///
- /// The 4 key.
- ///
- D4 = 38,
-
- ///
- /// The 5 key.
- ///
- D5 = 39,
-
- ///
- /// The 6 key.
- ///
- D6 = 40,
-
- ///
- /// The 7 key.
- ///
- D7 = 41,
-
- ///
- /// The 8 key.
- ///
- D8 = 42,
-
- ///
- /// The 9 key.
- ///
- D9 = 43,
-
- ///
- /// The A key.
- ///
- A = 44,
-
- ///
- /// The B key.
- ///
- B = 45,
-
- ///
- /// The C key.
- ///
- C = 46,
-
- ///
- /// The D key.
- ///
- D = 47,
-
- ///
- /// The E key.
- ///
- E = 48,
-
- ///
- /// The F key.
- ///
- F = 49,
-
- ///
- /// The G key.
- ///
- G = 50,
-
- ///
- /// The H key.
- ///
- H = 51,
-
- ///
- /// The I key.
- ///
- I = 52,
-
- ///
- /// The J key.
- ///
- J = 53,
-
- ///
- /// The K key.
- ///
- AvnKeyK = 54,
-
- ///
- /// The L key.
- ///
- L = 55,
-
- ///
- /// The M key.
- ///
- M = 56,
-
- ///
- /// The N key.
- ///
- N = 57,
-
- ///
- /// The O key.
- ///
- O = 58,
-
- ///
- /// The P key.
- ///
- P = 59,
-
- ///
- /// The Q key.
- ///
- Q = 60,
-
- ///
- /// The R key.
- ///
- R = 61,
-
- ///
- /// The S key.
- ///
- S = 62,
-
- ///
- /// The T key.
- ///
- T = 63,
-
- ///
- /// The U key.
- ///
- U = 64,
-
- ///
- /// The V key.
- ///
- V = 65,
-
- ///
- /// The W key.
- ///
- W = 66,
-
- ///
- /// The X key.
- ///
- X = 67,
-
- ///
- /// The Y key.
- ///
- Y = 68,
-
- ///
- /// The Z key.
- ///
- Z = 69,
-
- ///
- /// The left Windows key.
- ///
- LWin = 70,
-
- ///
- /// The right Windows key.
- ///
- RWin = 71,
-
- ///
- /// The Application key.
- ///
- Apps = 72,
-
- ///
- /// The Sleep key.
- ///
- Sleep = 73,
-
- ///
- /// The 0 key on the numeric keypad.
- ///
- NumPad0 = 74,
-
- ///
- /// The 1 key on the numeric keypad.
- ///
- NumPad1 = 75,
-
- ///
- /// The 2 key on the numeric keypad.
- ///
- NumPad2 = 76,
-
- ///
- /// The 3 key on the numeric keypad.
- ///
- NumPad3 = 77,
-
- ///
- /// The 4 key on the numeric keypad.
- ///
- NumPad4 = 78,
-
- ///
- /// The 5 key on the numeric keypad.
- ///
- NumPad5 = 79,
-
- ///
- /// The 6 key on the numeric keypad.
- ///
- NumPad6 = 80,
-
- ///
- /// The 7 key on the numeric keypad.
- ///
- NumPad7 = 81,
-
- ///
- /// The 8 key on the numeric keypad.
- ///
- NumPad8 = 82,
-
- ///
- /// The 9 key on the numeric keypad.
- ///
- NumPad9 = 83,
-
- ///
- /// The Multiply key.
- ///
- Multiply = 84,
-
- ///
- /// The Add key.
- ///
- Add = 85,
-
- ///
- /// The Separator key.
- ///
- Separator = 86,
-
- ///
- /// The Subtract key.
- ///
- Subtract = 87,
-
- ///
- /// The Decimal key.
- ///
- Decimal = 88,
-
- ///
- /// The Divide key.
- ///
- Divide = 89,
-
- ///
- /// The F1 key.
- ///
- F1 = 90,
-
- ///
- /// The F2 key.
- ///
- F2 = 91,
-
- ///
- /// The F3 key.
- ///
- F3 = 92,
-
- ///
- /// The F4 key.
- ///
- F4 = 93,
-
- ///
- /// The F5 key.
- ///
- F5 = 94,
-
- ///
- /// The F6 key.
- ///
- F6 = 95,
-
- ///
- /// The F7 key.
- ///
- F7 = 96,
-
- ///
- /// The F8 key.
- ///
- F8 = 97,
-
- ///
- /// The F9 key.
- ///
- F9 = 98,
-
- ///
- /// The F10 key.
- ///
- F10 = 99,
-
- ///
- /// The F11 key.
- ///
- F11 = 100,
-
- ///
- /// The F12 key.
- ///
- F12 = 101,
-
- ///
- /// The F13 key.
- ///
- F13 = 102,
-
- ///
- /// The F14 key.
- ///
- F14 = 103,
-
- ///
- /// The F15 key.
- ///
- F15 = 104,
-
- ///
- /// The F16 key.
- ///
- F16 = 105,
-
- ///
- /// The F17 key.
- ///
- F17 = 106,
-
- ///
- /// The F18 key.
- ///
- F18 = 107,
-
- ///
- /// The F19 key.
- ///
- F19 = 108,
-
- ///
- /// The F20 key.
- ///
- F20 = 109,
-
- ///
- /// The F21 key.
- ///
- F21 = 110,
-
- ///
- /// The F22 key.
- ///
- F22 = 111,
-
- ///
- /// The F23 key.
- ///
- F23 = 112,
-
- ///
- /// The F24 key.
- ///
- F24 = 113,
-
- ///
- /// The Numlock key.
- ///
- NumLock = 114,
-
- ///
- /// The Scroll key.
- ///
- Scroll = 115,
-
- ///
- /// The left Shift key.
- ///
- LeftShift = 116,
-
- ///
- /// The right Shift key.
- ///
- RightShift = 117,
-
- ///
- /// The left Ctrl key.
- ///
- LeftCtrl = 118,
-
- ///
- /// The right Ctrl key.
- ///
- RightCtrl = 119,
-
- ///
- /// The left Alt key.
- ///
- LeftAlt = 120,
-
- ///
- /// The right Alt key.
- ///
- RightAlt = 121,
-
- ///
- /// The browser Back key.
- ///
- BrowserBack = 122,
-
- ///
- /// The browser Forward key.
- ///
- BrowserForward = 123,
-
- ///
- /// The browser Refresh key.
- ///
- BrowserRefresh = 124,
-
- ///
- /// The browser Stop key.
- ///
- BrowserStop = 125,
-
- ///
- /// The browser Search key.
- ///
- BrowserSearch = 126,
-
- ///
- /// The browser Favorites key.
- ///
- BrowserFavorites = 127,
-
- ///
- /// The browser Home key.
- ///
- BrowserHome = 128,
-
- ///
- /// The Volume Mute key.
- ///
- VolumeMute = 129,
-
- ///
- /// The Volume Down key.
- ///
- VolumeDown = 130,
-
- ///
- /// The Volume Up key.
- ///
- VolumeUp = 131,
-
- ///
- /// The media Next Track key.
- ///
- MediaNextTrack = 132,
-
- ///
- /// The media Previous Track key.
- ///
- MediaPreviousTrack = 133,
-
- ///
- /// The media Stop key.
- ///
- MediaStop = 134,
-
- ///
- /// The media Play/Pause key.
- ///
- MediaPlayPause = 135,
-
- ///
- /// The Launch Mail key.
- ///
- LaunchMail = 136,
-
- ///
- /// The Select Media key.
- ///
- SelectMedia = 137,
-
- ///
- /// The Launch Application 1 key.
- ///
- LaunchApplication1 = 138,
-
- ///
- /// The Launch Application 2 key.
- ///
- LaunchApplication2 = 139,
-
- ///
- /// The OEM Semicolon key.
- ///
- OemSemicolon = 140,
-
- ///
- /// The OEM 1 key.
- ///
- Oem1 = 140,
-
- ///
- /// The OEM Plus key.
- ///
- OemPlus = 141,
-
- ///
- /// The OEM Comma key.
- ///
- OemComma = 142,
-
- ///
- /// The OEM Minus key.
- ///
- OemMinus = 143,
-
- ///
- /// The OEM Period key.
- ///
- OemPeriod = 144,
-
- ///
- /// The OEM Question Mark key.
- ///
- OemQuestion = 145,
-
- ///
- /// The OEM 2 key.
- ///
- Oem2 = 145,
-
- ///
- /// The OEM Tilde key.
- ///
- OemTilde = 146,
-
- ///
- /// The OEM 3 key.
- ///
- Oem3 = 146,
-
- ///
- /// The ABNT_C1 (Brazilian) key.
- ///
- AbntC1 = 147,
-
- ///
- /// The ABNT_C2 (Brazilian) key.
- ///
- AbntC2 = 148,
-
- ///
- /// The OEM Open Brackets key.
- ///
- OemOpenBrackets = 149,
-
- ///
- /// The OEM 4 key.
- ///
- Oem4 = 149,
-
- ///
- /// The OEM Pipe key.
- ///
- OemPipe = 150,
-
- ///
- /// The OEM 5 key.
- ///
- Oem5 = 150,
-
- ///
- /// The OEM Close Brackets key.
- ///
- OemCloseBrackets = 151,
-
- ///
- /// The OEM 6 key.
- ///
- Oem6 = 151,
-
- ///
- /// The OEM Quotes key.
- ///
- OemQuotes = 152,
-
- ///
- /// The OEM 7 key.
- ///
- Oem7 = 152,
-
- ///
- /// The OEM 8 key.
- ///
- Oem8 = 153,
-
- ///
- /// The OEM Backslash key.
- ///
- OemBackslash = 154,
-
- ///
- /// The OEM 3 key.
- ///
- Oem102 = 154,
-
- ///
- /// A special key masking the real key being processed by an IME.
- ///
- ImeProcessed = 155,
-
- ///
- /// A special key masking the real key being processed as a system key.
- ///
- System = 156,
-
- ///
- /// The OEM ATTN key.
- ///
- OemAttn = 157,
-
- ///
- /// The DBE_ALPHANUMERIC key.
- ///
- DbeAlphanumeric = 157,
-
- ///
- /// The OEM Finish key.
- ///
- OemFinish = 158,
-
- ///
- /// The DBE_KATAKANA key.
- ///
- DbeKatakana = 158,
-
- ///
- /// The DBE_HIRAGANA key.
- ///
- DbeHiragana = 159,
-
- ///
- /// The OEM Copy key.
- ///
- OemCopy = 159,
-
- ///
- /// The DBE_SBCSCHAR key.
- ///
- DbeSbcsChar = 160,
-
- ///
- /// The OEM Auto key.
- ///
- OemAuto = 160,
-
- ///
- /// The DBE_DBCSCHAR key.
- ///
- DbeDbcsChar = 161,
-
- ///
- /// The OEM ENLW key.
- ///
- OemEnlw = 161,
-
- ///
- /// The OEM BackTab key.
- ///
- OemBackTab = 162,
-
- ///
- /// The DBE_ROMAN key.
- ///
- DbeRoman = 162,
-
- ///
- /// The DBE_NOROMAN key.
- ///
- DbeNoRoman = 163,
-
- ///
- /// The ATTN key.
- ///
- Attn = 163,
-
- ///
- /// The CRSEL key.
- ///
- CrSel = 164,
-
- ///
- /// The DBE_ENTERWORDREGISTERMODE key.
- ///
- DbeEnterWordRegisterMode = 164,
-
- ///
- /// The EXSEL key.
- ///
- ExSel = 165,
-
- ///
- /// The DBE_ENTERIMECONFIGMODE key.
- ///
- DbeEnterImeConfigureMode = 165,
-
- ///
- /// The ERASE EOF Key.
- ///
- EraseEof = 166,
-
- ///
- /// The DBE_FLUSHSTRING key.
- ///
- DbeFlushString = 166,
-
- ///
- /// The Play key.
- ///
- Play = 167,
-
- ///
- /// The DBE_CODEINPUT key.
- ///
- DbeCodeInput = 167,
-
- ///
- /// The DBE_NOCODEINPUT key.
- ///
- DbeNoCodeInput = 168,
-
- ///
- /// The Zoom key.
- ///
- Zoom = 168,
-
- ///
- /// Reserved for future use.
- ///
- NoName = 169,
-
- ///
- /// The DBE_DETERMINESTRING key.
- ///
- DbeDetermineString = 169,
-
- ///
- /// The DBE_ENTERDLGCONVERSIONMODE key.
- ///
- DbeEnterDialogConversionMode = 170,
-
- ///
- /// The PA1 key.
- ///
- Pa1 = 170,
-
- ///
- /// The OEM Clear key.
- ///
- OemClear = 171,
-
- ///
- /// The key is used with another key to create a single combined character.
- ///
- DeadCharProcessed = 172,
-};
-
-#endif
diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
index d5cad4d1ca..dba3ee6d31 100644
--- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
+++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
@@ -8,19 +8,20 @@
/* Begin PBXBuildFile section */
1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; };
+ 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; };
1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; };
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; };
- 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; };
- 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; };
1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; };
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; };
1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; };
+ 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; };
37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; };
37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; };
37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; };
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; };
520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; };
+ 522D5959258159C1006F7F7A /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 522D5958258159C1006F7F7A /* Carbon.framework */; };
5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; };
@@ -32,13 +33,13 @@
/* Begin PBXFileReference section */
1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; };
+ 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; };
1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = ""; };
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; };
- 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; };
- 1AFD334023E03C4F0042899B /* controlhost.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = controlhost.mm; sourceTree = ""; };
1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = ""; };
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = ""; };
+ 1AFD334023E03C4F0042899B /* controlhost.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = controlhost.mm; sourceTree = ""; };
37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; };
379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; };
37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; };
@@ -49,6 +50,7 @@
37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; };
37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; };
520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; };
+ 522D5958258159C1006F7F7A /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; };
5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; };
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; };
5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; };
@@ -69,6 +71,7 @@
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */,
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */,
AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */,
+ 522D5959258159C1006F7F7A /* Carbon.framework in Frameworks */,
AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -79,6 +82,7 @@
AB661C1C2148230E00291242 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 522D5958258159C1006F7F7A /* Carbon.framework */,
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */,
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */,
AB1E522B217613570091CD71 /* OpenGL.framework */,
diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h
index 5d299374e5..3ce83d370a 100644
--- a/native/Avalonia.Native/src/OSX/AvnString.h
+++ b/native/Avalonia.Native/src/OSX/AvnString.h
@@ -11,6 +11,7 @@
extern IAvnString* CreateAvnString(NSString* string);
extern IAvnStringArray* CreateAvnStringArray(NSArray* array);
+extern IAvnStringArray* CreateAvnStringArray(NSArray* array);
extern IAvnStringArray* CreateAvnStringArray(NSString* string);
extern IAvnString* CreateByteArray(void* data, int len);
#endif /* AvnString_h */
diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm
index 00b748ef63..001cf151d8 100644
--- a/native/Avalonia.Native/src/OSX/AvnString.mm
+++ b/native/Avalonia.Native/src/OSX/AvnString.mm
@@ -85,6 +85,16 @@ public:
}
}
+ AvnStringArrayImpl(NSArray* array)
+ {
+ for(int c = 0; c < [array count]; c++)
+ {
+ ComPtr s;
+ *s.getPPV() = new AvnStringImpl([array objectAtIndex:c].absoluteString);
+ _list.push_back(s);
+ }
+ }
+
AvnStringArrayImpl(NSString* string)
{
ComPtr s;
@@ -117,6 +127,11 @@ IAvnStringArray* CreateAvnStringArray(NSArray * array)
return new AvnStringArrayImpl(array);
}
+IAvnStringArray* CreateAvnStringArray(NSArray * array)
+{
+ return new AvnStringArrayImpl(array);
+}
+
IAvnStringArray* CreateAvnStringArray(NSString* string)
{
return new AvnStringArrayImpl(string);
diff --git a/native/Avalonia.Native/src/OSX/KeyTransform.h b/native/Avalonia.Native/src/OSX/KeyTransform.h
index ea4fbecd5c..2f434570c9 100644
--- a/native/Avalonia.Native/src/OSX/KeyTransform.h
+++ b/native/Avalonia.Native/src/OSX/KeyTransform.h
@@ -1,9 +1,14 @@
#ifndef keytransform_h
#define keytransform_h
#include "common.h"
-#include "key.h"
#include
+
true
- build\
+ build\;buildTransitive\
true
- build\
+ build\;buildTransitive\
true
- build\
+ build\;buildTransitive\
+
diff --git a/readme.md b/readme.md
index 726a634e1a..67b706f428 100644
--- a/readme.md
+++ b/readme.md
@@ -2,15 +2,13 @@
[](https://www.nuget.org/packages/Avalonia) [](https://www.nuget.org/packages/Avalonia) [](https://www.myget.org/gallery/avalonia-ci) 
-
-
## 📖 About AvaloniaUI
Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
-> **Note:** The UI theme you see in the picture above is still work-in-progress and will be available in the upcoming Avalonia 0.10.0 release. However, you can connect to our nightly build feed and install latest pre-release versions of Avalonia NuGet packages, if you are willing to help out with the development and testing. See [Using nightly build feed](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) for more info.
+([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery))
To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. [Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
@@ -31,15 +29,22 @@ Install-Package Avalonia.Desktop
Examples of UIs built with Avalonia

+([Synfonia](https://github.com/jmacato/Synfonia))
+

+([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery))

+([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery))

+([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery))
## JetBrains Rider
-If you need to develop Avalonia app with JetBrains Rider you can use latest Rider [preview builds](https://www.jetbrains.com/rider/nextversion/).
+[JetBrains Rider](https://www.jetbrains.com/rider/whatsnew/?mkt_tok=eyJpIjoiTURBNU1HSmhNV0kwTUdFMiIsInQiOiJtNnU2VEc1TlNLa1ZRVkROYmdZYVpYREJsaU1qdUhmS3dxSzRHczdYWHl0RVlTNDMwSFwvNUs3VENTNVM0bVcyNFdaRmVYZzVWTTF1N3VrQWNGTkJreEhlam1hMlB4UVVWcHBGM1dNOUxoXC95YnRQdGgyUXl1YmZCM3h3d3BVWWdBIn0%3D#avalonia-support) now has official support for Avalonia.
+
+Code completion, inspections and refactorings are supported out of the box, for XAML previewer add `https://plugins.jetbrains.com/plugins/dev/14839` to plugin repositories and install [AvaloniaRider](https://github.com/ForNeVeR/AvaloniaRider) plugin.
## Bleeding Edge Builds
diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
index 5b82e2caee..1a68c4d732 100644
--- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
+++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
@@ -16,7 +16,7 @@
Resources\Resource.Designer.cs
Off
False
- v9.0
+ v11.0
Properties\AndroidManifest.xml
@@ -71,21 +71,23 @@
+
-
- Designer
-
+
-
+
+
-
+
+ Resources\drawable\Icon.png
+
diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs
index 40d001a195..2ab03551b6 100644
--- a/samples/ControlCatalog.Android/MainActivity.cs
+++ b/samples/ControlCatalog.Android/MainActivity.cs
@@ -1,31 +1,18 @@
-using System;
-using Android.App;
+using Android.App;
using Android.OS;
using Android.Content.PM;
using Avalonia.Android;
-using Avalonia.Controls;
-using Avalonia.Controls.Templates;
-using Avalonia.Markup.Xaml;
-using Avalonia.Media;
-using Avalonia.Styling;
-using Avalonia.Themes.Default;
-using Avalonia;
namespace ControlCatalog.Android
{
- [Activity(Label = "ControlCatalog.Android", MainLauncher = true, Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance)]
+ [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance)]
public class MainActivity : AvaloniaActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
- if (Avalonia.Application.Current == null)
- {
- AppBuilder.Configure()
- .UseAndroid()
- .SetupWithoutStarting();
- Content = new MainView();
- }
base.OnCreate(savedInstanceState);
+
+ Content = new MainView();
}
}
}
diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
index e39ec39f1c..9effda7e79 100644
--- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
+++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs
index 96f0e76fd8..b1ca548e2c 100644
--- a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs
+++ b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs
@@ -2,7 +2,6 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -15,7 +14,7 @@ namespace ControlCatalog.Android
{
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
public partial class Resource
{
@@ -26,8 +25,6 @@ namespace ControlCatalog.Android
public static void UpdateIdValues()
{
- global::Avalonia.Android.Resource.String.ApplicationName = global::ControlCatalog.Android.Resource.String.ApplicationName;
- global::Avalonia.Android.Resource.String.Hello = global::ControlCatalog.Android.Resource.String.Hello;
}
public partial class Attribute
@@ -43,69 +40,59 @@ namespace ControlCatalog.Android
}
}
- public partial class Drawable
+ public partial class Color
{
- // aapt resource value: 0x7f020000
- public const int Icon = 2130837504;
+ // aapt resource value: 0x7F010000
+ public const int splash_background = 2130771968;
- static Drawable()
+ static Color()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
- private Drawable()
+ private Color()
{
}
}
- public partial class Id
+ public partial class Drawable
{
- // aapt resource value: 0x7f050000
- public const int MyButton = 2131034112;
-
- static Id()
- {
- global::Android.Runtime.ResourceIdManager.UpdateIdValues();
- }
-
- private Id()
- {
- }
- }
-
- public partial class Layout
- {
+ // aapt resource value: 0x7F020000
+ public const int Icon = 2130837504;
- // aapt resource value: 0x7f030000
- public const int Main = 2130903040;
+ // aapt resource value: 0x7F020001
+ public const int splash_screen = 2130837505;
- static Layout()
+ static Drawable()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
- private Layout()
+ private Drawable()
{
}
}
- public partial class String
+ public partial class Style
{
- // aapt resource value: 0x7f040001
- public const int ApplicationName = 2130968577;
+ // aapt resource value: 0x7F030000
+ public const int MyTheme = 2130903040;
+
+ // aapt resource value: 0x7F030001
+ public const int MyTheme_NoActionBar = 2130903041;
- // aapt resource value: 0x7f040000
- public const int Hello = 2130968576;
+ // aapt resource value: 0x7F030002
+ public const int MyTheme_Splash = 2130903042;
- static String()
+ static Style()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
- private String()
+ private Style()
{
}
}
diff --git a/samples/ControlCatalog.Android/Resources/drawable/Icon.png b/samples/ControlCatalog.Android/Resources/drawable/Icon.png
deleted file mode 100644
index 8074c4c571..0000000000
Binary files a/samples/ControlCatalog.Android/Resources/drawable/Icon.png and /dev/null differ
diff --git a/samples/ControlCatalog.Android/Resources/drawable/splash_screen.xml b/samples/ControlCatalog.Android/Resources/drawable/splash_screen.xml
new file mode 100644
index 0000000000..2e920b4b3b
--- /dev/null
+++ b/samples/ControlCatalog.Android/Resources/drawable/splash_screen.xml
@@ -0,0 +1,13 @@
+
+
+
+ -
+
+
+
+
+
+
diff --git a/samples/ControlCatalog.Android/Resources/layout/Main.axml b/samples/ControlCatalog.Android/Resources/layout/Main.axml
deleted file mode 100644
index 570c96ad72..0000000000
--- a/samples/ControlCatalog.Android/Resources/layout/Main.axml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/samples/ControlCatalog.Android/Resources/values/Strings.xml b/samples/ControlCatalog.Android/Resources/values/Strings.xml
deleted file mode 100644
index 95221a08a9..0000000000
--- a/samples/ControlCatalog.Android/Resources/values/Strings.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- Hello World, Click Me!
- ControlCatalog.Android
-
diff --git a/samples/ControlCatalog.Android/Resources/values/colors.xml b/samples/ControlCatalog.Android/Resources/values/colors.xml
new file mode 100644
index 0000000000..59279d5d32
--- /dev/null
+++ b/samples/ControlCatalog.Android/Resources/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
diff --git a/samples/ControlCatalog.Android/Resources/values/styles.xml b/samples/ControlCatalog.Android/Resources/values/styles.xml
new file mode 100644
index 0000000000..e017b6facf
--- /dev/null
+++ b/samples/ControlCatalog.Android/Resources/values/styles.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog.Android/SplashActivity.cs b/samples/ControlCatalog.Android/SplashActivity.cs
new file mode 100644
index 0000000000..6d7c6bc116
--- /dev/null
+++ b/samples/ControlCatalog.Android/SplashActivity.cs
@@ -0,0 +1,32 @@
+using Android.App;
+using Android.Content;
+using Android.OS;
+using Application = Android.App.Application;
+
+using Avalonia;
+
+namespace ControlCatalog.Android
+{
+ [Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)]
+ public class SplashActivity : Activity
+ {
+ protected override void OnCreate(Bundle savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+ }
+
+ protected override void OnResume()
+ {
+ base.OnResume();
+
+ if (Avalonia.Application.Current == null)
+ {
+ AppBuilder.Configure()
+ .UseAndroid()
+ .SetupWithoutStarting();
+ }
+
+ StartActivity(new Intent(Application.Context, typeof(MainActivity)));
+ }
+ }
+}
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index d5aedf7783..3c2d2ee359 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -1,7 +1,7 @@
- Exe
+ WinExe
netcoreapp3.1
true
@@ -15,6 +15,10 @@
+
+
+ en
+
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 1dc8c09c0e..0c8fd9465c 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -109,12 +109,12 @@ namespace ControlCatalog.NetCore
.With(new X11PlatformOptions
{
EnableMultiTouch = true,
- UseDBusMenu = true
+ UseDBusMenu = true,
+ EnableIme = true,
})
.With(new Win32PlatformOptions
{
- EnableMultitouch = true,
- AllowEglInitialization = true
+ EnableMultitouch = true
})
.UseSkia()
.UseManagedSystemDialogs()
diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs
index 020fb2fff3..f3ec7b48aa 100644
--- a/samples/ControlCatalog/App.xaml.cs
+++ b/samples/ControlCatalog/App.xaml.cs
@@ -39,6 +39,10 @@ namespace ControlCatalog
public static Styles DefaultLight = new Styles
{
+ new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
+ {
+ Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
+ },
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
@@ -60,6 +64,10 @@ namespace ControlCatalog
public static Styles DefaultDark = new Styles
{
+ new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
+ {
+ Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
+ },
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
diff --git a/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf b/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf
new file mode 100644
index 0000000000..61e2583a6c
Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf differ
diff --git a/samples/ControlCatalog/Assets/avalonia-32.png b/samples/ControlCatalog/Assets/avalonia-32.png
new file mode 100644
index 0000000000..7b443e7a25
Binary files /dev/null and b/samples/ControlCatalog/Assets/avalonia-32.png differ
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 1aa926a2a6..53ad213d92 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -27,6 +27,6 @@
-
+
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index f001425964..6537c470d5 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -21,7 +21,14 @@
+
+
+
+
+
+
@@ -34,6 +41,9 @@
+
+
+
diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml
index 6a70bb082f..a107ee2163 100644
--- a/samples/ControlCatalog/MainWindow.xaml
+++ b/samples/ControlCatalog/MainWindow.xaml
@@ -18,11 +18,11 @@
-
+
-
+
diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml
index 96cfcc5288..7635e1ccc3 100644
--- a/samples/ControlCatalog/Pages/AcrylicPage.xaml
+++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml
@@ -16,13 +16,13 @@
-
-
+
+
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
new file mode 100644
index 0000000000..e15637aa0f
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ Context Flyout
+ A right click Flyout that can be applied to any control.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs
new file mode 100644
index 0000000000..e64d4a2cdd
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs
@@ -0,0 +1,45 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+using Avalonia.Interactivity;
+namespace ControlCatalog.Pages
+{
+ public class ContextFlyoutPage : UserControl
+ {
+ private TextBox _textBox;
+
+ public ContextFlyoutPage()
+ {
+ InitializeComponent();
+
+ var vm = new ContextFlyoutPageViewModel();
+ vm.View = this;
+ DataContext = vm;
+
+ _textBox = this.FindControl("TextBox");
+
+ var cutButton = this.FindControl
diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
index ad2cec2ae3..5f33cadf2e 100644
--- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
+++ b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
@@ -4,7 +4,6 @@ using Android.Content.PM;
using Android.OS;
using Avalonia.Android;
using Avalonia.Controls;
-using Avalonia.Controls.Templates;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Styling;
@@ -17,18 +16,18 @@ namespace Avalonia.AndroidTestApplication
Icon = "@drawable/icon",
LaunchMode = LaunchMode.SingleInstance/*,
ScreenOrientation = ScreenOrientation.Landscape*/)]
- public class MainBaseActivity : Activity
+ public class MainBaseActivity : AvaloniaActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
- base.OnCreate(savedInstanceState);
if (Avalonia.Application.Current == null)
{
AppBuilder.Configure()
.UseAndroid()
.SetupWithoutStarting();
}
- SetContentView(new AvaloniaView(this) { Content = App.CreateSimpleWindow() });
+ base.OnCreate(savedInstanceState);
+ Content = App.CreateSimpleWindow();
}
}
@@ -38,8 +37,7 @@ namespace Avalonia.AndroidTestApplication
{
Styles.Add(new DefaultTheme());
- var loader = new AvaloniaXamlLoader();
- var baseLight = (IStyle)loader.Load(
+ var baseLight = (IStyle)AvaloniaXamlLoader.Load(
new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"));
Styles.Add(baseLight);
@@ -74,13 +72,33 @@ namespace Avalonia.AndroidTestApplication
Height = 40,
Background = Brushes.LightGreen,
Foreground = Brushes.Black
- }
+ },
+ CreateTextBox(Input.TextInput.TextInputContentType.Normal),
+ CreateTextBox(Input.TextInput.TextInputContentType.Password),
+ CreateTextBox(Input.TextInput.TextInputContentType.Email),
+ CreateTextBox(Input.TextInput.TextInputContentType.Url),
+ CreateTextBox(Input.TextInput.TextInputContentType.Phone),
+ CreateTextBox(Input.TextInput.TextInputContentType.Number),
}
}
};
return window;
}
+
+ private static TextBox CreateTextBox(Input.TextInput.TextInputContentType contentType)
+ {
+ var textBox = new TextBox()
+ {
+ Margin = new Thickness(20, 10),
+ Watermark = contentType.ToString(),
+ BorderThickness = new Thickness(3),
+ FontSize = 20
+ };
+ textBox.TextInputOptionsQuery += (s, e) => { e.ContentType = contentType; };
+
+ return textBox;
+ }
}
}
diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
index 4792c8a1ec..57ee503005 100644
--- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
+++ b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs
index e171dd6162..83db67fcee 100644
--- a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs
+++ b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs
@@ -2,7 +2,6 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -15,7 +14,7 @@ namespace Avalonia.AndroidTestApplication
{
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
public partial class Resource
{
@@ -26,8 +25,6 @@ namespace Avalonia.AndroidTestApplication
public static void UpdateIdValues()
{
- global::Avalonia.Android.Resource.String.ApplicationName = global::Avalonia.AndroidTestApplication.Resource.String.ApplicationName;
- global::Avalonia.Android.Resource.String.Hello = global::Avalonia.AndroidTestApplication.Resource.String.Hello;
}
public partial class Attribute
@@ -46,8 +43,8 @@ namespace Avalonia.AndroidTestApplication
public partial class Drawable
{
- // aapt resource value: 0x7f020000
- public const int Icon = 2130837504;
+ // aapt resource value: 0x7F010000
+ public const int Icon = 2130771968;
static Drawable()
{
@@ -62,11 +59,11 @@ namespace Avalonia.AndroidTestApplication
public partial class String
{
- // aapt resource value: 0x7f030001
- public const int ApplicationName = 2130903041;
+ // aapt resource value: 0x7F020000
+ public const int ApplicationName = 2130837504;
- // aapt resource value: 0x7f030000
- public const int Hello = 2130903040;
+ // aapt resource value: 0x7F020001
+ public const int Hello = 2130837505;
static String()
{
diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs
index 067d9f462f..a415046513 100644
--- a/src/Avalonia.Animation/Animatable.cs
+++ b/src/Avalonia.Animation/Animatable.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Linq;
using Avalonia.Data;
#nullable enable
@@ -93,16 +94,35 @@ namespace Avalonia.Animation
var oldTransitions = change.OldValue.GetValueOrDefault();
var newTransitions = change.NewValue.GetValueOrDefault();
+ // When transitions are replaced, we add the new transitions before removing the old
+ // transitions, so that when the old transition being disposed causes the value to
+ // change, there is a corresponding entry in `_transitionStates`. This means that we
+ // need to account for any transitions present in both the old and new transitions
+ // collections.
if (newTransitions is object)
{
+ var toAdd = (IList)newTransitions;
+
+ if (newTransitions.Count > 0 && oldTransitions?.Count > 0)
+ {
+ toAdd = newTransitions.Except(oldTransitions).ToList();
+ }
+
newTransitions.CollectionChanged += TransitionsCollectionChanged;
- AddTransitions(newTransitions);
+ AddTransitions(toAdd);
}
if (oldTransitions is object)
{
+ var toRemove = (IList)oldTransitions;
+
+ if (oldTransitions.Count > 0 && newTransitions?.Count > 0)
+ {
+ toRemove = oldTransitions.Except(newTransitions).ToList();
+ }
+
oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
- RemoveTransitions(oldTransitions);
+ RemoveTransitions(toRemove);
}
}
else if (_transitionsEnabled &&
@@ -115,9 +135,9 @@ namespace Avalonia.Animation
{
var transition = Transitions[i];
- if (transition.Property == change.Property)
+ if (transition.Property == change.Property &&
+ _transitionState.TryGetValue(transition, out var state))
{
- var state = _transitionState[transition];
var oldValue = state.BaseValue;
var newValue = GetAnimationBaseValue(transition.Property);
diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs
index ca1d97290e..c42153ec4f 100644
--- a/src/Avalonia.Animation/Animation.cs
+++ b/src/Avalonia.Animation/Animation.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Animation
///
public static readonly DirectProperty DurationProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_duration),
+ nameof(Duration),
o => o._duration,
(o, v) => o._duration = v);
@@ -31,7 +31,7 @@ namespace Avalonia.Animation
///
public static readonly DirectProperty IterationCountProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_iterationCount),
+ nameof(IterationCount),
o => o._iterationCount,
(o, v) => o._iterationCount = v);
@@ -40,7 +40,7 @@ namespace Avalonia.Animation
///
public static readonly DirectProperty PlaybackDirectionProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_playbackDirection),
+ nameof(PlaybackDirection),
o => o._playbackDirection,
(o, v) => o._playbackDirection = v);
@@ -49,7 +49,7 @@ namespace Avalonia.Animation
///
public static readonly DirectProperty FillModeProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_fillMode),
+ nameof(FillMode),
o => o._fillMode,
(o, v) => o._fillMode = v);
@@ -58,7 +58,7 @@ namespace Avalonia.Animation
///
public static readonly DirectProperty EasingProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_easing),
+ nameof(Easing),
o => o._easing,
(o, v) => o._easing = v);
@@ -67,7 +67,7 @@ namespace Avalonia.Animation
///
public static readonly DirectProperty DelayProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_delay),
+ nameof(Delay),
o => o._delay,
(o, v) => o._delay = v);
@@ -76,7 +76,7 @@ namespace Avalonia.Animation
///
public static readonly DirectProperty DelayBetweenIterationsProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_delayBetweenIterations),
+ nameof(DelayBetweenIterations),
o => o._delayBetweenIterations,
(o, v) => o._delayBetweenIterations = v);
@@ -85,7 +85,7 @@ namespace Avalonia.Animation
///
public static readonly DirectProperty SpeedRatioProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_speedRatio),
+ nameof(SpeedRatio),
o => o._speedRatio,
(o, v) => o._speedRatio = v,
defaultBindingMode: BindingMode.TwoWay);
@@ -209,6 +209,17 @@ namespace Avalonia.Animation
( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator) ),
};
+ ///
+ /// Registers a that can handle
+ /// a value type that matches the specified condition.
+ ///
+ ///
+ /// The condition to which the
+ /// is to be activated and used.
+ ///
+ ///
+ /// The type of the animator to instantiate.
+ ///
public static void RegisterAnimator(Func condition)
where TAnimator : IAnimator
{
diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs
index 0660440e30..d784227620 100644
--- a/src/Avalonia.Animation/Animators/Animator`1.cs
+++ b/src/Avalonia.Animation/Animators/Animator`1.cs
@@ -104,6 +104,11 @@ namespace Avalonia.Animation.Animators
throw new Exception("Index time is out of keyframe time range.");
}
+ public virtual IDisposable BindAnimation(Animatable control, IObservable instance)
+ {
+ return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
+ }
+
///
/// Runs the KeyFrames Animation.
///
@@ -116,7 +121,8 @@ namespace Avalonia.Animation.Animators
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
InterpolationHandler);
- return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
+
+ return BindAnimation(control, instance);
}
///
diff --git a/src/Avalonia.Animation/Transitions/DoubleTransition.cs b/src/Avalonia.Animation/Transitions/DoubleTransition.cs
index 8cae1e1f81..d5bb1aac20 100644
--- a/src/Avalonia.Animation/Transitions/DoubleTransition.cs
+++ b/src/Avalonia.Animation/Transitions/DoubleTransition.cs
@@ -1,6 +1,8 @@
using System;
using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
+
namespace Avalonia.Animation
{
///
@@ -8,15 +10,13 @@ namespace Avalonia.Animation
///
public class DoubleTransition : Transition
{
+ private static readonly DoubleAnimator s_animator = new DoubleAnimator();
+
///
public override IObservable DoTransition(IObservable progress, double oldValue, double newValue)
{
return progress
- .Select(p =>
- {
- var f = Easing.Ease(p);
- return ((newValue - oldValue) * f) + oldValue;
- });
+ .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}
diff --git a/src/Avalonia.Animation/Transitions/FloatTransition.cs b/src/Avalonia.Animation/Transitions/FloatTransition.cs
index 427563e559..37b644fa96 100644
--- a/src/Avalonia.Animation/Transitions/FloatTransition.cs
+++ b/src/Avalonia.Animation/Transitions/FloatTransition.cs
@@ -1,6 +1,8 @@
using System;
using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
+
namespace Avalonia.Animation
{
///
@@ -8,12 +10,13 @@ namespace Avalonia.Animation
///
public class FloatTransition : Transition
{
+ private static readonly FloatAnimator s_animator = new FloatAnimator();
+
///
public override IObservable DoTransition(IObservable progress, float oldValue, float newValue)
{
- var delta = newValue - oldValue;
return progress
- .Select(p => (float)Easing.Ease(p) * delta + oldValue);
+ .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}
diff --git a/src/Avalonia.Animation/Transitions/IntegerTransition.cs b/src/Avalonia.Animation/Transitions/IntegerTransition.cs
index 7a85bd75dc..223b2ba531 100644
--- a/src/Avalonia.Animation/Transitions/IntegerTransition.cs
+++ b/src/Avalonia.Animation/Transitions/IntegerTransition.cs
@@ -1,6 +1,8 @@
using System;
using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
+
namespace Avalonia.Animation
{
///
@@ -8,12 +10,13 @@ namespace Avalonia.Animation
///
public class IntegerTransition : Transition
{
+ private static readonly Int32Animator s_animator = new Int32Animator();
+
///
public override IObservable DoTransition(IObservable progress, int oldValue, int newValue)
{
- var delta = newValue - oldValue;
return progress
- .Select(p => (int)(Easing.Ease(p) * delta + oldValue));
+ .Select(progress => s_animator.Interpolate(Easing.Ease(progress), oldValue, newValue));
}
}
}
diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt
deleted file mode 100644
index d17f25ec49..0000000000
--- a/src/Avalonia.Base/ApiCompatBaseline.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Compat issues with assembly Avalonia.Base:
-MembersMustExist : Member 'public void Avalonia.Threading.AvaloniaSynchronizationContext..ctor(Avalonia.Threading.AvaloniaSynchronizationContext.INonPumpingPlatformWaitProvider)' does not exist in the implementation but it does exist in the contract.
-TypesMustExist : Type 'Avalonia.Threading.AvaloniaSynchronizationContext.INonPumpingPlatformWaitProvider' does not exist in the implementation but it does exist in the contract.
-Total Issues: 2
diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index 9bfd387ca9..0470dbfe89 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -5,9 +5,6 @@
Avalonia
True
-
-
-
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 6645d25b5d..fdd688cf9d 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -23,7 +23,7 @@ namespace Avalonia
private EventHandler _propertyChanged;
private List _inheritanceChildren;
private ValueStore _values;
- private ValueStore Values => _values ?? (_values = new ValueStore(this));
+ private bool _batchUpdate;
///
/// Initializes a new instance of the class.
@@ -117,6 +117,22 @@ namespace Avalonia
set { this.Bind(binding.Property, value); }
}
+ private ValueStore Values
+ {
+ get
+ {
+ if (_values is null)
+ {
+ _values = new ValueStore(this);
+
+ if (_batchUpdate)
+ _values.BeginBatchUpdate();
+ }
+
+ return _values;
+ }
+ }
+
public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
@@ -434,6 +450,28 @@ namespace Avalonia
_values?.CoerceValue(property);
}
+ public void BeginBatchUpdate()
+ {
+ if (_batchUpdate)
+ {
+ throw new InvalidOperationException("Batch update already in progress.");
+ }
+
+ _batchUpdate = true;
+ _values?.BeginBatchUpdate();
+ }
+
+ public void EndBatchUpdate()
+ {
+ if (!_batchUpdate)
+ {
+ throw new InvalidOperationException("No batch update in progress.");
+ }
+
+ _batchUpdate = false;
+ _values?.EndBatchUpdate();
+ }
+
///
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
{
diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs
index 3ae0445e9b..5117fdb170 100644
--- a/src/Avalonia.Base/AvaloniaProperty.cs
+++ b/src/Avalonia.Base/AvaloniaProperty.cs
@@ -17,9 +17,9 @@ namespace Avalonia
public static readonly object UnsetValue = new UnsetValueType();
private static int s_nextId;
- private readonly PropertyMetadata _defaultMetadata;
- private readonly Dictionary _metadata;
- private readonly Dictionary _metadataCache = new Dictionary();
+ private readonly AvaloniaPropertyMetadata _defaultMetadata;
+ private readonly Dictionary _metadata;
+ private readonly Dictionary _metadataCache = new Dictionary();
private bool _hasMetadataOverrides;
@@ -35,7 +35,7 @@ namespace Avalonia
string name,
Type valueType,
Type ownerType,
- PropertyMetadata metadata,
+ AvaloniaPropertyMetadata metadata,
Action notifying = null)
{
Contract.Requires(name != null);
@@ -48,7 +48,7 @@ namespace Avalonia
throw new ArgumentException("'name' may not contain periods.");
}
- _metadata = new Dictionary();
+ _metadata = new Dictionary();
Name = name;
PropertyType = valueType;
@@ -69,12 +69,12 @@ namespace Avalonia
protected AvaloniaProperty(
AvaloniaProperty source,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
{
Contract.Requires(source != null);
Contract.Requires(ownerType != null);
- _metadata = new Dictionary();
+ _metadata = new Dictionary();
Name = source.Name;
PropertyType = source.PropertyType;
@@ -419,7 +419,7 @@ namespace Avalonia
///
/// The property metadata.
///
- public PropertyMetadata GetMetadata() where T : IAvaloniaObject
+ public AvaloniaPropertyMetadata GetMetadata() where T : IAvaloniaObject
{
return GetMetadata(typeof(T));
}
@@ -432,7 +432,7 @@ namespace Avalonia
/// The property metadata.
///
///
- public PropertyMetadata GetMetadata(Type type)
+ public AvaloniaPropertyMetadata GetMetadata(Type type)
{
if (!_hasMetadataOverrides)
{
@@ -521,7 +521,7 @@ namespace Avalonia
///
/// The type.
/// The metadata.
- protected void OverrideMetadata(Type type, PropertyMetadata metadata)
+ protected void OverrideMetadata(Type type, AvaloniaPropertyMetadata metadata)
{
Contract.Requires(type != null);
Contract.Requires(metadata != null);
@@ -542,14 +542,14 @@ namespace Avalonia
protected abstract IObservable GetChanged();
- private PropertyMetadata GetMetadataWithOverrides(Type type)
+ private AvaloniaPropertyMetadata GetMetadataWithOverrides(Type type)
{
if (type is null)
{
throw new ArgumentNullException(nameof(type));
}
- if (_metadataCache.TryGetValue(type, out PropertyMetadata result))
+ if (_metadataCache.TryGetValue(type, out AvaloniaPropertyMetadata result))
{
return result;
}
diff --git a/src/Avalonia.Base/PropertyMetadata.cs b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs
similarity index 85%
rename from src/Avalonia.Base/PropertyMetadata.cs
rename to src/Avalonia.Base/AvaloniaPropertyMetadata.cs
index 806051e1d1..2963567b14 100644
--- a/src/Avalonia.Base/PropertyMetadata.cs
+++ b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs
@@ -5,15 +5,15 @@ namespace Avalonia
///
/// Base class for avalonia property metadata.
///
- public class PropertyMetadata
+ public class AvaloniaPropertyMetadata
{
private BindingMode _defaultBindingMode;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The default binding mode.
- public PropertyMetadata(
+ public AvaloniaPropertyMetadata(
BindingMode defaultBindingMode = BindingMode.Default)
{
_defaultBindingMode = defaultBindingMode;
@@ -37,7 +37,7 @@ namespace Avalonia
/// The base metadata to merge.
/// The property to which the metadata is being applied.
public virtual void Merge(
- PropertyMetadata baseMetadata,
+ AvaloniaPropertyMetadata baseMetadata,
AvaloniaProperty property)
{
if (_defaultBindingMode == BindingMode.Default)
diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs
index d5549e979b..247ea0a25a 100644
--- a/src/Avalonia.Base/AvaloniaProperty`1.cs
+++ b/src/Avalonia.Base/AvaloniaProperty`1.cs
@@ -23,7 +23,7 @@ namespace Avalonia
protected AvaloniaProperty(
string name,
Type ownerType,
- PropertyMetadata metadata,
+ AvaloniaPropertyMetadata metadata,
Action notifying = null)
: base(name, typeof(TValue), ownerType, metadata, notifying)
{
@@ -40,7 +40,7 @@ namespace Avalonia
protected AvaloniaProperty(
AvaloniaProperty source,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
: this(source as AvaloniaProperty ?? throw new InvalidOperationException(), ownerType, metadata)
{
}
@@ -54,7 +54,7 @@ namespace Avalonia
protected AvaloniaProperty(
AvaloniaProperty source,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
: base(source, ownerType, metadata)
{
_changed = source._changed;
diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs
index dc8421fb35..42f941da0c 100644
--- a/src/Avalonia.Base/Data/BindingOperations.cs
+++ b/src/Avalonia.Base/Data/BindingOperations.cs
@@ -45,7 +45,7 @@ namespace Avalonia.Data
case BindingMode.OneWay:
return target.Bind(property, binding.Observable ?? binding.Subject, binding.Priority);
case BindingMode.TwoWay:
- return new CompositeDisposable(
+ return new TwoWayBindingDisposable(
target.Bind(property, binding.Subject, binding.Priority),
target.GetObservable(property).Subscribe(binding.Subject));
case BindingMode.OneTime:
@@ -88,6 +88,32 @@ namespace Avalonia.Data
throw new ArgumentException("Invalid binding mode.");
}
}
+
+ private sealed class TwoWayBindingDisposable : IDisposable
+ {
+ private readonly IDisposable _first;
+ private readonly IDisposable _second;
+ private bool _isDisposed;
+
+ public TwoWayBindingDisposable(IDisposable first, IDisposable second)
+ {
+ _first = first;
+ _second = second;
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _first.Dispose();
+ _second.Dispose();
+
+ _isDisposed = true;
+ }
+ }
}
public sealed class DoNothingType
diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs
index 6e3c9ae67b..e79980518f 100644
--- a/src/Avalonia.Base/Data/BindingValue.cs
+++ b/src/Avalonia.Base/Data/BindingValue.cs
@@ -108,12 +108,12 @@ namespace Avalonia.Data
/// Gets a value indicating whether the binding value represents either a binding or data
/// validation error.
///
- public bool HasError => Type.HasFlagCustom(BindingValueType.HasError);
+ public bool HasError => Type.HasAllFlags(BindingValueType.HasError);
///
/// Gets a value indicating whether the binding value has a value.
///
- public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue);
+ public bool HasValue => Type.HasAllFlags(BindingValueType.HasValue);
///
/// Gets the type of the binding value.
diff --git a/src/Avalonia.Base/Data/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs
index 6740c2168f..9329cdd6af 100644
--- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs
+++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs
@@ -12,5 +12,11 @@ namespace Avalonia.Data.Converters
///
public static readonly IMultiValueConverter And =
new FuncMultiValueConverter(x => x.All(y => y));
+
+ ///
+ /// A multi-value converter that returns true if any of the inputs is true.
+ ///
+ public static readonly IMultiValueConverter Or =
+ new FuncMultiValueConverter(x => x.Any(y => y));
}
}
diff --git a/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
index 7e7417d2f5..7ff0a8ceca 100644
--- a/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
+++ b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
@@ -63,7 +63,7 @@ namespace Avalonia.Data.Converters
}
}
- void OnPropertyChanged(object sender,PropertyChangedEventArgs args)
+ void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (string.IsNullOrWhiteSpace(args.PropertyName)
|| dependencyProperties?.Contains(args.PropertyName) == true)
@@ -88,12 +88,7 @@ namespace Avalonia.Data.Converters
var parameter = Expression.Parameter(typeof(object), "parameter");
- var instance = Expression.Convert
- (
- Expression.Constant(target),
- method.DeclaringType
- );
-
+ var instance = ConvertTarget(target, method);
var call = Expression.Call
(
@@ -114,11 +109,7 @@ namespace Avalonia.Data.Converters
var parameter = Expression.Parameter(typeof(object), "parameter");
- var instance = Expression.Convert
- (
- Expression.Constant(target),
- method.DeclaringType
- );
+ var instance = ConvertTarget(target, method);
Expression body;
@@ -167,11 +158,7 @@ namespace Avalonia.Data.Converters
, System.Reflection.MethodInfo method)
{
var parameter = Expression.Parameter(typeof(object), "parameter");
- var instance = Expression.Convert
- (
- Expression.Constant(target),
- method.DeclaringType
- );
+ var instance = ConvertTarget(target, method);
var call = Expression.Call
(
instance,
@@ -183,6 +170,8 @@ namespace Avalonia.Data.Converters
.Compile();
}
+ private static Expression? ConvertTarget(object? target, MethodInfo method) =>
+ target is null ? null : Expression.Convert(Expression.Constant(target), method.DeclaringType);
internal class WeakPropertyChangedProxy
{
@@ -224,7 +213,7 @@ namespace Avalonia.Data.Converters
else
Unsubscribe();
}
-
+
}
}
}
diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
index 3bf6842cd6..d600603d5c 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Data.Core.Plugins
{
private readonly Dictionary<(Type, string), PropertyInfo> _propertyLookup =
new Dictionary<(Type, string), PropertyInfo>();
-
+
///
public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj.GetType(), propertyName) != null;
@@ -51,7 +51,7 @@ namespace Avalonia.Data.Core.Plugins
private PropertyInfo GetFirstPropertyWithName(Type type, string propertyName)
{
var key = (type, propertyName);
-
+
if (!_propertyLookup.TryGetValue(key, out PropertyInfo propertyInfo))
{
propertyInfo = TryFindAndCacheProperty(type, propertyName);
@@ -59,7 +59,7 @@ namespace Avalonia.Data.Core.Plugins
return propertyInfo;
}
-
+
private PropertyInfo TryFindAndCacheProperty(Type type, string propertyName)
{
PropertyInfo found = null;
@@ -90,7 +90,7 @@ namespace Avalonia.Data.Core.Plugins
private readonly PropertyInfo _property;
private bool _eventRaised;
- public Accessor(WeakReference reference, PropertyInfo property)
+ public Accessor(WeakReference reference, PropertyInfo property)
{
Contract.Requires(reference != null);
Contract.Requires(property != null);
diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs
index d0a918dc88..363d3da0ef 100644
--- a/src/Avalonia.Base/Data/Core/SettableNode.cs
+++ b/src/Avalonia.Base/Data/Core/SettableNode.cs
@@ -15,7 +15,8 @@ namespace Avalonia.Data.Core
private bool ShouldNotSet(object value)
{
- if (PropertyType == null)
+ var propertyType = PropertyType;
+ if (propertyType == null)
{
return false;
}
@@ -37,7 +38,7 @@ namespace Avalonia.Data.Core
return false;
}
- if (PropertyType.IsValueType)
+ if (propertyType.IsValueType)
{
return lastValue.Equals(value);
}
diff --git a/src/Avalonia.Base/Data/IndexerBinding.cs b/src/Avalonia.Base/Data/IndexerBinding.cs
index cc3baa4530..4d07b53dd2 100644
--- a/src/Avalonia.Base/Data/IndexerBinding.cs
+++ b/src/Avalonia.Base/Data/IndexerBinding.cs
@@ -1,6 +1,4 @@
-using System;
-
-namespace Avalonia.Data
+namespace Avalonia.Data
{
public class IndexerBinding : IBinding
{
@@ -24,23 +22,7 @@ namespace Avalonia.Data
object anchor = null,
bool enableDataValidation = false)
{
- var mode = Mode == BindingMode.Default ?
- targetProperty.GetMetadata(target.GetType()).DefaultBindingMode :
- Mode;
-
- switch (mode)
- {
- case BindingMode.OneTime:
- return InstancedBinding.OneTime(Source.GetObservable(Property));
- case BindingMode.OneWay:
- return InstancedBinding.OneWay(Source.GetObservable(Property));
- case BindingMode.OneWayToSource:
- return InstancedBinding.OneWayToSource(Source.GetSubject(Property));
- case BindingMode.TwoWay:
- return InstancedBinding.TwoWay(Source.GetSubject(Property));
- default:
- throw new NotSupportedException("Unsupported BindingMode.");
- }
+ return new InstancedBinding(Source.GetSubject(Property), Mode, BindingPriority.LocalValue);
}
}
}
diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs
index 8e044d7896..9dec399e35 100644
--- a/src/Avalonia.Base/Data/Optional.cs
+++ b/src/Avalonia.Base/Data/Optional.cs
@@ -153,4 +153,18 @@ namespace Avalonia.Data
///
public static Optional Empty => default;
}
+
+ public static class OptionalExtensions
+ {
+ ///
+ /// Casts the type of an using only the C# cast operator.
+ ///
+ /// The target type.
+ /// The binding value.
+ /// The cast value.
+ public static Optional Cast(this Optional value)
+ {
+ return value.HasValue ? new Optional((T)value.Value) : Optional.Empty;
+ }
+ }
}
diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs
index a2f113adb7..e6cc1edfdf 100644
--- a/src/Avalonia.Base/DirectPropertyBase.cs
+++ b/src/Avalonia.Base/DirectPropertyBase.cs
@@ -26,7 +26,7 @@ namespace Avalonia
protected DirectPropertyBase(
string name,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
: base(name, ownerType, metadata)
{
}
@@ -41,7 +41,7 @@ namespace Avalonia
protected DirectPropertyBase(
AvaloniaProperty source,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
: this(source as DirectPropertyBase ?? throw new InvalidOperationException(), ownerType, metadata)
{
}
@@ -55,7 +55,7 @@ namespace Avalonia
protected DirectPropertyBase(
DirectPropertyBase source,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
: base(source, ownerType, metadata)
{
}
diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs
index 59a60507dc..205967984d 100644
--- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs
+++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs
@@ -5,7 +5,7 @@ namespace Avalonia
///
/// Metadata for direct avalonia properties.
///
- public class DirectPropertyMetadata : PropertyMetadata, IDirectPropertyMetadata
+ public class DirectPropertyMetadata : AvaloniaPropertyMetadata, IDirectPropertyMetadata
{
///
/// Initializes a new instance of the class.
@@ -47,7 +47,7 @@ namespace Avalonia
object IDirectPropertyMetadata.UnsetValue => UnsetValue;
///
- public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property)
+ public override void Merge(AvaloniaPropertyMetadata baseMetadata, AvaloniaProperty property)
{
base.Merge(baseMetadata, property);
diff --git a/src/Avalonia.Base/EnumExtensions.cs b/src/Avalonia.Base/EnumExtensions.cs
index 1e4864283f..19eb42a700 100644
--- a/src/Avalonia.Base/EnumExtensions.cs
+++ b/src/Avalonia.Base/EnumExtensions.cs
@@ -9,12 +9,70 @@ namespace Avalonia
public static class EnumExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static unsafe bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum
+ [Obsolete("This method is obsolete. Use HasAllFlags instead.")]
+ public static bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum
+ => value.HasAllFlags(flag);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe bool HasAllFlags(this T value, T flags) where T : unmanaged, Enum
{
- var intValue = *(int*)&value;
- var intFlag = *(int*)&flag;
+ if (sizeof(T) == 1)
+ {
+ var byteValue = Unsafe.As(ref value);
+ var byteFlags = Unsafe.As(ref flags);
+ return (byteValue & byteFlags) == byteFlags;
+ }
+ else if (sizeof(T) == 2)
+ {
+ var shortValue = Unsafe.As(ref value);
+ var shortFlags = Unsafe.As(ref flags);
+ return (shortValue & shortFlags) == shortFlags;
+ }
+ else if (sizeof(T) == 4)
+ {
+ var intValue = Unsafe.As(ref value);
+ var intFlags = Unsafe.As(ref flags);
+ return (intValue & intFlags) == intFlags;
+ }
+ else if (sizeof(T) == 8)
+ {
+ var longValue = Unsafe.As(ref value);
+ var longFlags = Unsafe.As(ref flags);
+ return (longValue & longFlags) == longFlags;
+ }
+ else
+ throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf() + " are not supported");
+ }
- return (intValue & intFlag) == intFlag;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe bool HasAnyFlag(this T value, T flags) where T : unmanaged, Enum
+ {
+ if (sizeof(T) == 1)
+ {
+ var byteValue = Unsafe.As(ref value);
+ var byteFlags = Unsafe.As(ref flags);
+ return (byteValue & byteFlags) != 0;
+ }
+ else if (sizeof(T) == 2)
+ {
+ var shortValue = Unsafe.As(ref value);
+ var shortFlags = Unsafe.As(ref flags);
+ return (shortValue & shortFlags) != 0;
+ }
+ else if (sizeof(T) == 4)
+ {
+ var intValue = Unsafe.As(ref value);
+ var intFlags = Unsafe.As(ref flags);
+ return (intValue & intFlags) != 0;
+ }
+ else if (sizeof(T) == 8)
+ {
+ var longValue = Unsafe.As(ref value);
+ var longFlags = Unsafe.As(ref flags);
+ return (longValue & longFlags) != 0;
+ }
+ else
+ throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf() + " are not supported");
}
}
}
diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs
index 3c19b47a05..2ad220dddd 100644
--- a/src/Avalonia.Base/Logging/LogArea.cs
+++ b/src/Avalonia.Base/Logging/LogArea.cs
@@ -34,5 +34,10 @@ namespace Avalonia.Logging
/// The log event comes from the control system.
///
public const string Control = "Control";
+
+ ///
+ /// The log event comes from Win32Platform.
+ ///
+ public const string Win32Platform = nameof(Win32Platform);
}
}
diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs
index e06cb75a4d..b054c186ae 100644
--- a/src/Avalonia.Base/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs
@@ -11,10 +11,12 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+[assembly: InternalsVisibleTo("Avalonia.Visuals, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests")]
+[assembly: InternalsVisibleTo("Avalonia.Visuals")]
#endif
diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
index 0d563947e7..362736eb06 100644
--- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
@@ -9,8 +9,9 @@ namespace Avalonia.PropertyStore
///
/// Represents an untyped interface to .
///
- internal interface IBindingEntry : IPriorityValueEntry, IDisposable
+ internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
{
+ void Start(bool ignoreBatchUpdate);
}
///
@@ -22,6 +23,8 @@ namespace Avalonia.PropertyStore
private readonly IAvaloniaObject _owner;
private IValueSink _sink;
private IDisposable? _subscription;
+ private bool _isSubscribed;
+ private bool _batchUpdate;
private Optional _value;
public BindingEntry(
@@ -39,10 +42,20 @@ namespace Avalonia.PropertyStore
}
public StyledPropertyBase Property { get; }
- public BindingPriority Priority { get; }
+ public BindingPriority Priority { get; private set; }
public IObservable> Source { get; }
Optional IValue.GetValue() => _value.ToObject();
+ public void BeginBatchUpdate() => _batchUpdate = true;
+
+ public void EndBatchUpdate()
+ {
+ _batchUpdate = false;
+
+ if (_sink is ValueStore)
+ Start();
+ }
+
public Optional GetValue(BindingPriority maxPriority)
{
return Priority >= maxPriority ? _value : Optional.Empty;
@@ -52,14 +65,21 @@ namespace Avalonia.PropertyStore
{
_subscription?.Dispose();
_subscription = null;
- _sink.Completed(Property, this, _value);
+ OnCompleted();
}
- public void OnCompleted() => _sink.Completed(Property, this, _value);
+ public void OnCompleted()
+ {
+ var oldValue = _value;
+ _value = default;
+ Priority = BindingPriority.Unset;
+ _isSubscribed = false;
+ _sink.Completed(Property, this, oldValue);
+ }
public void OnError(Exception error)
{
- throw new NotImplementedException();
+ throw new NotImplementedException("BindingEntry.OnError is not implemented", error);
}
public void OnNext(BindingValue value)
@@ -79,13 +99,39 @@ namespace Avalonia.PropertyStore
}
}
- public void Start()
+ public void Start() => Start(false);
+
+ public void Start(bool ignoreBatchUpdate)
{
- _subscription = Source.Subscribe(this);
+ // We can't use _subscription to check whether we're subscribed because it won't be set
+ // until Subscribe has finished, which will be too late to prevent reentrancy. In addition
+ // don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
+ if (!_isSubscribed &&
+ Priority != BindingPriority.Unset &&
+ (!_batchUpdate || ignoreBatchUpdate))
+ {
+ _isSubscribed = true;
+ _subscription = Source.Subscribe(this);
+ }
}
public void Reparent(IValueSink sink) => _sink = sink;
-
+
+ public void RaiseValueChanged(
+ IValueSink sink,
+ IAvaloniaObject owner,
+ AvaloniaProperty property,
+ Optional oldValue,
+ Optional newValue)
+ {
+ sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
+ owner,
+ (AvaloniaProperty)property,
+ oldValue.Cast(),
+ newValue.Cast(),
+ Priority));
+ }
+
private void UpdateValue(BindingValue value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
index 46f6f9a137..dc4a1d88c1 100644
--- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
@@ -1,23 +1,31 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
#nullable enable
namespace Avalonia.PropertyStore
{
+ ///
+ /// Represents an untyped interface to .
+ ///
+ internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
+ {
+ }
+
///
/// Stores a value with a priority in a or
/// .
///
/// The property type.
- internal class ConstantValueEntry : IPriorityValueEntry, IDisposable
+ internal class ConstantValueEntry : IPriorityValueEntry, IConstantValueEntry
{
private IValueSink _sink;
private Optional _value;
public ConstantValueEntry(
StyledPropertyBase property,
- T value,
+ [AllowNull] T value,
BindingPriority priority,
IValueSink sink)
{
@@ -28,7 +36,7 @@ namespace Avalonia.PropertyStore
}
public StyledPropertyBase Property { get; }
- public BindingPriority Priority { get; }
+ public BindingPriority Priority { get; private set; }
Optional IValue.GetValue() => _value.ToObject();
public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation)
@@ -36,7 +44,30 @@ namespace Avalonia.PropertyStore
return Priority >= maxPriority ? _value : Optional.Empty;
}
- public void Dispose() => _sink.Completed(Property, this, _value);
+ public void Dispose()
+ {
+ var oldValue = _value;
+ _value = default;
+ Priority = BindingPriority.Unset;
+ _sink.Completed(Property, this, oldValue);
+ }
+
public void Reparent(IValueSink sink) => _sink = sink;
+ public void Start() { }
+
+ public void RaiseValueChanged(
+ IValueSink sink,
+ IAvaloniaObject owner,
+ AvaloniaProperty property,
+ Optional oldValue,
+ Optional newValue)
+ {
+ sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
+ owner,
+ (AvaloniaProperty)property,
+ oldValue.Cast(),
+ newValue.Cast(),
+ Priority));
+ }
}
}
diff --git a/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs b/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
new file mode 100644
index 0000000000..af4faf989c
--- /dev/null
+++ b/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
@@ -0,0 +1,8 @@
+namespace Avalonia.PropertyStore
+{
+ internal interface IBatchUpdate
+ {
+ void BeginBatchUpdate();
+ void EndBatchUpdate();
+ }
+}
diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs
index 249cfc360c..7f5245bb45 100644
--- a/src/Avalonia.Base/PropertyStore/IValue.cs
+++ b/src/Avalonia.Base/PropertyStore/IValue.cs
@@ -9,8 +9,15 @@ namespace Avalonia.PropertyStore
///
internal interface IValue
{
- Optional GetValue();
BindingPriority Priority { get; }
+ Optional GetValue();
+ void Start();
+ void RaiseValueChanged(
+ IValueSink sink,
+ IAvaloniaObject owner,
+ AvaloniaProperty property,
+ Optional oldValue,
+ Optional newValue);
}
///
diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
index 859e9ba81c..8fe2ad7794 100644
--- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
@@ -24,5 +24,21 @@ namespace Avalonia.PropertyStore
}
public void SetValue(T value) => _value = value;
+ public void Start() { }
+
+ public void RaiseValueChanged(
+ IValueSink sink,
+ IAvaloniaObject owner,
+ AvaloniaProperty property,
+ Optional oldValue,
+ Optional newValue)
+ {
+ sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
+ owner,
+ (AvaloniaProperty)property,
+ oldValue.Cast(),
+ newValue.Cast(),
+ BindingPriority.LocalValue));
+ }
}
}
diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs
index 5e223cad60..556f1a6269 100644
--- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs
+++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs
@@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
/// entries (sorted first by priority and then in the order
/// they were added) plus a local value.
///
- internal class PriorityValue : IValue, IValueSink
+ internal class PriorityValue : IValue, IValueSink, IBatchUpdate
{
private readonly IAvaloniaObject _owner;
private readonly IValueSink _sink;
@@ -26,6 +26,8 @@ namespace Avalonia.PropertyStore
private readonly Func? _coerceValue;
private Optional _localValue;
private Optional _value;
+ private bool _isCalculatingValue;
+ private bool _batchUpdate;
public PriorityValue(
IAvaloniaObject owner,
@@ -53,6 +55,18 @@ namespace Avalonia.PropertyStore
existing.Reparent(this);
_entries.Add(existing);
+ if (existing is IBindingEntry binding &&
+ existing.Priority == BindingPriority.LocalValue)
+ {
+ // Bit of a special case here: if we have a local value binding that is being
+ // promoted to a priority value we need to make sure the binding is subscribed
+ // even if we've got a batch operation in progress because otherwise we don't know
+ // whether the binding or a subsequent SetValue with local priority will win. A
+ // notification won't be sent during batch update anyway because it will be
+ // caught and stored for later by the ValueStore.
+ binding.Start(ignoreBatchUpdate: true);
+ }
+
var v = existing.GetValue();
if (v.HasValue)
@@ -78,6 +92,28 @@ namespace Avalonia.PropertyStore
public IReadOnlyList> Entries => _entries;
Optional IValue.GetValue() => _value.ToObject();
+ public void BeginBatchUpdate()
+ {
+ _batchUpdate = true;
+
+ foreach (var entry in _entries)
+ {
+ (entry as IBatchUpdate)?.BeginBatchUpdate();
+ }
+ }
+
+ public void EndBatchUpdate()
+ {
+ _batchUpdate = false;
+
+ foreach (var entry in _entries)
+ {
+ (entry as IBatchUpdate)?.EndBatchUpdate();
+ }
+
+ UpdateEffectiveValue(null);
+ }
+
public void ClearLocalValue()
{
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs(
@@ -134,10 +170,37 @@ namespace Avalonia.PropertyStore
var binding = new BindingEntry(_owner, Property, source, priority, this);
var insert = FindInsertPoint(binding.Priority);
_entries.Insert(insert, binding);
+
+ if (_batchUpdate)
+ {
+ binding.BeginBatchUpdate();
+
+ if (priority == BindingPriority.LocalValue)
+ {
+ binding.Start(ignoreBatchUpdate: true);
+ }
+ }
+
return binding;
}
- public void CoerceValue() => UpdateEffectiveValue(null);
+ public void UpdateEffectiveValue() => UpdateEffectiveValue(null);
+ public void Start() => UpdateEffectiveValue(null);
+
+ public void RaiseValueChanged(
+ IValueSink sink,
+ IAvaloniaObject owner,
+ AvaloniaProperty property,
+ Optional oldValue,
+ Optional newValue)
+ {
+ sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
+ owner,
+ (AvaloniaProperty)property,
+ oldValue.Cast(),
+ newValue.Cast(),
+ Priority));
+ }
void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change)
{
@@ -146,7 +209,7 @@ namespace Avalonia.PropertyStore
_localValue = default;
}
- if (change is AvaloniaPropertyChangedEventArgs c)
+ if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs c)
{
UpdateEffectiveValue(c);
}
@@ -188,41 +251,47 @@ namespace Avalonia.PropertyStore
public (Optional, BindingPriority) CalculateValue(BindingPriority maxPriority)
{
- var reachedLocalValues = false;
+ _isCalculatingValue = true;
- for (var i = _entries.Count - 1; i >= 0; --i)
+ try
{
- var entry = _entries[i];
-
- if (entry.Priority < maxPriority)
+ for (var i = _entries.Count - 1; i >= 0; --i)
{
- continue;
+ var entry = _entries[i];
+
+ if (entry.Priority < maxPriority)
+ {
+ continue;
+ }
+
+ entry.Start();
+
+ if (entry.Priority >= BindingPriority.LocalValue &&
+ maxPriority <= BindingPriority.LocalValue &&
+ _localValue.HasValue)
+ {
+ return (_localValue, BindingPriority.LocalValue);
+ }
+
+ var entryValue = entry.GetValue();
+
+ if (entryValue.HasValue)
+ {
+ return (entryValue, entry.Priority);
+ }
}
- if (!reachedLocalValues &&
- entry.Priority >= BindingPriority.LocalValue &&
- maxPriority <= BindingPriority.LocalValue &&
- _localValue.HasValue)
+ if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}
- var entryValue = entry.GetValue();
-
- if (entryValue.HasValue)
- {
- return (entryValue, entry.Priority);
- }
+ return (default, BindingPriority.Unset);
}
-
- if (!reachedLocalValues &&
- maxPriority <= BindingPriority.LocalValue &&
- _localValue.HasValue)
+ finally
{
- return (_localValue, BindingPriority.LocalValue);
+ _isCalculatingValue = false;
}
-
- return (default, BindingPriority.Unset);
}
private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs? change)
diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
index 238aba5c96..6a3f9b0b30 100644
--- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
+++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
namespace Avalonia.Reactive
{
@@ -55,9 +56,9 @@ namespace Avalonia.Reactive
newValue = (T)e.Sender.GetValue(e.Property);
}
- if (!Equals(newValue, _value))
+ if (!EqualityComparer.Default.Equals(newValue, _value))
{
- _value = (T)newValue;
+ _value = newValue;
PublishNext(_value);
}
}
diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs
index cf0a0c34ec..ec7bff4dfb 100644
--- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs
+++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs
@@ -6,7 +6,7 @@ namespace Avalonia
///
/// Metadata for styled avalonia properties.
///
- public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyMetadata
+ public class StyledPropertyMetadata : AvaloniaPropertyMetadata, IStyledPropertyMetadata
{
private Optional _defaultValue;
@@ -39,7 +39,7 @@ namespace Avalonia
object IStyledPropertyMetadata.DefaultValue => DefaultValue;
///
- public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property)
+ public override void Merge(AvaloniaPropertyMetadata baseMetadata, AvaloniaProperty property)
{
base.Merge(baseMetadata, property);
diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
index 6e52b6770a..c513f75962 100644
--- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
+++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -22,6 +22,9 @@ namespace Avalonia.Utilities
_entries = s_emptyEntries;
}
+ public int Count => _entries.Length - 1;
+ public TValue this[int index] => _entries[index].Value;
+
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
@@ -91,7 +94,7 @@ namespace Avalonia.Utilities
return (0, false);
}
- public bool TryGetValue(AvaloniaProperty property, [MaybeNull] out TValue value)
+ public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
@@ -163,18 +166,6 @@ namespace Avalonia.Utilities
}
}
- public Dictionary ToDictionary()
- {
- var dict = new Dictionary(_entries.Length - 1);
-
- for (int i = 0; i < _entries.Length - 1; ++i)
- {
- dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
- }
-
- return dict;
- }
-
private struct Entry
{
internal int PropertyId;
diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs
index 446b366dc8..1ea369954a 100644
--- a/src/Avalonia.Base/Utilities/MathUtilities.cs
+++ b/src/Avalonia.Base/Utilities/MathUtilities.cs
@@ -30,6 +30,21 @@ namespace Avalonia.Utilities
return (-eps < delta) && (eps > delta);
}
+ ///
+ /// AreClose - Returns whether or not two doubles are "close". That is, whether or
+ /// not they are within epsilon of each other.
+ ///
+ /// The first double to compare.
+ /// The second double to compare.
+ /// The fixed epsilon value used to compare.
+ public static bool AreClose(double value1, double value2, double eps)
+ {
+ //in case they are Infinities (then epsilon check does not work)
+ if (value1 == value2) return true;
+ double delta = value1 - value2;
+ return (-eps < delta) && (eps > delta);
+ }
+
///
/// AreClose - Returns whether or not two floats are "close". That is, whether or
/// not they are within epsilon of each other.
diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs
index d0d88166a7..9f2308a062 100644
--- a/src/Avalonia.Base/Utilities/TypeUtilities.cs
+++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs
@@ -372,8 +372,8 @@ namespace Avalonia.Utilities
const string implicitName = "op_Implicit";
const string explicitName = "op_Explicit";
- bool allowImplicit = (operatorType & OperatorType.Implicit) != 0;
- bool allowExplicit = (operatorType & OperatorType.Explicit) != 0;
+ bool allowImplicit = operatorType.HasAllFlags(OperatorType.Implicit);
+ bool allowExplicit = operatorType.HasAllFlags(OperatorType.Explicit);
foreach (MethodInfo method in fromType.GetMethods())
{
diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs
index 6b89fcbdb9..495f13e1a9 100644
--- a/src/Avalonia.Base/ValueStore.cs
+++ b/src/Avalonia.Base/ValueStore.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Utilities;
@@ -26,6 +28,7 @@ namespace Avalonia
private readonly AvaloniaObject _owner;
private readonly IValueSink _sink;
private readonly AvaloniaPropertyValueStore _values;
+ private BatchUpdate? _batchUpdate;
public ValueStore(AvaloniaObject owner)
{
@@ -33,9 +36,28 @@ namespace Avalonia
_values = new AvaloniaPropertyValueStore();
}
+ public void BeginBatchUpdate()
+ {
+ _batchUpdate ??= new BatchUpdate(this);
+ _batchUpdate.Begin();
+ }
+
+ public void EndBatchUpdate()
+ {
+ if (_batchUpdate is null)
+ {
+ throw new InvalidOperationException("No batch update in progress.");
+ }
+
+ if (_batchUpdate.End())
+ {
+ _batchUpdate = null;
+ }
+ }
+
public bool IsAnimating(AvaloniaProperty property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
return slot.Priority < BindingPriority.LocalValue;
}
@@ -45,7 +67,7 @@ namespace Avalonia
public bool IsSet(AvaloniaProperty property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
return slot.GetValue().HasValue;
}
@@ -58,7 +80,7 @@ namespace Avalonia
BindingPriority maxPriority,
out T value)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
var v = ((IValue)slot).GetValue(maxPriority);
@@ -82,7 +104,7 @@ namespace Avalonia
IDisposable? result = null;
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
result = SetExisting(slot, property, value, priority);
}
@@ -90,23 +112,21 @@ namespace Avalonia
{
// If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue(_owner, property, this);
- _values.AddValue(property, entry);
+ AddValue(property, entry);
result = entry.SetValue(value, priority);
}
else
{
- var change = new AvaloniaPropertyChangedEventArgs(_owner, property, default, value, priority);
-
if (priority == BindingPriority.LocalValue)
{
- _values.AddValue(property, new LocalValueEntry(value));
- _sink.ValueChanged(change);
+ AddValue(property, new LocalValueEntry(value));
+ NotifyValueChanged(property, default, value, priority);
}
else
{
var entry = new ConstantValueEntry(property, value, priority, this);
- _values.AddValue(property, entry);
- _sink.ValueChanged(change);
+ AddValue(property, entry);
+ NotifyValueChanged(property, default, value, priority);
result = entry;
}
}
@@ -119,7 +139,7 @@ namespace Avalonia
IObservable> source,
BindingPriority priority)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
return BindExisting(slot, property, source, priority);
}
@@ -128,62 +148,69 @@ namespace Avalonia
// If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue(_owner, property, this);
var binding = entry.AddBinding(source, priority);
- _values.AddValue(property, entry);
- binding.Start();
+ AddValue(property, entry);
return binding;
}
else
{
var entry = new BindingEntry(_owner, property, source, priority, this);
- _values.AddValue(property, entry);
- entry.Start();
+ AddValue(property, entry);
return entry;
}
}
public void ClearLocalValue(StyledPropertyBase property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue p)
{
p.ClearLocalValue();
}
- else
+ else if (slot.Priority == BindingPriority.LocalValue)
{
- var remove = slot is ConstantValueEntry c ?
- c.Priority == BindingPriority.LocalValue :
- !(slot is IPriorityValueEntry);
+ var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
- if (remove)
+ // During batch update values can't be removed immediately because they're needed to raise
+ // a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
+ // by setting their priority to Unset.
+ if (!IsBatchUpdating())
{
- var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
_values.Remove(property);
- _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
- _owner,
- property,
- new Optional(old),
- default,
- BindingPriority.Unset));
}
+ else if (slot is IDisposable d)
+ {
+ d.Dispose();
+ }
+ else
+ {
+ // Local value entries are optimized and contain only a single value field to save space,
+ // so there's no way to mark them for removal at the end of a batch update. Instead convert
+ // them to a constant value entry with Unset priority in the event of a local value being
+ // cleared during a batch update.
+ var sentinel = new ConstantValueEntry(property, default, BindingPriority.Unset, _sink);
+ _values.SetValue(property, sentinel);
+ }
+
+ NotifyValueChanged(property, old, default, BindingPriority.Unset);
}
}
}
public void CoerceValue(StyledPropertyBase property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue p)
{
- p.CoerceValue();
+ p.UpdateEffectiveValue();
}
}
}
public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
var slotValue = slot.GetValue();
return new Diagnostics.AvaloniaPropertyValue(
@@ -198,7 +225,17 @@ namespace Avalonia
void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change)
{
- _sink.ValueChanged(change);
+ if (_batchUpdate is object)
+ {
+ if (change.IsEffectiveValueChange)
+ {
+ NotifyValueChanged(change.Property, change.OldValue, change.NewValue, change.Priority);
+ }
+ }
+ else
+ {
+ _sink.ValueChanged(change);
+ }
}
void IValueSink.Completed(
@@ -206,13 +243,18 @@ namespace Avalonia
IPriorityValueEntry entry,
Optional oldValue)
{
- if (_values.TryGetValue(property, out var slot))
+ // We need to include remove sentinels here so call `_values.TryGetValue` directly.
+ if (_values.TryGetValue(property, out var slot) && slot == entry)
{
- if (slot == entry)
+ if (_batchUpdate is null)
{
_values.Remove(property);
_sink.Completed(property, entry, oldValue);
}
+ else
+ {
+ _batchUpdate.ValueChanged(property, oldValue.ToObject());
+ }
}
}
@@ -240,16 +282,13 @@ namespace Avalonia
{
var old = l.GetValue(BindingPriority.LocalValue);
l.SetValue(value);
- _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
- _owner,
- property,
- old,
- value,
- priority));
+ NotifyValueChanged(property, old, value, priority);
}
else
{
var priorityValue = new PriorityValue(_owner, property, this, l);
+ if (IsBatchUpdating())
+ priorityValue.BeginBatchUpdate();
result = priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue);
}
@@ -273,6 +312,11 @@ namespace Avalonia
if (slot is IPriorityValueEntry e)
{
priorityValue = new PriorityValue(_owner, property, this, e);
+
+ if (IsBatchUpdating())
+ {
+ priorityValue.BeginBatchUpdate();
+ }
}
else if (slot is PriorityValue p)
{
@@ -289,8 +333,181 @@ namespace Avalonia
var binding = priorityValue.AddBinding(source, priority);
_values.SetValue(property, priorityValue);
- binding.Start();
+ priorityValue.UpdateEffectiveValue();
return binding;
}
+
+ private void AddValue(AvaloniaProperty property, IValue value)
+ {
+ _values.AddValue(property, value);
+ if (IsBatchUpdating() && value is IBatchUpdate batch)
+ batch.BeginBatchUpdate();
+ value.Start();
+ }
+
+ private void NotifyValueChanged(
+ AvaloniaProperty property,
+ Optional oldValue,
+ BindingValue newValue,
+ BindingPriority priority)
+ {
+ if (_batchUpdate is null)
+ {
+ _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
+ _owner,
+ property,
+ oldValue,
+ newValue,
+ priority));
+ }
+ else
+ {
+ _batchUpdate.ValueChanged(property, oldValue.ToObject());
+ }
+ }
+
+ private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true;
+
+ private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value)
+ {
+ return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value);
+ }
+
+ private static bool IsRemoveSentinel(IValue value)
+ {
+ // Local value entries are optimized and contain only a single value field to save space,
+ // so there's no way to mark them for removal at the end of a batch update. Instead a
+ // ConstantValueEntry with a priority of Unset is used as a sentinel value.
+ return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset;
+ }
+
+ private class BatchUpdate
+ {
+ private ValueStore _owner;
+ private List? _notifications;
+ private int _batchUpdateCount;
+ private int _iterator = -1;
+
+ public BatchUpdate(ValueStore owner) => _owner = owner;
+
+ public bool IsBatchUpdating => _batchUpdateCount > 0;
+
+ public void Begin()
+ {
+ if (_batchUpdateCount++ == 0)
+ {
+ var values = _owner._values;
+
+ for (var i = 0; i < values.Count; ++i)
+ {
+ (values[i] as IBatchUpdate)?.BeginBatchUpdate();
+ }
+ }
+ }
+
+ public bool End()
+ {
+ if (--_batchUpdateCount > 0)
+ return false;
+
+ var values = _owner._values;
+
+ // First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed
+ // but notifications will still not be raised because the owner ValueStore will still have a reference
+ // to this batch update object.
+ for (var i = 0; i < values.Count; ++i)
+ {
+ (values[i] as IBatchUpdate)?.EndBatchUpdate();
+
+ // Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it
+ // does, abort and continue batch updating.
+ if (_batchUpdateCount > 0)
+ return false;
+ }
+
+ if (_notifications is object)
+ {
+ // Raise all batched notifications. Doing this can cause other notifications to be added and even
+ // cause a new batch update to start, so we need to handle _notifications being modified by storing
+ // the index in field.
+ _iterator = 0;
+
+ for (; _iterator < _notifications.Count; ++_iterator)
+ {
+ var entry = _notifications[_iterator];
+
+ if (values.TryGetValue(entry.property, out var slot))
+ {
+ var oldValue = entry.oldValue;
+ var newValue = slot.GetValue();
+
+ // Raising this notification can cause a new batch update to be started, which in turn
+ // results in another change to the property. In this case we need to update the old value
+ // so that the *next* notification has an oldValue which follows on from the newValue
+ // raised here.
+ _notifications[_iterator] = new Notification
+ {
+ property = entry.property,
+ oldValue = newValue,
+ };
+
+ // Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs.
+ slot.RaiseValueChanged(_owner._sink, _owner._owner, entry.property, oldValue, newValue);
+
+ // During batch update values can't be removed immediately because they're needed to raise
+ // the _sink.ValueChanged notification. They instead mark themselves for removal by setting
+ // their priority to Unset. We need to re-read the slot here because raising ValueChanged
+ // could have caused it to be updated.
+ if (values.TryGetValue(entry.property, out var updatedSlot) &&
+ updatedSlot.Priority == BindingPriority.Unset)
+ {
+ values.Remove(entry.property);
+ }
+ }
+ else
+ {
+ throw new AvaloniaInternalException("Value could not be found at the end of batch update.");
+ }
+
+ // If a new batch update was started while ending this one, abort.
+ if (_batchUpdateCount > 0)
+ return false;
+ }
+ }
+
+ _iterator = int.MaxValue - 1;
+ return true;
+ }
+
+ public void ValueChanged(AvaloniaProperty property, Optional oldValue)
+ {
+ _notifications ??= new List();
+
+ for (var i = 0; i < _notifications.Count; ++i)
+ {
+ if (_notifications[i].property == property)
+ {
+ oldValue = _notifications[i].oldValue;
+ _notifications.RemoveAt(i);
+
+ if (i <= _iterator)
+ --_iterator;
+ break;
+ }
+ }
+
+ _notifications.Add(new Notification
+ {
+ property = property,
+ oldValue = oldValue,
+ });
+ }
+
+ private struct Notification
+ {
+ public AvaloniaProperty property;
+ public Optional oldValue;
+ }
+ }
}
}
diff --git a/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs b/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs
new file mode 100644
index 0000000000..f207b558a3
--- /dev/null
+++ b/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs
@@ -0,0 +1,12 @@
+using System;
+using XamlX.Transform;
+
+namespace Avalonia.Build.Tasks
+{
+ public class DeterministicIdGenerator : IXamlIdentifierGenerator
+ {
+ private int _nextId = 1;
+
+ public string GenerateIdentifierPart() => (_nextId++).ToString();
+ }
+}
diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
index 6ef8a98fae..508045dccb 100644
--- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
+++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
@@ -22,7 +22,6 @@ using XamlX.IL;
namespace Avalonia.Build.Tasks
{
-
public static partial class XamlCompilerTaskExecutor
{
static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
@@ -99,7 +98,8 @@ namespace Avalonia.Build.Tasks
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage),
AvaloniaXamlIlLanguage.CustomValueConverter,
new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)),
- new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)));
+ new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)),
+ new DeterministicIdGenerator());
var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext",
diff --git a/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt b/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt
deleted file mode 100644
index 82472c505a..0000000000
--- a/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-Compat issues with assembly Avalonia.Controls.DataGrid:
-MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.DataGridTextColumn.FontFamilyProperty' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public System.String Avalonia.Controls.DataGridTextColumn.FontFamily.get()' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public void Avalonia.Controls.DataGridTextColumn.FontFamily.set(System.String)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 3
diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
index 92734b128d..5b1e43b8f4 100644
--- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
+++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
@@ -2595,7 +2595,7 @@ namespace Avalonia.Collections
/// Whether the specified flag is set
private bool CheckFlag(CollectionViewFlags flags)
{
- return (_flags & flags) != 0;
+ return _flags.HasAllFlags(flags);
}
///
@@ -3275,7 +3275,7 @@ namespace Avalonia.Collections
addIndex);
// next check if we need to add an item into the current group
- // bool needsGrouping = false;
+ bool needsGrouping = false;
if (Count == 1 && GroupDescriptions.Count > 0)
{
// if this is the first item being added
@@ -3302,7 +3302,7 @@ namespace Avalonia.Collections
// otherwise, we need to validate that it is within the current page.
if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex)
{
- //needsGrouping = true;
+ needsGrouping = true;
int pageStartIndex = PageIndex * PageSize;
@@ -3340,6 +3340,13 @@ namespace Avalonia.Collections
}
}
+ // if we need to add the item into the current group
+ // that will be displayed
+ if (needsGrouping)
+ {
+ this._group.AddToSubgroups(addedItem, false /*loading*/);
+ }
+
int addedIndex = IndexOf(addedItem);
// if the item is within the current page
diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
index 662ff91329..ca6020128c 100644
--- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
+++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
@@ -265,6 +265,43 @@ namespace Avalonia.Collections
{
return new DataGridPathSortDescription(propertyPath, direction, comparer, null);
}
+
+ public static DataGridSortDescription FromComparer(IComparer comparer, ListSortDirection direction = ListSortDirection.Ascending)
+ {
+ return new DataGridComparerSortDesctiption(comparer, direction);
+ }
+ }
+
+ public class DataGridComparerSortDesctiption : DataGridSortDescription
+ {
+ private readonly IComparer _innerComparer;
+ private readonly ListSortDirection _direction;
+ private readonly IComparer _comparer;
+
+ public IComparer SourceComparer => _innerComparer;
+ public override IComparer Comparer => _comparer;
+ public override ListSortDirection Direction => _direction;
+ public DataGridComparerSortDesctiption(IComparer comparer, ListSortDirection direction)
+ {
+ _innerComparer = comparer;
+ _direction = direction;
+ _comparer = Comparer.Create((x, y) => Compare(x, y));
+ }
+
+ private int Compare(object x, object y)
+ {
+ int result = _innerComparer.Compare(x, y);
+
+ if (Direction == ListSortDirection.Descending)
+ return -result;
+ else
+ return result;
+ }
+ public override DataGridSortDescription SwitchSortDirection()
+ {
+ var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
+ return new DataGridComparerSortDesctiption(_innerComparer, newDirection);
+ }
}
public class DataGridSortDescriptionCollection : AvaloniaList
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index 5bb2763566..1b4632d368 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -1,6 +1,6 @@
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
-// All other rights reserved.
+// All other rights reserved.
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
@@ -92,7 +92,7 @@ namespace Avalonia.Controls
private ContentControl _topRightCornerHeader;
private Control _frozenColumnScrollBarSpacer;
- // the sum of the widths in pixels of the scrolling columns preceding
+ // the sum of the widths in pixels of the scrolling columns preceding
// the first displayed scrolling column
private double _horizontalOffset;
@@ -143,7 +143,7 @@ namespace Avalonia.Controls
private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode.
private ICellEditBinding _currentCellEditBinding;
- // An approximation of the sum of the heights in pixels of the scrolling rows preceding
+ // An approximation of the sum of the heights in pixels of the scrolling rows preceding
// the first displayed scrolling row. Since the scrolled off rows are discarded, the grid
// does not know their actual height. The heights used for the approximation are the ones
// set as the rows were scrolled off.
@@ -162,7 +162,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register(nameof(CanUserReorderColumns));
///
- /// Gets or sets a value that indicates whether the user can change
+ /// Gets or sets a value that indicates whether the user can change
/// the column display order by dragging column headers with the mouse.
///
public bool CanUserReorderColumns
@@ -247,8 +247,8 @@ namespace Avalonia.Controls
/// Gets or sets the that is used to paint the background of odd-numbered rows.
///
///
- /// The brush that is used to paint the background of odd-numbered rows. The default is a
- /// with a
+ /// The brush that is used to paint the background of odd-numbered rows. The default is a
+ /// with a
/// value of white (ARGB value #00FFFFFF).
///
public IBrush AlternatingRowBackground
@@ -379,8 +379,8 @@ namespace Avalonia.Controls
public bool IsValid
{
get { return _isValid; }
- internal set
- {
+ internal set
+ {
SetAndRaise(IsValidProperty, ref _isValid, value);
PseudoClasses.Set(":invalid", !value);
}
@@ -398,7 +398,7 @@ namespace Avalonia.Controls
}
///
- /// Gets or sets the maximum width of columns in the .
+ /// Gets or sets the maximum width of columns in the .
///
public double MaxColumnWidth
{
@@ -418,7 +418,7 @@ namespace Avalonia.Controls
}
///
- /// Gets or sets the minimum width of columns in the .
+ /// Gets or sets the minimum width of columns in the .
///
public double MinColumnWidth
{
@@ -496,7 +496,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register(nameof(VerticalGridLinesBrush));
///
- /// Gets or sets the that is used to paint grid lines separating columns.
+ /// Gets or sets the that is used to paint grid lines separating columns.
///
public IBrush VerticalGridLinesBrush
{
@@ -535,14 +535,15 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect(
nameof(SelectedIndex),
o => o.SelectedIndex,
- (o, v) => o.SelectedIndex = v);
+ (o, v) => o.SelectedIndex = v,
+ defaultBindingMode: BindingMode.TwoWay);
///
/// Gets or sets the index of the current selection.
///
///
/// The index of the current selection, or -1 if the selection is empty.
- ///
+ ///
public int SelectedIndex
{
get { return _selectedIndex; }
@@ -553,7 +554,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect(
nameof(SelectedItem),
o => o.SelectedItem,
- (o, v) => o.SelectedItem = v);
+ (o, v) => o.SelectedItem = v,
+ defaultBindingMode: BindingMode.TwoWay);
///
/// Gets or sets the data item corresponding to the selected row.
@@ -582,7 +584,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register(nameof(AutoGenerateColumns));
///
- /// Gets or sets a value that indicates whether columns are created
+ /// Gets or sets a value that indicates whether columns are created
/// automatically when the property is set.
///
public bool AutoGenerateColumns
@@ -626,7 +628,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register(nameof(AreRowDetailsFrozen));
///
- /// Gets or sets a value that indicates whether the row details sections remain
+ /// Gets or sets a value that indicates whether the row details sections remain
/// fixed at the width of the display area or can scroll horizontally.
///
public bool AreRowDetailsFrozen
@@ -881,7 +883,7 @@ namespace Avalonia.Controls
{
int index = (int)e.NewValue;
- // GetDataItem returns null if index is >= Count, we do not check newValue
+ // GetDataItem returns null if index is >= Count, we do not check newValue
// against Count here to avoid enumerating through an Enumerable twice
// Setting SelectedItem coerces the finally value of the SelectedIndex
object newSelectedItem = (index < 0) ? null : DataConnection.GetDataItem(index);
@@ -910,6 +912,11 @@ namespace Avalonia.Controls
// Clear all row selections
ClearRowSelection(resetAnchorSlot: true);
+
+ if (DataConnection.CollectionView != null)
+ {
+ DataConnection.CollectionView.MoveCurrentTo(null);
+ }
}
else
{
@@ -1168,14 +1175,14 @@ namespace Avalonia.Controls
}
///
- /// Occurs one time for each public, non-static property in the bound data type when the
- /// property is changed and the
+ /// Occurs one time for each public, non-static property in the bound data type when the
+ /// property is changed and the
/// property is true.
///
public event EventHandler AutoGeneratingColumn;
///
- /// Occurs before a cell or row enters editing mode.
+ /// Occurs before a cell or row enters editing mode.
///
public event EventHandler BeginningEdit;
@@ -1195,7 +1202,7 @@ namespace Avalonia.Controls
public event EventHandler CellPointerPressed;
///
- /// Occurs when the
+ /// Occurs when the
/// property of a column changes.
///
public event EventHandler ColumnDisplayIndexChanged;
@@ -1218,14 +1225,14 @@ namespace Avalonia.Controls
public event EventHandler CurrentCellChanged;
///
- /// Occurs after a
+ /// Occurs after a
/// is instantiated, so that you can customize it before it is used.
///
public event EventHandler LoadingRow;
///
/// Occurs when a cell in a enters editing mode.
- ///
+ ///
///
public event EventHandler PreparingCellForEdit;
@@ -1243,7 +1250,7 @@ namespace Avalonia.Controls
RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble);
///
- /// Occurs when the or
+ /// Occurs when the or
/// property value changes.
///
public event EventHandler SelectionChanged
@@ -1258,19 +1265,19 @@ namespace Avalonia.Controls
public event EventHandler Sorting;
///
- /// Occurs when a
+ /// Occurs when a
/// object becomes available for reuse.
///
public event EventHandler UnloadingRow;
///
- /// Occurs when a new row details template is applied to a row, so that you can customize
+ /// Occurs when a new row details template is applied to a row, so that you can customize
/// the details section before it is used.
///
public event EventHandler LoadingRowDetails;
///
- /// Occurs when the
+ /// Occurs when the
/// property value changes.
///
public event EventHandler RowDetailsVisibilityChanged;
@@ -1282,7 +1289,7 @@ namespace Avalonia.Controls
///
/// Gets a collection that contains all the columns in the control.
- ///
+ ///
public ObservableCollection Columns
{
get
@@ -1456,7 +1463,7 @@ namespace Avalonia.Controls
}
// Height currently available for cells this value is smaller. This height is reduced by the existence of ColumnHeaders
- // or a horizontal scrollbar. Layout is asynchronous so changes to the ColumnHeaders or the horizontal scrollbar are
+ // or a horizontal scrollbar. Layout is asynchronous so changes to the ColumnHeaders or the horizontal scrollbar are
// not reflected immediately.
internal double CellsHeight
{
@@ -1555,7 +1562,7 @@ namespace Avalonia.Controls
internal static double HorizontalGridLinesThickness => DATAGRID_horizontalGridLinesThickness;
- // the sum of the widths in pixels of the scrolling columns preceding
+ // the sum of the widths in pixels of the scrolling columns preceding
// the first displayed scrolling column
internal double HorizontalOffset
{
@@ -2083,20 +2090,20 @@ namespace Avalonia.Controls
}
///
- /// Measures the children of a to prepare for
- /// arranging them during the
- /// pass.
+ /// Measures the children of a to prepare for
+ /// arranging them during the
+ /// pass.
///
///
/// The size that the determines it needs during layout, based on its calculations of child object allocated sizes.
///
///
- /// The available size that this element can give to child elements. Indicates an upper limit that
+ /// The available size that this element can give to child elements. Indicates an upper limit that
/// child elements should not exceed.
///
protected override Size MeasureOverride(Size availableSize)
{
- // Delay layout until after the initial measure to avoid invalid calculations when the
+ // Delay layout until after the initial measure to avoid invalid calculations when the
// DataGrid is not part of the visual tree
if (!_measured)
{
@@ -2285,6 +2292,17 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Comparator class so we can sort list by the display index
+ ///
+ public class DisplayIndexComparer : IComparer
+ {
+ int IComparer.Compare(DataGridColumn x, DataGridColumn y)
+ {
+ return (x.DisplayIndexWithFiller < y.DisplayIndexWithFiller) ? -1 : 1;
+ }
+ }
+
///
/// Builds the visual tree for the column header when a new template is applied.
///
@@ -2309,8 +2327,11 @@ namespace Avalonia.Controls
ColumnsInternal.FillerColumn.IsRepresented = false;
}
_columnHeadersPresenter.OwningGrid = this;
- // Columns were added before before our Template was applied, add the ColumnHeaders now
- foreach (DataGridColumn column in ColumnsItemsInternal)
+
+ // Columns were added before our Template was applied, add the ColumnHeaders now
+ List sortedInternal = new List(ColumnsItemsInternal);
+ sortedInternal.Sort(new DisplayIndexComparer());
+ foreach (DataGridColumn column in sortedInternal)
{
InsertDisplayedColumnHeader(column);
}
@@ -3006,7 +3027,7 @@ namespace Avalonia.Controls
/// If the editing element has focus, this method will set focus to the DataGrid itself
/// in order to force the element to lose focus. It will then wait for the editing element's
/// LostFocus event, at which point it will perform the specified action.
- ///
+ ///
/// NOTE: It is important to understand that the specified action will be performed when the editing
/// element loses focus only if this method returns true. If it returns false, then the action
/// will not be performed later on, and should instead be performed by the caller, if necessary.
@@ -3065,7 +3086,7 @@ namespace Avalonia.Controls
{
if (!_scrollingByHeight)
{
- // Update layout when RowDetails are expanded or collapsed, just updating the vertical scroll bar is not enough
+ // Update layout when RowDetails are expanded or collapsed, just updating the vertical scroll bar is not enough
// since rows could be added or removed
InvalidateMeasure();
}
@@ -3278,7 +3299,7 @@ namespace Avalonia.Controls
{
// Current cell was reset because the commit deleted row(s).
// Since the user wants to change the current cell, we don't
- // want to end up with no current cell. We pick the last row
+ // want to end up with no current cell. We pick the last row
// in the grid which may be the 'new row'.
int lastSlot = LastVisibleSlot;
if (forCurrentCellChange &&
@@ -3336,7 +3357,7 @@ namespace Avalonia.Controls
if (_ignoreNextScrollBarsLayout)
{
_ignoreNextScrollBarsLayout = false;
- //
+ //
}
@@ -3393,7 +3414,7 @@ namespace Avalonia.Controls
}
// Now cellsWidth is the width potentially available for displaying data cells.
- // Now cellsHeight is the height potentially available for displaying data cells.
+ // Now cellsHeight is the height potentially available for displaying data cells.
bool needHorizScrollbar = false;
bool needVertScrollbar = false;
@@ -3418,7 +3439,7 @@ namespace Avalonia.Controls
Debug.Assert(cellsHeight >= 0);
needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true;
- if (vertScrollBarWidth > 0 &&
+ if (vertScrollBarWidth > 0 &&
allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
MathUtilities.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth)))
{
@@ -3458,7 +3479,7 @@ namespace Avalonia.Controls
// we compute the number of visible columns only after we set up the vertical scroll bar.
ComputeDisplayedColumns();
- if ((vertScrollBarWidth > 0 || horizScrollBarHeight > 0) &&
+ if ((vertScrollBarWidth > 0 || horizScrollBarHeight > 0) &&
allowHorizScrollbar &&
needVertScrollbar && !needHorizScrollbar &&
MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
@@ -3963,7 +3984,8 @@ namespace Avalonia.Controls
{
var errorList =
binding.ValidationErrors
- .SelectMany(ex => ValidationUtil.UnpackException(ex))
+ .SelectMany(ValidationUtil.UnpackException)
+ .Select(ValidationUtil.UnpackDataValidationException)
.ToList();
DataValidationErrors.SetErrors(editingElement, errorList);
@@ -4124,7 +4146,7 @@ namespace Avalonia.Controls
}
///
- /// Exits editing mode without trying to commit or revert the editing, and
+ /// Exits editing mode without trying to commit or revert the editing, and
/// without repopulating the edited row's cell.
///
//TODO TabStop
@@ -5103,9 +5125,9 @@ namespace Avalonia.Controls
{
if (ctrl || _editingColumnIndex == -1 || IsReadOnly)
{
- //Go to the next/previous control on the page when
+ //Go to the next/previous control on the page when
// - Ctrl key is used
- // - Potential current cell is not edited, or the datagrid is read-only.
+ // - Potential current cell is not edited, or the datagrid is read-only.
return false;
}
@@ -5337,7 +5359,7 @@ namespace Avalonia.Controls
_focusedRow = null;
}
- private void SelectAll()
+ public void SelectAll()
{
SetRowsSelection(0, SlotCount - 1);
}
@@ -5516,11 +5538,11 @@ namespace Avalonia.Controls
// v---v
//|<|_____|###|>|
// ^ ^
- // min max
+ // min max
// we want to make the relative size of the thumb reflect the relative size of the viewing area
// viewportSize / (max + viewportSize) = cellsWidth / max
- // -> viewportSize = max * cellsWidth / (max - cellsWidth)
+ // -> viewportSize = max * cellsWidth / (max - cellsWidth)
// always zero
_hScrollBar.Minimum = 0;
@@ -5572,7 +5594,7 @@ namespace Avalonia.Controls
_hScrollBar.Maximum = 0;
if (_hScrollBar.IsVisible)
{
- // This will trigger a call to this method via Cells_SizeChanged for
+ // This will trigger a call to this method via Cells_SizeChanged for
// which no processing is needed.
_hScrollBar.IsVisible = false;
_ignoreNextScrollBarsLayout = true;
@@ -5591,14 +5613,14 @@ namespace Avalonia.Controls
// v---v
//|<|_____|###|>|
// ^ ^
- // min max
+ // min max
// we want to make the relative size of the thumb reflect the relative size of the viewing area
// viewportSize / (max + viewportSize) = cellsWidth / max
// -> viewportSize = max * cellsHeight / (totalVisibleHeight - cellsHeight)
// -> = max * cellsHeight / (totalVisibleHeight - cellsHeight)
// -> = max * cellsHeight / max
- // -> = cellsHeight
+ // -> = cellsHeight
// always zero
_vScrollBar.Minimum = 0;
@@ -5621,7 +5643,7 @@ namespace Avalonia.Controls
if (!_vScrollBar.IsVisible)
{
- // This will trigger a call to this method via Cells_SizeChanged for
+ // This will trigger a call to this method via Cells_SizeChanged for
// which no processing is needed.
_vScrollBar.IsVisible = true;
if (_vScrollBar.DesiredSize.Width == 0)
@@ -5637,7 +5659,7 @@ namespace Avalonia.Controls
_vScrollBar.Maximum = 0;
if (_vScrollBar.IsVisible)
{
- // This will trigger a call to this method via Cells_SizeChanged for
+ // This will trigger a call to this method via Cells_SizeChanged for
// which no processing is needed.
_vScrollBar.IsVisible = false;
_ignoreNextScrollBarsLayout = true;
@@ -5660,8 +5682,8 @@ namespace Avalonia.Controls
Debug.Assert(slot >= 0);
// Before changing selection, check if the current cell needs to be committed, and
- // check if the current row needs to be committed. If any of those two operations are required and fail,
- // do not change selection, and do not change current cell.
+ // check if the current row needs to be committed. If any of those two operations are required and fail,
+ // do not change selection, and do not change current cell.
bool wasInEdit = EditingColumnIndex != -1;
diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
index 1e72a07760..90401a00a2 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
@@ -10,7 +10,8 @@ using System.Reactive.Disposables;
using System.Reactive.Subjects;
using Avalonia.Reactive;
using System.Diagnostics;
-using Avalonia.Controls.Utils;
+using Avalonia.Controls.Utils;
+using Avalonia.Markup.Xaml.MarkupExtensions;
namespace Avalonia.Controls
{
@@ -47,14 +48,15 @@ namespace Avalonia.Controls
if (_binding != null)
{
- if(_binding is Avalonia.Data.Binding binding)
+ if(_binding is BindingBase binding)
{
if (binding.Mode == BindingMode.OneWayToSource)
{
throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead.");
}
- if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default)
+ var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
+ if (!string.IsNullOrEmpty(path) && binding.Mode == BindingMode.Default)
{
binding.Mode = BindingMode.TwoWay;
}
@@ -136,13 +138,16 @@ namespace Avalonia.Controls
internal void SetHeaderFromBinding()
{
if (OwningGrid != null && OwningGrid.DataConnection.DataType != null
- && Header == null && Binding != null && Binding is Binding binding
- && !String.IsNullOrWhiteSpace(binding.Path))
+ && Header == null && Binding != null && Binding is BindingBase binding)
{
- string header = OwningGrid.DataConnection.DataType.GetDisplayName(binding.Path);
- if (header != null)
+ var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
+ if (!string.IsNullOrWhiteSpace(path))
{
- Header = header;
+ var header = OwningGrid.DataConnection.DataType.GetDisplayName(path);
+ if (header != null)
+ {
+ Header = header;
+ }
}
}
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
index e2a067ac61..ccf1f3f77a 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
@@ -17,7 +17,6 @@ namespace Avalonia.Controls
///
public class DataGridCheckBoxColumn : DataGridBoundColumn
{
- private bool _beganEditWithKeyboard;
private CheckBox _currentCheckBox;
private DataGrid _owningGrid;
@@ -153,23 +152,7 @@ namespace Avalonia.Controls
{
if (editingElement is CheckBox editingCheckBox)
{
- bool? uneditedValue = editingCheckBox.IsChecked;
- bool editValue = false;
- if(editingEventArgs is PointerPressedEventArgs args)
- {
- // Editing was triggered by a mouse click
- Point position = args.GetPosition(editingCheckBox);
- Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
- editValue = rect.Contains(position);
- }
- else if (_beganEditWithKeyboard)
- {
- // Editing began by a user pressing spacebar
- editValue = true;
- _beganEditWithKeyboard = false;
- }
-
- if (editValue)
+ void EditValue()
{
// User clicked the checkbox itself or pressed space, let's toggle the IsChecked value
if (editingCheckBox.IsThreeState)
@@ -192,6 +175,40 @@ namespace Avalonia.Controls
editingCheckBox.IsChecked = !editingCheckBox.IsChecked;
}
}
+
+ bool? uneditedValue = editingCheckBox.IsChecked;
+ if(editingEventArgs is PointerPressedEventArgs args)
+ {
+ void ProcessPointerArgs()
+ {
+ // Editing was triggered by a mouse click
+ Point position = args.GetPosition(editingCheckBox);
+ Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
+ if(rect.Contains(position))
+ {
+ EditValue();
+ }
+ }
+
+ void OnLayoutUpdated(object sender, EventArgs e)
+ {
+ if(!editingCheckBox.Bounds.IsEmpty)
+ {
+ editingCheckBox.LayoutUpdated -= OnLayoutUpdated;
+ ProcessPointerArgs();
+ }
+ }
+
+ if(editingCheckBox.Bounds.IsEmpty)
+ {
+ editingCheckBox.LayoutUpdated += OnLayoutUpdated;
+ }
+ else
+ {
+ ProcessPointerArgs();
+ }
+ }
+
return uneditedValue;
}
return false;
@@ -284,13 +301,10 @@ namespace Avalonia.Controls
CheckBox checkBox = GetCellContent(row) as CheckBox;
if (checkBox == _currentCheckBox)
{
- _beganEditWithKeyboard = true;
OwningGrid.BeginEdit();
- return;
}
}
}
- _beganEditWithKeyboard = false;
}
private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e)
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
index 92ddd4e736..9141fb2463 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
@@ -12,6 +12,7 @@ using System;
using System.Linq;
using System.Diagnostics;
using Avalonia.Controls.Utils;
+using Avalonia.Markup.Xaml.MarkupExtensions;
namespace Avalonia.Controls
{
@@ -1008,6 +1009,14 @@ namespace Avalonia.Controls
get;
set;
}
+ ///
+ /// Holds a Comparer to use for sorting, if not using the default.
+ ///
+ public System.Collections.IComparer CustomSortComparer
+ {
+ get;
+ set;
+ }
///
/// We get the sort description from the data source. We don't worry whether we can modify sort -- perhaps the sort description
@@ -1019,6 +1028,14 @@ namespace Avalonia.Controls
&& OwningGrid.DataConnection != null
&& OwningGrid.DataConnection.SortDescriptions != null)
{
+ if(CustomSortComparer != null)
+ {
+ return
+ OwningGrid.DataConnection.SortDescriptions
+ .OfType()
+ .FirstOrDefault(s => s.SourceComparer == CustomSortComparer);
+ }
+
string propertyName = GetSortPropertyName();
return OwningGrid.DataConnection.SortDescriptions.FirstOrDefault(s => s.HasPropertyPath && s.PropertyPath == propertyName);
@@ -1033,13 +1050,16 @@ namespace Avalonia.Controls
if (String.IsNullOrEmpty(result))
{
-
- if(this is DataGridBoundColumn boundColumn &&
- boundColumn.Binding != null &&
- boundColumn.Binding is Binding binding &&
- binding.Path != null)
+ if (this is DataGridBoundColumn boundColumn)
{
- result = binding.Path;
+ if (boundColumn.Binding is Binding binding)
+ {
+ result = binding.Path;
+ }
+ else if (boundColumn.Binding is CompiledBindingExtension compiledBinding)
+ {
+ result = compiledBinding.Path.ToString();
+ }
}
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
index 7f8d205949..6f957497cb 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
@@ -274,6 +274,12 @@ namespace Avalonia.Controls
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
}
+ else if (OwningColumn.CustomSortComparer != null)
+ {
+ newSort = DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer);
+
+ owningGrid.DataConnection.SortDescriptions.Add(newSort);
+ }
else
{
string propertyName = OwningColumn.GetSortPropertyName();
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
index 46bcd0d347..a4577ee952 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
@@ -5,6 +5,7 @@
using Avalonia.Controls.Utils;
using Avalonia.Data;
+using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
@@ -141,9 +142,9 @@ namespace Avalonia.Controls
Debug.Assert(dataGridColumn != null);
if (dataGridColumn is DataGridBoundColumn dataGridBoundColumn &&
- dataGridBoundColumn.Binding is Binding binding)
+ dataGridBoundColumn.Binding is BindingBase binding)
{
- string path = binding.Path;
+ var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
if (string.IsNullOrWhiteSpace(path))
{
diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
index e513a7b678..7e95dd100c 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
@@ -8,6 +8,7 @@ using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
+using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
@@ -22,6 +23,7 @@ namespace Avalonia.Controls
o => o.CellTemplate,
(o, v) => o.CellTemplate = v);
+ [Content]
public IDataTemplate CellTemplate
{
get { return _cellTemplate; }
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
index 6ad58bf250..0d19f4c479 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
@@ -3,14 +3,15 @@
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
-using Avalonia.Media;
using System;
using System.Diagnostics;
+using Avalonia.Layout;
+using Avalonia.Media;
namespace Avalonia.Controls.Primitives
{
///
- /// Used within the template of a to specify the
+ /// Used within the template of a to specify the
/// location in the control's visual tree where the rows are to be added.
///
public sealed class DataGridRowsPresenter : Panel
@@ -22,9 +23,10 @@ namespace Avalonia.Controls.Primitives
}
private double _measureHeightOffset = 0;
+
private double CalculateEstimatedAvailableHeight(Size availableSize)
{
- if(!Double.IsPositiveInfinity(availableSize.Height))
+ if (!Double.IsPositiveInfinity(availableSize.Height))
{
return availableSize.Height + _measureHeightOffset;
}
@@ -50,10 +52,10 @@ namespace Avalonia.Controls.Primitives
return base.ArrangeOverride(finalSize);
}
- if(OwningGrid.RowsPresenterAvailableSize.HasValue)
+ if (OwningGrid.RowsPresenterAvailableSize.HasValue)
{
var availableHeight = OwningGrid.RowsPresenterAvailableSize.Value.Height;
- if(!Double.IsPositiveInfinity(availableHeight))
+ if (!Double.IsPositiveInfinity(availableHeight))
{
_measureHeightOffset = finalSize.Height - availableHeight;
OwningGrid.RowsPresenterEstimatedAvailableHeight = finalSize.Height;
@@ -108,6 +110,18 @@ namespace Avalonia.Controls.Primitives
///
protected override Size MeasureOverride(Size availableSize)
{
+ if (double.IsInfinity(availableSize.Height))
+ {
+ if (VisualRoot is TopLevel topLevel)
+ {
+ double maxHeight = topLevel.IsArrangeValid ?
+ topLevel.Bounds.Height :
+ LayoutHelper.ApplyLayoutConstraints(topLevel, availableSize).Height;
+
+ availableSize = availableSize.WithHeight(maxHeight);
+ }
+ }
+
if (availableSize.Height == 0 || OwningGrid == null)
{
return base.MeasureOverride(availableSize);
diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
index cf062e0920..09d19c8e43 100644
--- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
+++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
@@ -137,12 +137,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
similarity index 90%
rename from src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs
rename to src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
index c7611c8c46..e5b3b080e2 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
@@ -1,21 +1,9 @@
using Avalonia.Controls;
using Avalonia.Data;
-using Avalonia.Data.Converters;
using Avalonia.Media;
-namespace Avalonia.Diagnostics.Views
+namespace Avalonia.Diagnostics.Controls
{
- internal static class Converters
- {
- public static IValueConverter HasConstraintConverter =
- new FuncValueConverter(ConvertToDecoration);
-
- private static TextDecorationCollection ConvertToDecoration(object arg)
- {
- return arg != null ? TextDecorations.Underline : null;
- }
- }
-
internal class ThicknessEditor : ContentControl
{
public static readonly DirectProperty ThicknessProperty =
@@ -44,7 +32,9 @@ namespace Avalonia.Diagnostics.Views
AvaloniaProperty.RegisterDirect(nameof(Bottom), o => o.Bottom,
(o, v) => o.Bottom = v);
-
+ public static readonly StyledProperty HighlightProperty =
+ AvaloniaProperty.Register(nameof(Highlight));
+
private Thickness _thickness;
private string _header;
private bool _isPresent = true;
@@ -52,7 +42,6 @@ namespace Avalonia.Diagnostics.Views
private double _top;
private double _right;
private double _bottom;
-
private bool _isUpdatingThickness;
public Thickness Thickness
@@ -97,6 +86,12 @@ namespace Avalonia.Diagnostics.Views
set => SetAndRaise(BottomProperty, ref _bottom, value);
}
+ public IBrush Highlight
+ {
+ get => GetValue(HighlightProperty);
+ set => SetValue(HighlightProperty, value);
+ }
+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
similarity index 70%
rename from src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
rename to src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
index 37ba5155fd..63ac3ab62f 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
@@ -1,17 +1,16 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
-using Avalonia.Media;
namespace Avalonia.Diagnostics.Converters
{
- internal class BoolToBrushConverter : IValueConverter
+ internal class BoolToOpacityConverter : IValueConverter
{
- public IBrush Brush { get; set; }
+ public double Opacity { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
- return (bool)value ? Brush : Brushes.Transparent;
+ return (bool)value ? 1d : Opacity;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs
new file mode 100644
index 0000000000..8d10981ba7
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Globalization;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+
+namespace Avalonia.Diagnostics.Converters
+{
+ internal class EnumToCheckedConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return Equals(value, parameter);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool isChecked && isChecked)
+ {
+ return parameter;
+ }
+
+ return BindingOperations.DoNothing;
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
index 4899be2955..7942d22962 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
@@ -6,6 +6,8 @@ using Avalonia.Diagnostics.Views;
using Avalonia.Input;
using Avalonia.Interactivity;
+#nullable enable
+
namespace Avalonia.Diagnostics
{
public static class DevTools
@@ -13,12 +15,20 @@ namespace Avalonia.Diagnostics
private static readonly Dictionary s_open = new Dictionary();
public static IDisposable Attach(TopLevel root, KeyGesture gesture)
+ {
+ return Attach(root, new DevToolsOptions()
+ {
+ Gesture = gesture,
+ });
+ }
+
+ public static IDisposable Attach(TopLevel root, DevToolsOptions options)
{
void PreviewKeyDown(object sender, KeyEventArgs e)
{
- if (gesture.Matches(e))
+ if (options.Gesture.Matches(e))
{
- Open(root);
+ Open(root, options);
}
}
@@ -28,7 +38,9 @@ namespace Avalonia.Diagnostics
RoutingStrategies.Tunnel);
}
- public static IDisposable Open(TopLevel root)
+ public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions());
+
+ public static IDisposable Open(TopLevel root, DevToolsOptions options)
{
if (s_open.TryGetValue(root, out var window))
{
@@ -38,15 +50,15 @@ namespace Avalonia.Diagnostics
{
window = new MainWindow
{
- Width = 1024,
- Height = 512,
Root = root,
+ Width = options.Size.Width,
+ Height = options.Size.Height,
};
window.Closed += DevToolsClosed;
s_open.Add(root, window);
- if (root is Window inspectedWindow)
+ if (options.ShowAsChildWindow && root is Window inspectedWindow)
{
window.Show(inspectedWindow);
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
new file mode 100644
index 0000000000..f9978f3b7e
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
@@ -0,0 +1,26 @@
+using Avalonia.Input;
+
+namespace Avalonia.Diagnostics
+{
+ ///
+ /// Describes options used to customize DevTools.
+ ///
+ public class DevToolsOptions
+ {
+ ///
+ /// Gets or sets the key gesture used to open DevTools.
+ ///
+ public KeyGesture Gesture { get; set; } = new KeyGesture(Key.F12);
+
+ ///
+ /// Gets or sets a value indicating whether DevTools should be displayed as a child window
+ /// of the window being inspected. The default value is true.
+ ///
+ public bool ShowAsChildWindow { get; set; } = true;
+
+ ///
+ /// Gets or sets the initial size of the DevTools window. The default value is 1280x720.
+ ///
+ public Size Size { get; set; } = new Size(1280, 720);
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
index 65117dc3d1..36fe12d89c 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
@@ -16,6 +16,8 @@ namespace Avalonia.Diagnostics.Models
public object Handler { get; }
+ public bool BeginsNewRoute { get; set; }
+
public string HandlerName
{
get
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
index fa41eacbeb..32592559e5 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
@@ -1,8 +1,15 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
+using System.Reflection;
using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
@@ -12,6 +19,10 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IVisual _control;
private readonly IDictionary> _propertyIndex;
private AvaloniaPropertyViewModel _selectedProperty;
+ private string _styleFilter;
+ private bool _snapshotStyles;
+ private bool _showInactiveStyles;
+ private string _styleStatus;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{
@@ -43,20 +54,160 @@ namespace Avalonia.Diagnostics.ViewModels
{
ao.PropertyChanged += ControlPropertyChanged;
}
+
+ AppliedStyles = new ObservableCollection();
+ PseudoClasses = new ObservableCollection();
+
+ if (control is StyledElement styledElement)
+ {
+ styledElement.Classes.CollectionChanged += OnClassesChanged;
+
+ var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes(true);
+
+ foreach (var classAttribute in pseudoClassAttributes)
+ {
+ foreach (var className in classAttribute.PseudoClasses)
+ {
+ PseudoClasses.Add(new PseudoClassViewModel(className, styledElement));
+ }
+ }
+
+ var styleDiagnostics = styledElement.GetStyleDiagnostics();
+
+ foreach (var appliedStyle in styleDiagnostics.AppliedStyles)
+ {
+ var styleSource = appliedStyle.Source;
+
+ var setters = new List();
+
+ if (styleSource is Style style)
+ {
+ foreach (var setter in style.Setters)
+ {
+ if (setter is Setter regularSetter)
+ {
+ var setterValue = regularSetter.Value;
+
+ var resourceInfo = GetResourceInfo(setterValue);
+
+ SetterViewModel setterVm;
+
+ if (resourceInfo.HasValue)
+ {
+ var resourceKey = resourceInfo.Value.resourceKey;
+ var resourceValue = styledElement.FindResource(resourceKey);
+
+ setterVm = new ResourceSetterViewModel(regularSetter.Property, resourceKey, resourceValue, resourceInfo.Value.isDynamic);
+ }
+ else
+ {
+ setterVm = new SetterViewModel(regularSetter.Property, setterValue);
+ }
+
+ setters.Add(setterVm);
+ }
+ }
+
+ AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters));
+ }
+ }
+
+ UpdateStyles();
+ }
+ }
+
+ private (object resourceKey, bool isDynamic)? GetResourceInfo(object value)
+ {
+ if (value is StaticResourceExtension staticResource)
+ {
+ return (staticResource.ResourceKey, false);
+ }
+ else if (value is DynamicResourceExtension dynamicResource)
+ {
+ return (dynamicResource.ResourceKey, true);
+ }
+
+ return null;
}
public TreePageViewModel TreePage { get; }
public DataGridCollectionView PropertiesView { get; }
+ public ObservableCollection AppliedStyles { get; }
+
+ public ObservableCollection PseudoClasses { get; }
+
public AvaloniaPropertyViewModel SelectedProperty
{
get => _selectedProperty;
set => RaiseAndSetIfChanged(ref _selectedProperty, value);
}
-
+
+ public string StyleFilter
+ {
+ get => _styleFilter;
+ set => RaiseAndSetIfChanged(ref _styleFilter, value);
+ }
+
+ public bool SnapshotStyles
+ {
+ get => _snapshotStyles;
+ set => RaiseAndSetIfChanged(ref _snapshotStyles, value);
+ }
+
+ public bool ShowInactiveStyles
+ {
+ get => _showInactiveStyles;
+ set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
+ }
+
+ public string StyleStatus
+ {
+ get => _styleStatus;
+ set => RaiseAndSetIfChanged(ref _styleStatus, value);
+ }
+
public ControlLayoutViewModel Layout { get; }
+ protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+ {
+ base.OnPropertyChanged(e);
+
+ if (e.PropertyName == nameof(StyleFilter))
+ {
+ UpdateStyleFilters();
+ }
+ else if (e.PropertyName == nameof(SnapshotStyles))
+ {
+ if (!SnapshotStyles)
+ {
+ UpdateStyles();
+ }
+ }
+ }
+
+ private void UpdateStyleFilters()
+ {
+ var filter = StyleFilter;
+ bool hasFilter = !string.IsNullOrEmpty(filter);
+
+ foreach (var style in AppliedStyles)
+ {
+ var hasVisibleSetter = false;
+
+ foreach (var setter in style.Setters)
+ {
+ setter.IsVisible =
+ !hasFilter || setter.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
+
+ hasVisibleSetter |= setter.IsVisible;
+ }
+
+ style.IsVisible = hasVisibleSetter;
+ }
+ }
+
public void Dispose()
{
if (_control is INotifyPropertyChanged inpc)
@@ -68,6 +219,11 @@ namespace Avalonia.Diagnostics.ViewModels
{
ao.PropertyChanged -= ControlPropertyChanged;
}
+
+ if (_control is StyledElement se)
+ {
+ se.Classes.CollectionChanged -= OnClassesChanged;
+ }
}
private IEnumerable GetAvaloniaProperties(object o)
@@ -129,6 +285,74 @@ namespace Avalonia.Diagnostics.ViewModels
property.Update();
}
}
+
+ if (!SnapshotStyles)
+ {
+ UpdateStyles();
+ }
+ }
+
+ private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (!SnapshotStyles)
+ {
+ UpdateStyles();
+ }
+ }
+
+ private void UpdateStyles()
+ {
+ int activeCount = 0;
+
+ foreach (var style in AppliedStyles)
+ {
+ style.Update();
+
+ if (style.IsActive)
+ {
+ activeCount++;
+ }
+ }
+
+ var propertyBuckets = new Dictionary>();
+
+ foreach (var style in AppliedStyles)
+ {
+ if (!style.IsActive)
+ {
+ continue;
+ }
+
+ foreach (var setter in style.Setters)
+ {
+ if (propertyBuckets.TryGetValue(setter.Property, out var setters))
+ {
+ foreach (var otherSetter in setters)
+ {
+ otherSetter.IsActive = false;
+ }
+
+ setter.IsActive = true;
+
+ setters.Add(setter);
+ }
+ else
+ {
+ setter.IsActive = true;
+
+ setters = new List { setter };
+
+ propertyBuckets.Add(setter.Property, setters);
+ }
+ }
+ }
+
+ foreach (var pseudoClass in PseudoClasses)
+ {
+ pseudoClass.Update();
+ }
+
+ StyleStatus = $"Styles ({activeCount}/{AppliedStyles.Count} active)";
}
private bool FilterProperty(object arg)
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
index fd2e4c3355..b0718bc6ce 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
@@ -1,4 +1,6 @@
+using System;
using System.ComponentModel;
+using System.Text;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.VisualTree;
@@ -8,27 +10,58 @@ namespace Avalonia.Diagnostics.ViewModels
internal class ControlLayoutViewModel : ViewModelBase
{
private readonly IVisual _control;
- private Thickness _marginThickness;
private Thickness _borderThickness;
- private Thickness _paddingThickness;
- private double _width;
private double _height;
- private string _widthConstraint;
private string _heightConstraint;
+ private HorizontalAlignment _horizontalAlignment;
+ private Thickness _marginThickness;
+ private Thickness _paddingThickness;
private bool _updatingFromControl;
+ private VerticalAlignment _verticalAlignment;
+ private double _width;
+ private string _widthConstraint;
+
+ public ControlLayoutViewModel(IVisual control)
+ {
+ _control = control;
+
+ HasPadding = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Decorator.PaddingProperty);
+ HasBorder = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Border.BorderThicknessProperty);
+
+ if (control is AvaloniaObject ao)
+ {
+ MarginThickness = ao.GetValue(Layoutable.MarginProperty);
+
+ if (HasPadding)
+ {
+ PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
+ }
+
+ if (HasBorder)
+ {
+ BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
+ }
+
+ HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty);
+ VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty);
+ }
+
+ UpdateSize();
+ UpdateSizeConstraints();
+ }
public Thickness MarginThickness
{
get => _marginThickness;
set => RaiseAndSetIfChanged(ref _marginThickness, value);
}
-
+
public Thickness BorderThickness
{
get => _borderThickness;
set => RaiseAndSetIfChanged(ref _borderThickness, value);
}
-
+
public Thickness PaddingThickness
{
get => _paddingThickness;
@@ -59,35 +92,21 @@ namespace Avalonia.Diagnostics.ViewModels
private set => RaiseAndSetIfChanged(ref _heightConstraint, value);
}
- public bool HasPadding { get; }
-
- public bool HasBorder { get; }
-
- public ControlLayoutViewModel(IVisual control)
+ public HorizontalAlignment HorizontalAlignment
{
- _control = control;
-
- HasPadding = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Decorator.PaddingProperty);
- HasBorder = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Border.BorderThicknessProperty);
-
- if (control is AvaloniaObject ao)
- {
- MarginThickness = ao.GetValue(Layoutable.MarginProperty);
+ get => _horizontalAlignment;
+ private set => RaiseAndSetIfChanged(ref _horizontalAlignment, value);
+ }
- if (HasPadding)
- {
- PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
- }
+ public VerticalAlignment VerticalAlignment
+ {
+ get => _verticalAlignment;
+ private set => RaiseAndSetIfChanged(ref _verticalAlignment, value);
+ }
- if (HasBorder)
- {
- BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
- }
- }
+ public bool HasPadding { get; }
- UpdateSize();
- UpdateSizeConstraints();
- }
+ public bool HasBorder { get; }
private void UpdateSizeConstraints()
{
@@ -95,12 +114,27 @@ namespace Avalonia.Diagnostics.ViewModels
{
string CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty)
{
- if (ao.IsSet(minProperty) || ao.IsSet(maxProperty))
+ bool hasMin = ao.IsSet(minProperty);
+ bool hasMax = ao.IsSet(maxProperty);
+
+ if (hasMin || hasMax)
{
- var minValue = ao.GetValue(minProperty);
- var maxValue = ao.GetValue(maxProperty);
+ var builder = new StringBuilder();
+
+ if (hasMin)
+ {
+ var minValue = ao.GetValue(minProperty);
+ builder.AppendFormat("Min: {0}", Math.Round(minValue, 2));
+ builder.AppendLine();
+ }
- return $"{minValue} < size < {maxValue}";
+ if (hasMax)
+ {
+ var maxValue = ao.GetValue(maxProperty);
+ builder.AppendFormat("Max: {0}", Math.Round(maxValue, 2));
+ }
+
+ return builder.ToString();
}
return null;
@@ -134,6 +168,14 @@ namespace Avalonia.Diagnostics.ViewModels
{
ao.SetValue(Border.BorderThicknessProperty, BorderThickness);
}
+ else if (e.PropertyName == nameof(HorizontalAlignment))
+ {
+ ao.SetValue(Layoutable.HorizontalAlignmentProperty, HorizontalAlignment);
+ }
+ else if (e.PropertyName == nameof(VerticalAlignment))
+ {
+ ao.SetValue(Layoutable.VerticalAlignmentProperty, VerticalAlignment);
+ }
}
}
@@ -154,15 +196,15 @@ namespace Avalonia.Diagnostics.ViewModels
if (e.Property == Layoutable.MarginProperty)
{
MarginThickness = ao.GetValue(Layoutable.MarginProperty);
- }
+ }
else if (e.Property == Decorator.PaddingProperty)
{
PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
- }
+ }
else if (e.Property == Border.BorderThicknessProperty)
{
BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
- }
+ }
else if (e.Property == Layoutable.MinWidthProperty ||
e.Property == Layoutable.MaxWidthProperty ||
e.Property == Layoutable.MinHeightProperty ||
@@ -170,6 +212,14 @@ namespace Avalonia.Diagnostics.ViewModels
{
UpdateSizeConstraints();
}
+ else if (e.Property == Layoutable.HorizontalAlignmentProperty)
+ {
+ HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty);
+ }
+ else if (e.Property == Layoutable.VerticalAlignmentProperty)
+ {
+ VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty);
+ }
}
}
}
@@ -183,8 +233,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
var size = _control.Bounds;
- Width = size.Width;
- Height = size.Height;
+ Width = Math.Round(size.Width, 2);
+ Height = Math.Round(size.Height, 2);
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
index 60f247ead1..b56374d353 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
@@ -2,25 +2,17 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
-using Avalonia.Controls;
-using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventOwnerTreeNode : EventTreeNodeBase
{
- private static readonly RoutedEvent[] s_defaultEvents =
- {
- Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent,
- InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
- };
-
public EventOwnerTreeNode(Type type, IEnumerable events, EventsPageViewModel vm)
: base(null, type.Name)
{
Children = new AvaloniaList(events.OrderBy(e => e.Name)
- .Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
+ .Select(e => new EventTreeNode(this, e, vm)));
IsExpanded = true;
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
index 9291b3bdf7..ea54302ebd 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
@@ -9,7 +9,6 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventTreeNode : EventTreeNodeBase
{
- private readonly RoutedEvent _event;
private readonly EventsPageViewModel _parentViewModel;
private bool _isRegistered;
private FiredEvent _currentEvent;
@@ -20,10 +19,12 @@ namespace Avalonia.Diagnostics.ViewModels
Contract.Requires(@event != null);
Contract.Requires(vm != null);
- _event = @event;
+ Event = @event;
_parentViewModel = vm;
}
+ public RoutedEvent Event { get; }
+
public override bool? IsEnabled
{
get => base.IsEnabled;
@@ -53,8 +54,10 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (IsEnabled.GetValueOrDefault() && !_isRegistered)
{
+ var allRoutes = RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble;
+
// FIXME: This leaks event handlers.
- _event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true);
+ Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true);
_isRegistered = true;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
index 1bd4117f23..c27cad29e8 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
@@ -8,11 +8,13 @@ namespace Avalonia.Diagnostics.ViewModels
internal bool _updateParent = true;
private bool _isExpanded;
private bool? _isEnabled = false;
+ private bool _isVisible;
protected EventTreeNodeBase(EventTreeNodeBase parent, string text)
{
Parent = parent;
Text = text;
+ IsVisible = true;
}
public IAvaloniaReadOnlyList Children
@@ -33,6 +35,12 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _isEnabled, value);
}
+ public bool IsVisible
+ {
+ get => _isVisible;
+ set => RaiseAndSetIfChanged(ref _isVisible, value);
+ }
+
public EventTreeNodeBase Parent
{
get;
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
index 361e82bc73..dd85fcf14c 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
@@ -1,28 +1,43 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Globalization;
+using System.ComponentModel;
using System.Linq;
using Avalonia.Controls;
-using Avalonia.Data.Converters;
+using Avalonia.Diagnostics.Models;
+using Avalonia.Input;
using Avalonia.Interactivity;
-using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventsPageViewModel : ViewModelBase
{
- private readonly IControl _root;
+ private static readonly HashSet s_defaultEvents = new HashSet()
+ {
+ Button.ClickEvent,
+ InputElement.KeyDownEvent,
+ InputElement.KeyUpEvent,
+ InputElement.TextInputEvent,
+ InputElement.PointerReleasedEvent,
+ InputElement.PointerPressedEvent
+ };
+
+ private readonly MainViewModel _mainViewModel;
+ private string _eventTypeFilter;
private FiredEvent _selectedEvent;
+ private EventTreeNodeBase _selectedNode;
- public EventsPageViewModel(IControl root)
+ public EventsPageViewModel(MainViewModel mainViewModel)
{
- _root = root;
+ _mainViewModel = mainViewModel;
Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
.GroupBy(e => e.OwnerType)
.OrderBy(e => e.Key.Name)
.Select(g => new EventOwnerTreeNode(g.Key, g, this))
.ToArray();
+
+ EnableDefault();
}
public string Name => "Events";
@@ -37,9 +52,140 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _selectedEvent, value);
}
- private void Clear()
+ public EventTreeNodeBase SelectedNode
+ {
+ get => _selectedNode;
+ set => RaiseAndSetIfChanged(ref _selectedNode, value);
+ }
+
+ public string EventTypeFilter
+ {
+ get => _eventTypeFilter;
+ set => RaiseAndSetIfChanged(ref _eventTypeFilter, value);
+ }
+
+ public void Clear()
{
RecordedEvents.Clear();
}
+
+ public void DisableAll()
+ {
+ EvaluateNodeEnabled(_ => false);
+ }
+
+ public void EnableDefault()
+ {
+ EvaluateNodeEnabled(node => s_defaultEvents.Contains(node.Event));
+ }
+
+ public void RequestTreeNavigateTo(EventChainLink navTarget)
+ {
+ if (navTarget.Handler is IControl control)
+ {
+ _mainViewModel.RequestTreeNavigateTo(control, true);
+ }
+ }
+
+ public void SelectEventByType(RoutedEvent evt)
+ {
+ foreach (var node in Nodes)
+ {
+ var result = FindNode(node, evt);
+
+ if (result != null && result.IsVisible)
+ {
+ SelectedNode = result;
+
+ break;
+ }
+ }
+
+ static EventTreeNodeBase FindNode(EventTreeNodeBase node, RoutedEvent eventType)
+ {
+ if (node is EventTreeNode eventNode && eventNode.Event == eventType)
+ {
+ return node;
+ }
+
+ if (node.Children != null)
+ {
+ foreach (var child in node.Children)
+ {
+ var result = FindNode(child, eventType);
+
+ if (result != null)
+ {
+ return result;
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+ {
+ base.OnPropertyChanged(e);
+
+ if (e.PropertyName == nameof(EventTypeFilter))
+ {
+ UpdateEventFilters();
+ }
+ }
+
+ private void EvaluateNodeEnabled(Func eval)
+ {
+ void ProcessNode(EventTreeNodeBase node)
+ {
+ if (node is EventTreeNode eventNode)
+ {
+ node.IsEnabled = eval(eventNode);
+ }
+
+ if (node.Children != null)
+ {
+ foreach (var childNode in node.Children)
+ {
+ ProcessNode(childNode);
+ }
+ }
+ }
+
+ foreach (var node in Nodes)
+ {
+ ProcessNode(node);
+ }
+ }
+
+ private void UpdateEventFilters()
+ {
+ var filter = EventTypeFilter;
+ bool hasFilter = !string.IsNullOrEmpty(filter);
+
+ foreach (var node in Nodes)
+ {
+ FilterNode(node, false);
+ }
+
+ bool FilterNode(EventTreeNodeBase node, bool isParentVisible)
+ {
+ bool matchesFilter = !hasFilter || node.Text.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
+ bool hasVisibleChild = false;
+
+ if (node.Children != null)
+ {
+ foreach (var childNode in node.Children)
+ {
+ hasVisibleChild |= FilterNode(childNode, matchesFilter);
+ }
+ }
+
+ node.IsVisible = hasVisibleChild || matchesFilter || isParentVisible;
+
+ return node.IsVisible;
+ }
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
index ae53cf6154..5fb528eead 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
@@ -62,13 +62,18 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- public void AddToChain(object handler, bool handled, RoutingStrategies route)
- {
- AddToChain(new EventChainLink(handler, handled, route));
- }
-
public void AddToChain(EventChainLink link)
{
+ if (EventChain.Count > 0)
+ {
+ var prevLink = EventChain[EventChain.Count-1];
+
+ if (prevLink.Route != link.Route)
+ {
+ link.BeginsNewRoute = true;
+ }
+ }
+
EventChain.Add(link);
if (HandledBy == null && link.Handled)
HandledBy = link;
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
index bf7d0e232a..49263eafdc 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
@@ -4,6 +4,7 @@ using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Threading;
+using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
@@ -27,7 +28,7 @@ namespace Avalonia.Diagnostics.ViewModels
_root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root));
- _events = new EventsPageViewModel(root);
+ _events = new EventsPageViewModel(this);
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged;
@@ -163,6 +164,14 @@ namespace Avalonia.Diagnostics.ViewModels
tree?.SelectControl(control);
}
+ public void EnableSnapshotStyles(bool enable)
+ {
+ if (Content is TreePageViewModel treeVm && treeVm.Details != null)
+ {
+ treeVm.Details.SnapshotStyles = enable;
+ }
+ }
+
public void Dispose()
{
KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged;
@@ -185,5 +194,19 @@ namespace Avalonia.Diagnostics.ViewModels
UpdateFocusedControl();
}
}
+
+ public void RequestTreeNavigateTo(IControl control, bool isVisualTree)
+ {
+ var tree = isVisualTree ? _visualTree : _logicalTree;
+
+ var node = tree.FindNode(control);
+
+ if (node != null)
+ {
+ SelectedTab = isVisualTree ? 1 : 0;
+
+ tree.SelectControl(control);
+ }
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs
new file mode 100644
index 0000000000..69126c2e2f
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs
@@ -0,0 +1,51 @@
+using Avalonia.Controls;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class PseudoClassViewModel : ViewModelBase
+ {
+ private readonly IPseudoClasses _pseudoClasses;
+ private readonly StyledElement _source;
+ private bool _isActive;
+ private bool _isUpdating;
+
+ public PseudoClassViewModel(string name, StyledElement source)
+ {
+ Name = name;
+ _source = source;
+ _pseudoClasses = _source.Classes;
+
+ Update();
+ }
+
+ public string Name { get; }
+
+ public bool IsActive
+ {
+ get => _isActive;
+ set
+ {
+ RaiseAndSetIfChanged(ref _isActive, value);
+
+ if (!_isUpdating)
+ {
+ _pseudoClasses.Set(Name, value);
+ }
+ }
+ }
+
+ public void Update()
+ {
+ try
+ {
+ _isUpdating = true;
+
+ IsActive = _source.Classes.Contains(Name);
+ }
+ finally
+ {
+ _isUpdating = false;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
new file mode 100644
index 0000000000..a82e13fcfa
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
@@ -0,0 +1,27 @@
+using Avalonia.Media;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class ResourceSetterViewModel : SetterViewModel
+ {
+ public object Key { get; }
+
+ public IBrush Tint { get; }
+
+ public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object resourceValue, bool isDynamic) : base(property, resourceValue)
+ {
+ Key = resourceKey;
+ Tint = isDynamic ? Brushes.Orange : Brushes.Brown;
+ }
+
+ public void CopyResourceKey()
+ {
+ if (Key is null)
+ {
+ return;
+ }
+
+ CopyToClipboard(Key.ToString());
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
new file mode 100644
index 0000000000..e835f5a878
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
@@ -0,0 +1,59 @@
+using Avalonia.Input.Platform;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class SetterViewModel : ViewModelBase
+ {
+ private bool _isActive;
+ private bool _isVisible;
+
+ public AvaloniaProperty Property { get; }
+
+ public string Name { get; }
+
+ public object Value { get; }
+
+ public bool IsActive
+ {
+ get => _isActive;
+ set => RaiseAndSetIfChanged(ref _isActive, value);
+ }
+
+ public bool IsVisible
+ {
+ get => _isVisible;
+ set => RaiseAndSetIfChanged(ref _isVisible, value);
+ }
+
+ public SetterViewModel(AvaloniaProperty property, object value)
+ {
+ Property = property;
+ Name = property.Name;
+ Value = value;
+ IsActive = true;
+ IsVisible = true;
+ }
+
+ public void CopyValue()
+ {
+ if (Value is null)
+ {
+ return;
+ }
+
+ CopyToClipboard(Value.ToString());
+ }
+
+ public void CopyPropertyName()
+ {
+ CopyToClipboard(Property.Name);
+ }
+
+ protected static void CopyToClipboard(string value)
+ {
+ var clipboard = AvaloniaLocator.Current.GetService();
+
+ clipboard?.SetTextAsync(value);
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs
new file mode 100644
index 0000000000..06e2409800
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class StyleViewModel : ViewModelBase
+ {
+ private readonly IStyleInstance _styleInstance;
+ private bool _isActive;
+ private bool _isVisible;
+
+ public StyleViewModel(IStyleInstance styleInstance, string name, List setters)
+ {
+ _styleInstance = styleInstance;
+ IsVisible = true;
+ Name = name;
+ Setters = setters;
+
+ Update();
+ }
+
+ public bool IsActive
+ {
+ get => _isActive;
+ set => RaiseAndSetIfChanged(ref _isActive, value);
+ }
+
+ public bool IsVisible
+ {
+ get => _isVisible;
+ set => RaiseAndSetIfChanged(ref _isVisible, value);
+ }
+
+ public string Name { get; }
+
+ public List Setters { get; }
+
+ public void Update()
+ {
+ IsActive = _styleInstance.IsActive;
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
index bd65a3b06b..6b779cd6ac 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
@@ -31,11 +31,18 @@ namespace Avalonia.Diagnostics.ViewModels
get => _selectedNode;
private set
{
+ var oldDetails = Details;
+
if (RaiseAndSetIfChanged(ref _selectedNode, value))
{
Details = value != null ?
new ControlDetailsViewModel(this, value.Visual) :
null;
+
+ if (Details != null && oldDetails != null)
+ {
+ Details.StyleFilter = oldDetails.StyleFilter;
+ }
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
index 2e0b6813ba..ab651b2a06 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
@@ -2,68 +2,15 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
- x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
+ xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
+ x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
+ x:Name="Main">
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -105,50 +52,134 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (
+
+
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
index c9568509f6..c6bd5a18aa 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
@@ -1,24 +1,10 @@
-using System;
using Avalonia.Controls;
-using Avalonia.Controls.Shapes;
using Avalonia.Markup.Xaml;
-using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.Views
{
internal class ControlDetailsView : UserControl
{
- private ThicknessEditor _borderArea;
- private ThicknessEditor _paddingArea;
- private Rectangle _horizontalSizeBegin;
- private Rectangle _horizontalSizeEnd;
- private Rectangle _verticalSizeBegin;
- private Rectangle _verticalSizeEnd;
- private Grid _layoutRoot;
- private Border _horizontalSize;
- private Border _verticalSize;
- private Border _contentArea;
-
public ControlDetailsView()
{
InitializeComponent();
@@ -27,101 +13,6 @@ namespace Avalonia.Diagnostics.Views
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
-
- _borderArea = this.FindControl("BorderArea");
- _paddingArea = this.FindControl("PaddingArea");
-
- _horizontalSizeBegin = this.FindControl("HorizontalSizeBegin");
- _horizontalSizeEnd = this.FindControl("HorizontalSizeEnd");
- _verticalSizeBegin = this.FindControl("VerticalSizeBegin");
- _verticalSizeEnd = this.FindControl("VerticalSizeEnd");
-
- _horizontalSize = this.FindControl("HorizontalSize");
- _verticalSize = this.FindControl("VerticalSize");
-
- _contentArea = this.FindControl("ContentArea");
-
- _layoutRoot = this.FindControl("LayoutRoot");
-
- void SubscribeToBounds(Visual visual)
- {
- visual.GetPropertyChangedObservable(TransformedBoundsProperty)
- .Subscribe(UpdateSizeGuidelines);
- }
-
- SubscribeToBounds(_borderArea);
- SubscribeToBounds(_paddingArea);
- SubscribeToBounds(_contentArea);
- }
-
- private void UpdateSizeGuidelines(AvaloniaPropertyChangedEventArgs e)
- {
- void UpdateGuidelines(Visual area)
- {
- if (area.TransformedBounds is TransformedBounds bounds)
- {
- // Horizontal guideline
- {
- var sizeArea = TranslateToRoot((_horizontalSize.TransformedBounds ?? default).Bounds.BottomLeft,
- _horizontalSize);
-
- var start = TranslateToRoot(bounds.Bounds.BottomLeft, area);
-
- SetPosition(_horizontalSizeBegin, start);
-
- var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
-
- SetPosition(_horizontalSizeEnd, end.WithX(end.X - 1));
-
- var height = sizeArea.Y - start.Y + 2;
-
- _horizontalSizeBegin.Height = height;
- _horizontalSizeEnd.Height = height;
- }
-
- // Vertical guideline
- {
- var sizeArea = TranslateToRoot((_verticalSize.TransformedBounds ?? default).Bounds.TopRight, _verticalSize);
-
- var start = TranslateToRoot(bounds.Bounds.TopRight, area);
-
- SetPosition(_verticalSizeBegin, start);
-
- var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
-
- SetPosition(_verticalSizeEnd, end.WithY(end.Y - 1));
-
- var width = sizeArea.X - start.X + 2;
-
- _verticalSizeBegin.Width = width;
- _verticalSizeEnd.Width = width;
- }
- }
- }
-
- Point TranslateToRoot(Point point, IVisual from)
- {
- return from.TranslatePoint(point, _layoutRoot) ?? default;
- }
-
- static void SetPosition(Rectangle rect, Point start)
- {
- Canvas.SetLeft(rect, start.X);
- Canvas.SetTop(rect, start.Y);
- }
-
- if (_borderArea.IsPresent)
- {
- UpdateGuidelines(_borderArea);
- }
- else if (_paddingArea.IsPresent)
- {
- UpdateGuidelines(_paddingArea);
- }
- else
- {
- UpdateGuidelines(_contentArea);
- }
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
index b7f0860e70..d16fc40aac 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
@@ -2,58 +2,129 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
- x:Class="Avalonia.Diagnostics.Views.EventsPageView">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ x:Class="Avalonia.Diagnostics.Views.EventsPageView"
+ Margin="2">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
index c1c78d38f6..687a20c5f6 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
@@ -1,7 +1,14 @@
-using System.Linq;
+using System;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
using Avalonia.Controls;
+using Avalonia.Diagnostics.Models;
using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Input;
+using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
namespace Avalonia.Diagnostics.Views
{
@@ -12,13 +19,53 @@ namespace Avalonia.Diagnostics.Views
public EventsPageView()
{
InitializeComponent();
- _events = this.FindControl("events");
+ _events = this.FindControl("EventsList");
}
- private void RecordedEvents_CollectionChanged(object sender,
- System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+ public void NavigateTo(object sender, TappedEventArgs e)
{
- _events.ScrollIntoView(_events.Items.OfType().LastOrDefault());
+ if (DataContext is EventsPageViewModel vm && sender is Control control)
+ {
+ switch (control.Tag)
+ {
+ case EventChainLink chainLink:
+ {
+ vm.RequestTreeNavigateTo(chainLink);
+ break;
+ }
+ case RoutedEvent evt:
+ {
+ vm.SelectEventByType(evt);
+
+ break;
+ }
+ }
+ }
+ }
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+
+ if (DataContext is EventsPageViewModel vm)
+ {
+ vm.RecordedEvents.CollectionChanged += OnRecordedEventsChanged;
+ }
+ }
+
+ private void OnRecordedEventsChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (sender is ObservableCollection events)
+ {
+ var evt = events.LastOrDefault();
+
+ if (evt is null)
+ {
+ return;
+ }
+
+ Dispatcher.UIThread.Post(() => _events.ScrollIntoView(evt));
+ }
}
private void InitializeComponent()
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
new file mode 100644
index 0000000000..af6c84a76a
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs
new file mode 100644
index 0000000000..d4a09f7296
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs
@@ -0,0 +1,128 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Diagnostics.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Diagnostics.Views
+{
+ internal class LayoutExplorerView : UserControl
+ {
+ private readonly ThicknessEditor _borderArea;
+ private readonly ThicknessEditor _paddingArea;
+ private readonly Rectangle _horizontalSizeBegin;
+ private readonly Rectangle _horizontalSizeEnd;
+ private readonly Rectangle _verticalSizeBegin;
+ private readonly Rectangle _verticalSizeEnd;
+ private readonly Grid _layoutRoot;
+ private readonly Border _horizontalSize;
+ private readonly Border _verticalSize;
+ private readonly Border _contentArea;
+
+ public LayoutExplorerView()
+ {
+ InitializeComponent();
+
+ _borderArea = this.FindControl("BorderArea");
+ _paddingArea = this.FindControl("PaddingArea");
+
+ _horizontalSizeBegin = this.FindControl("HorizontalSizeBegin");
+ _horizontalSizeEnd = this.FindControl("HorizontalSizeEnd");
+ _verticalSizeBegin = this.FindControl("VerticalSizeBegin");
+ _verticalSizeEnd = this.FindControl("VerticalSizeEnd");
+
+ _horizontalSize = this.FindControl("HorizontalSize");
+ _verticalSize = this.FindControl("VerticalSize");
+
+ _contentArea = this.FindControl("ContentArea");
+
+ _layoutRoot = this.FindControl("LayoutRoot");
+
+ void SubscribeToBounds(Visual visual)
+ {
+ visual.GetPropertyChangedObservable(TransformedBoundsProperty)
+ .Subscribe(UpdateSizeGuidelines);
+ }
+
+ SubscribeToBounds(_borderArea);
+ SubscribeToBounds(_paddingArea);
+ SubscribeToBounds(_contentArea);
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void UpdateSizeGuidelines(AvaloniaPropertyChangedEventArgs e)
+ {
+ void UpdateGuidelines(Visual area)
+ {
+ if (area.TransformedBounds is TransformedBounds bounds)
+ {
+ // Horizontal guideline
+ {
+ var sizeArea = TranslateToRoot((_horizontalSize.TransformedBounds ?? default).Bounds.BottomLeft,
+ _horizontalSize);
+
+ var start = TranslateToRoot(bounds.Bounds.BottomLeft, area);
+
+ SetPosition(_horizontalSizeBegin, start);
+
+ var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
+
+ SetPosition(_horizontalSizeEnd, end.WithX(end.X - 1));
+
+ var height = sizeArea.Y - start.Y + 2;
+
+ _horizontalSizeBegin.Height = height;
+ _horizontalSizeEnd.Height = height;
+ }
+
+ // Vertical guideline
+ {
+ var sizeArea = TranslateToRoot((_verticalSize.TransformedBounds ?? default).Bounds.TopRight, _verticalSize);
+
+ var start = TranslateToRoot(bounds.Bounds.TopRight, area);
+
+ SetPosition(_verticalSizeBegin, start);
+
+ var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
+
+ SetPosition(_verticalSizeEnd, end.WithY(end.Y - 1));
+
+ var width = sizeArea.X - start.X + 2;
+
+ _verticalSizeBegin.Width = width;
+ _verticalSizeEnd.Width = width;
+ }
+ }
+ }
+
+ Point TranslateToRoot(Point point, IVisual from)
+ {
+ return from.TranslatePoint(point, _layoutRoot) ?? default;
+ }
+
+ static void SetPosition(Rectangle rect, Point start)
+ {
+ Canvas.SetLeft(rect, start.X);
+ Canvas.SetTop(rect, start.Y);
+ }
+
+ if (_borderArea.IsPresent)
+ {
+ UpdateGuidelines(_borderArea);
+ }
+ else if (_paddingArea.IsPresent)
+ {
+ UpdateGuidelines(_paddingArea);
+ }
+ else
+ {
+ UpdateGuidelines(_contentArea);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
index 3623e95597..7628ee2d8c 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
@@ -12,6 +12,10 @@
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
index c4f9185728..330121321a 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
@@ -90,6 +90,16 @@ namespace Avalonia.Diagnostics.Views
var vm = (MainViewModel)DataContext;
vm.SelectControl((IControl)control);
}
+ }
+ else if (e.Modifiers == RawInputModifiers.Alt)
+ {
+ if (e.Key == Key.S || e.Key == Key.D)
+ {
+ var enable = e.Key == Key.S;
+
+ var vm = (MainViewModel)DataContext;
+ vm.EnableSnapshotStyles(enable);
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
index 86137dfc57..a5328716fc 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
@@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.TreePageView">
-
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
index 1b61986ce6..3e1a238b36 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Generators;
@@ -6,6 +7,7 @@ using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
+using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.Views
{
@@ -100,12 +102,19 @@ namespace Avalonia.Diagnostics.Views
private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e)
{
var item = (TreeViewItem)sender;
- var headerPresenter = item.HeaderPresenter;
- headerPresenter.ApplyTemplate();
- var header = headerPresenter.Child;
- header.PointerEnter += AddAdorner;
- header.PointerLeave += RemoveAdorner;
+ // This depends on the default tree item template.
+ // We want to handle events in the item header but exclude events coming from children.
+ var header = item.FindDescendantOfType();
+
+ Debug.Assert(header != null);
+
+ if (header != null)
+ {
+ header.PointerEnter += AddAdorner;
+ header.PointerLeave += RemoveAdorner;
+ }
+
item.TemplateApplied -= TreeViewItemTemplateApplied;
}
}
diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
index e51cc2f3ce..08e8798cd5 100644
--- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
+++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
@@ -79,8 +79,8 @@
-
-
+
+
@@ -99,7 +99,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
index 09fab0ed3f..55e30396e1 100644
--- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
+++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
@@ -1,3 +1,4 @@
+using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Avalonia.Controls;
@@ -7,12 +8,18 @@ namespace Avalonia.Dialogs
{
public class AboutAvaloniaDialog : Window
{
+ private static readonly Version s_version = typeof(AboutAvaloniaDialog).Assembly.GetName().Version;
+
+ public static string Version { get; } = s_version.ToString(2);
+
+ public static bool IsDevelopmentBuild { get; } = s_version.Revision == 999;
+
public AboutAvaloniaDialog()
{
AvaloniaXamlLoader.Load(this);
DataContext = this;
}
-
+
public static void OpenBrowser(string url)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
diff --git a/src/Avalonia.Dialogs/ApiCompatBaseline.txt b/src/Avalonia.Dialogs/ApiCompatBaseline.txt
deleted file mode 100644
index fcc74cf864..0000000000
--- a/src/Avalonia.Dialogs/ApiCompatBaseline.txt
+++ /dev/null
@@ -1 +0,0 @@
-Total Issues: 0
diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
index dd476e2559..665693a654 100644
--- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
+++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
@@ -4,9 +4,6 @@
-
- Designer
-
diff --git a/src/Avalonia.Dialogs/ChildFitter.cs b/src/Avalonia.Dialogs/ChildFitter.cs
index 744d455d9d..4a6db62b32 100644
--- a/src/Avalonia.Dialogs/ChildFitter.cs
+++ b/src/Avalonia.Dialogs/ChildFitter.cs
@@ -4,7 +4,7 @@ using Avalonia.Layout;
namespace Avalonia.Dialogs
{
- internal class ChildFitter : Decorator
+ public class ChildFitter : Decorator
{
protected override Size MeasureOverride(Size availableSize)
{
diff --git a/src/Avalonia.Dialogs/FileSizeStringConverter.cs b/src/Avalonia.Dialogs/FileSizeStringConverter.cs
index 5b41b9da35..c2cdf1e502 100644
--- a/src/Avalonia.Dialogs/FileSizeStringConverter.cs
+++ b/src/Avalonia.Dialogs/FileSizeStringConverter.cs
@@ -6,7 +6,7 @@ using System.Text;
namespace Avalonia.Dialogs
{
- internal class FileSizeStringConverter : IValueConverter
+ public class FileSizeStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/ManagedFileChooser.cs
similarity index 66%
rename from src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
rename to src/Avalonia.Dialogs/ManagedFileChooser.cs
index 7f29407ed5..f9f38ac474 100644
--- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
+++ b/src/Avalonia.Dialogs/ManagedFileChooser.cs
@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
@@ -10,17 +11,14 @@ using Avalonia.Markup.Xaml;
namespace Avalonia.Dialogs
{
- internal class ManagedFileChooser : UserControl
+ public class ManagedFileChooser : TemplatedControl
{
private Control _quickLinksRoot;
private ListBox _filesView;
public ManagedFileChooser()
{
- AvaloniaXamlLoader.Load(this);
AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
- _quickLinksRoot = this.FindControl("QuickLinks");
- _filesView = this.FindControl("Files");
}
ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel;
@@ -34,19 +32,22 @@ namespace Avalonia.Dialogs
return;
}
- var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
- if (e.ClickCount == 2 || isQuickLink)
+ if (_quickLinksRoot != null)
{
- if (model.ItemType == ManagedFileChooserItemType.File)
+ var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
+ if (e.ClickCount == 2 || isQuickLink)
{
- Model?.SelectSingleFile(model);
+ if (model.ItemType == ManagedFileChooserItemType.File)
+ {
+ Model?.SelectSingleFile(model);
+ }
+ else
+ {
+ Model?.Navigate(model.Path);
+ }
+
+ e.Handled = true;
}
- else
- {
- Model?.Navigate(model.Path);
- }
-
- e.Handled = true;
}
}
@@ -79,10 +80,16 @@ namespace Avalonia.Dialogs
// Workaround for ListBox bug, scroll to the previous file
var indexOfPreselected = model.Items.IndexOf(preselected);
- if (indexOfPreselected > 1)
+ if ((_filesView != null) && (indexOfPreselected > 1))
{
_filesView.ScrollIntoView(indexOfPreselected - 1);
}
}
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ _quickLinksRoot = e.NameScope.Get("QuickLinks");
+ _filesView = e.NameScope.Get("Files");
+ }
}
}
diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/ManagedFileChooser.xaml
deleted file mode 100644
index 227cc1afc0..0000000000
--- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml
+++ /dev/null
@@ -1,144 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Show hidden files
-
-
-
-
-
-
- OK
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
index 0dc024c4dd..050d618ce1 100644
--- a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
+++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
@@ -67,7 +67,7 @@ namespace Avalonia.Dialogs
{
Directory.GetFiles(x.VolumePath);
}
- catch (UnauthorizedAccessException _)
+ catch (Exception _)
{
return null;
}
diff --git a/src/Avalonia.Dialogs/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/ResourceSelectorConverter.cs
index 9d8b6cb1c7..c8226b98e4 100644
--- a/src/Avalonia.Dialogs/ResourceSelectorConverter.cs
+++ b/src/Avalonia.Dialogs/ResourceSelectorConverter.cs
@@ -5,7 +5,7 @@ using Avalonia.Data.Converters;
namespace Avalonia.Dialogs
{
- internal class ResourceSelectorConverter : ResourceDictionary, IValueConverter
+ public class ResourceSelectorConverter : ResourceDictionary, IValueConverter
{
public object Convert(object key, Type targetType, object parameter, CultureInfo culture)
{
diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs
new file mode 100644
index 0000000000..5cd748be02
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Avalonia.FreeDesktop
+{
+ class DBusCallQueue
+ {
+ private readonly Func _errorHandler;
+
+ class Item
+ {
+ public Func Callback;
+ public Action OnFinish;
+ }
+ private Queue- _q = new Queue
- ();
+ private bool _processing;
+
+ public DBusCallQueue(Func errorHandler)
+ {
+ _errorHandler = errorHandler;
+ }
+
+ public void Enqueue(Func cb)
+ {
+ _q.Enqueue(new Item
+ {
+ Callback = cb
+ });
+ Process();
+ }
+
+ public Task EnqueueAsync(Func cb)
+ {
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ _q.Enqueue(new Item
+ {
+ Callback = cb,
+ OnFinish = e =>
+ {
+ if (e == null)
+ tcs.TrySetResult(0);
+ else
+ tcs.TrySetException(e);
+ }
+ });
+ Process();
+ return tcs.Task;
+ }
+
+ public Task EnqueueAsync(Func> cb)
+ {
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ _q.Enqueue(new Item
+ {
+ Callback = async () =>
+ {
+ var res = await cb();
+ tcs.TrySetResult(res);
+ },
+ OnFinish = e =>
+ {
+ if (e != null)
+ tcs.TrySetException(e);
+ }
+ });
+ Process();
+ return tcs.Task;
+ }
+
+ async void Process()
+ {
+ if(_processing)
+ return;
+ _processing = true;
+ try
+ {
+ while (_q.Count > 0)
+ {
+ var item = _q.Dequeue();
+ try
+ {
+ await item.Callback();
+ item.OnFinish?.Invoke(null);
+ }
+ catch(Exception e)
+ {
+ if (item.OnFinish != null)
+ item.OnFinish(e);
+ else
+ await _errorHandler(e);
+ }
+ }
+ }
+ finally
+ {
+ _processing = false;
+ }
+ }
+
+ public void FailAll()
+ {
+ while (_q.Count>0)
+ {
+ var item = _q.Dequeue();
+ item.OnFinish?.Invoke(new OperationCanceledException());
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs
index b445f86613..7996a94dd0 100644
--- a/src/Avalonia.FreeDesktop/DBusHelper.cs
+++ b/src/Avalonia.FreeDesktop/DBusHelper.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading;
+using Avalonia.Logging;
using Avalonia.Threading;
using Tmds.DBus;
@@ -48,8 +49,10 @@ namespace Avalonia.FreeDesktop
}
public static Connection Connection { get; private set; }
- public static Exception TryInitialize(string dbusAddress = null)
+ public static Connection TryInitialize(string dbusAddress = null)
{
+ if (Connection != null)
+ return Connection;
var oldContext = SynchronizationContext.Current;
try
{
@@ -70,13 +73,15 @@ namespace Avalonia.FreeDesktop
}
catch (Exception e)
{
- return e;
+ Logger.TryGet(LogEventLevel.Error, "DBUS")
+ ?.Log(null, "Unable to connect to DBus: " + e);
}
finally
{
SynchronizationContext.SetSynchronizationContext(oldContext);
}
- return null;
+
+ return Connection;
}
}
}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
new file mode 100644
index 0000000000..a7e83140ae
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
@@ -0,0 +1,288 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.FreeDesktop.DBusIme.Fcitx;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Avalonia.Logging;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme
+{
+ internal class DBusInputMethodFactory : IX11InputMethodFactory where T : ITextInputMethodImpl, IX11InputMethodControl
+ {
+ private readonly Func _factory;
+
+ public DBusInputMethodFactory(Func factory)
+ {
+ _factory = factory;
+ }
+
+ public (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid)
+ {
+ var im = _factory(xid);
+ return (im, im);
+ }
+ }
+
+ internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl
+ {
+ private List _disposables = new List();
+ private Queue _onlineNamesQueue = new Queue();
+ protected Connection Connection { get; }
+ private readonly string[] _knownNames;
+ private bool _connecting;
+ private string _currentName;
+ private DBusCallQueue _queue;
+ private bool _controlActive, _windowActive;
+ private bool? _imeActive;
+ private Rect _logicalRect;
+ private PixelRect? _lastReportedRect;
+ private double _scaling = 1;
+ private PixelPoint _windowPosition;
+
+ protected bool IsConnected => _currentName != null;
+
+ public DBusTextInputMethodBase(Connection connection, params string[] knownNames)
+ {
+ _queue = new DBusCallQueue(QueueOnError);
+ Connection = connection;
+ _knownNames = knownNames;
+ Watch();
+ }
+
+ async void Watch()
+ {
+ foreach (var name in _knownNames)
+ _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange));
+ }
+
+ protected abstract Task Connect(string name);
+
+ protected string GetAppName() =>
+ Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia";
+
+ private async void OnNameChange(ServiceOwnerChangedEventArgs args)
+ {
+ if (args.NewOwner != null && _currentName == null)
+ {
+ _onlineNamesQueue.Enqueue(args.ServiceName);
+ if(!_connecting)
+ {
+ _connecting = true;
+ try
+ {
+ while (_onlineNamesQueue.Count > 0)
+ {
+ var name = _onlineNamesQueue.Dequeue();
+ try
+ {
+ if (await Connect(name))
+ {
+ _onlineNamesQueue.Clear();
+ _currentName = name;
+ return;
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.TryGet(LogEventLevel.Error, "IME")
+ ?.Log(this, "Unable to create IME input context:\n" + e);
+ }
+ }
+ }
+ finally
+ {
+ _connecting = false;
+ }
+ }
+
+ }
+
+ // IME has crashed
+ if (args.NewOwner == null && args.ServiceName == _currentName)
+ {
+ _currentName = null;
+ foreach(var s in _disposables)
+ s.Dispose();
+ _disposables.Clear();
+
+ OnDisconnected();
+ Reset();
+
+ // Watch again
+ Watch();
+ }
+ }
+
+ protected virtual Task Disconnect()
+ {
+ return Task.CompletedTask;
+ }
+
+ protected virtual void OnDisconnected()
+ {
+
+ }
+
+ protected virtual void Reset()
+ {
+ _lastReportedRect = null;
+ _imeActive = null;
+ }
+
+ async Task QueueOnError(Exception e)
+ {
+ Logger.TryGet(LogEventLevel.Error, "IME")
+ ?.Log(this, "Error:\n" + e);
+ try
+ {
+ await Disconnect();
+ }
+ catch (Exception ex)
+ {
+ Logger.TryGet(LogEventLevel.Error, "IME")
+ ?.Log(this, "Error while destroying the context:\n" + ex);
+ }
+ OnDisconnected();
+ _currentName = null;
+ }
+
+ protected void Enqueue(Func cb) => _queue.Enqueue(cb);
+
+ protected void AddDisposable(IDisposable d) => _disposables.Add(d);
+
+ public void Dispose()
+ {
+ foreach(var d in _disposables)
+ d.Dispose();
+ _disposables.Clear();
+ try
+ {
+ Disconnect().ContinueWith(_ => { });
+ }
+ catch
+ {
+ // fire and forget
+ }
+ _currentName = null;
+ }
+
+ protected abstract Task SetCursorRectCore(PixelRect rect);
+ protected abstract Task SetActiveCore(bool active);
+ protected abstract Task ResetContextCore();
+ protected abstract Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode);
+
+ void UpdateActive()
+ {
+ _queue.Enqueue(async () =>
+ {
+ if(!IsConnected)
+ return;
+
+ var active = _windowActive && _controlActive;
+ if (active != _imeActive)
+ {
+ _imeActive = active;
+ await SetActiveCore(active);
+ }
+ });
+ }
+
+
+ void IX11InputMethodControl.SetWindowActive(bool active)
+ {
+ _windowActive = active;
+ UpdateActive();
+ }
+
+ void ITextInputMethodImpl.SetActive(bool active)
+ {
+ _controlActive = active;
+ UpdateActive();
+ }
+
+ bool IX11InputMethodControl.IsEnabled => IsConnected && _imeActive == true;
+
+ async ValueTask IX11InputMethodControl.HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode)
+ {
+ try
+ {
+ return await _queue.EnqueueAsync(async () => await HandleKeyCore(args, keyVal, keyCode));
+ }
+ // Disconnected
+ catch (OperationCanceledException)
+ {
+ return false;
+ }
+ // Error, disconnect
+ catch (Exception e)
+ {
+ await QueueOnError(e);
+ return false;
+ }
+ }
+
+ private Action _onCommit;
+ event Action IX11InputMethodControl.Commit
+ {
+ add => _onCommit += value;
+ remove => _onCommit -= value;
+ }
+
+ protected void FireCommit(string s) => _onCommit?.Invoke(s);
+
+ private Action _onForward;
+ event Action IX11InputMethodControl.ForwardKey
+ {
+ add => _onForward += value;
+ remove => _onForward -= value;
+ }
+
+ protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k);
+
+ void UpdateCursorRect()
+ {
+ _queue.Enqueue(async () =>
+ {
+ if(!IsConnected)
+ return;
+ var cursorRect = PixelRect.FromRect(_logicalRect, _scaling);
+ cursorRect = cursorRect.Translate(_windowPosition);
+ if (cursorRect != _lastReportedRect)
+ {
+ _lastReportedRect = cursorRect;
+ await SetCursorRectCore(cursorRect);
+ }
+ });
+ }
+
+ void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling)
+ {
+ _windowPosition = position;
+ _scaling = scaling;
+ UpdateCursorRect();
+ }
+
+ void ITextInputMethodImpl.SetCursorRect(Rect rect)
+ {
+ _logicalRect = rect;
+ UpdateCursorRect();
+ }
+
+ public abstract void SetOptions(TextInputOptionsQueryEventArgs options);
+
+ void ITextInputMethodImpl.Reset()
+ {
+ Reset();
+ _queue.Enqueue(async () =>
+ {
+ if (!IsConnected)
+ return;
+ await ResetContextCore();
+ });
+ }
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
new file mode 100644
index 0000000000..7ce2339763
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Tmds.DBus;
+
+[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)]
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ [DBusInterface("org.fcitx.Fcitx.InputMethod")]
+ interface IFcitxInputMethod : IDBusObject
+ {
+ Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async(
+ string Appname, int Pid);
+ }
+
+
+ [DBusInterface("org.fcitx.Fcitx.InputContext")]
+ interface IFcitxInputContext : IDBusObject
+ {
+ Task EnableICAsync();
+ Task CloseICAsync();
+ Task FocusInAsync();
+ Task FocusOutAsync();
+ Task ResetAsync();
+ Task MouseEventAsync(int X);
+ Task SetCursorLocationAsync(int X, int Y);
+ Task SetCursorRectAsync(int X, int Y, int W, int H);
+ Task SetCapacityAsync(uint Caps);
+ Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor);
+ Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
+ Task DestroyICAsync();
+ Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time);
+ Task WatchEnableIMAsync(Action handler, Action onError = null);
+ Task WatchCloseIMAsync(Action handler, Action onError = null);
+ Task WatchCommitStringAsync(Action handler, Action onError = null);
+ Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null);
+ Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action onError = null);
+ Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null);
+ Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action onError = null);
+ Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action onError = null);
+ Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null);
+ }
+
+ [DBusInterface("org.fcitx.Fcitx.InputContext1")]
+ interface IFcitxInputContext1 : IDBusObject
+ {
+ Task FocusInAsync();
+ Task FocusOutAsync();
+ Task ResetAsync();
+ Task SetCursorRectAsync(int X, int Y, int W, int H);
+ Task SetCapabilityAsync(ulong Caps);
+ Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor);
+ Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
+ Task DestroyICAsync();
+ Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time);
+ Task WatchCommitStringAsync(Action handler, Action onError = null);
+ Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null);
+ Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null);
+ Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action onError = null);
+ Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null);
+ }
+
+ [DBusInterface("org.fcitx.Fcitx.InputMethod1")]
+ interface IFcitxInputMethod1 : IDBusObject
+ {
+ Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0);
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
new file mode 100644
index 0000000000..6510a5877a
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ enum FcitxKeyEventType
+ {
+ FCITX_PRESS_KEY,
+ FCITX_RELEASE_KEY
+ };
+
+ [Flags]
+ enum FcitxCapabilityFlags
+ {
+ CAPACITY_NONE = 0,
+ CAPACITY_CLIENT_SIDE_UI = (1 << 0),
+ CAPACITY_PREEDIT = (1 << 1),
+ CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2),
+ CAPACITY_PASSWORD = (1 << 3),
+ CAPACITY_FORMATTED_PREEDIT = (1 << 4),
+ CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5),
+ CAPACITY_SURROUNDING_TEXT = (1 << 6),
+ CAPACITY_EMAIL = (1 << 7),
+ CAPACITY_DIGIT = (1 << 8),
+ CAPACITY_UPPERCASE = (1 << 9),
+ CAPACITY_LOWERCASE = (1 << 10),
+ CAPACITY_NOAUTOUPPERCASE = (1 << 11),
+ CAPACITY_URL = (1 << 12),
+ CAPACITY_DIALABLE = (1 << 13),
+ CAPACITY_NUMBER = (1 << 14),
+ CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15),
+ CAPACITY_SPELLCHECK = (1 << 16),
+ CAPACITY_NO_SPELLCHECK = (1 << 17),
+ CAPACITY_WORD_COMPLETION = (1 << 18),
+ CAPACITY_UPPERCASE_WORDS = (1 << 19),
+ CAPACITY_UPPERCASE_SENTENCES = (1 << 20),
+ CAPACITY_ALPHA = (1 << 21),
+ CAPACITY_NAME = (1 << 22),
+ CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23),
+ CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24),
+ };
+
+ [Flags]
+ enum FcitxKeyState
+ {
+ FcitxKeyState_None = 0,
+ FcitxKeyState_Shift = 1 << 0,
+ FcitxKeyState_CapsLock = 1 << 1,
+ FcitxKeyState_Ctrl = 1 << 2,
+ FcitxKeyState_Alt = 1 << 3,
+ FcitxKeyState_Alt_Shift = FcitxKeyState_Alt | FcitxKeyState_Shift,
+ FcitxKeyState_Ctrl_Shift = FcitxKeyState_Ctrl | FcitxKeyState_Shift,
+ FcitxKeyState_Ctrl_Alt = FcitxKeyState_Ctrl | FcitxKeyState_Alt,
+
+ FcitxKeyState_Ctrl_Alt_Shift =
+ FcitxKeyState_Ctrl | FcitxKeyState_Alt | FcitxKeyState_Shift,
+ FcitxKeyState_NumLock = 1 << 4,
+ FcitxKeyState_Super = 1 << 6,
+ FcitxKeyState_ScrollLock = 1 << 7,
+ FcitxKeyState_MousePressed = 1 << 8,
+ FcitxKeyState_HandledMask = 1 << 24,
+ FcitxKeyState_IgnoredMask = 1 << 25,
+ FcitxKeyState_Super2 = 1 << 26,
+ FcitxKeyState_Hyper = 1 << 27,
+ FcitxKeyState_Meta = 1 << 28,
+ FcitxKeyState_UsedMask = 0x5c001fff
+ };
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
new file mode 100644
index 0000000000..a03ea213aa
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ internal class FcitxICWrapper
+ {
+ private readonly IFcitxInputContext1 _modern;
+ private readonly IFcitxInputContext _old;
+
+ public FcitxICWrapper(IFcitxInputContext old)
+ {
+ _old = old;
+ }
+
+ public FcitxICWrapper(IFcitxInputContext1 modern)
+ {
+ _modern = modern;
+ }
+
+ public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync();
+
+ public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync();
+
+ public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync();
+
+ public Task SetCursorRectAsync(int x, int y, int w, int h) =>
+ _old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h);
+ public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync();
+
+ public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time)
+ {
+ if(_old!=null)
+ return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0;
+ return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time);
+ }
+
+ public Task WatchCommitStringAsync(Action handler) =>
+ _old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler);
+
+ public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler)
+ {
+ return _old?.WatchForwardKeyAsync(handler)
+ ?? _modern.WatchForwardKeyAsync(ev =>
+ handler((ev.keyval, ev.state, ev.type ? 1 : 0)));
+ }
+
+ public Task SetCapacityAsync(uint flags) =>
+ _old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags);
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
new file mode 100644
index 0000000000..31a061571f
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Diagnostics;
+using System.Reactive.Concurrency;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ internal class FcitxX11TextInputMethod : DBusTextInputMethodBase
+ {
+ private FcitxICWrapper _context;
+ private FcitxCapabilityFlags? _lastReportedFlags;
+
+ public FcitxX11TextInputMethod(Connection connection) : base(connection,
+ "org.fcitx.Fcitx",
+ "org.freedesktop.portal.Fcitx"
+ )
+ {
+
+ }
+
+ protected override async Task Connect(string name)
+ {
+ if (name == "org.fcitx.Fcitx")
+ {
+ var method = Connection.CreateProxy(name, "/inputmethod");
+ var resp = await method.CreateICv3Async(GetAppName(),
+ Process.GetCurrentProcess().Id);
+
+ var proxy = Connection.CreateProxy(name,
+ "/inputcontext_" + resp.icid);
+
+ _context = new FcitxICWrapper(proxy);
+ }
+ else
+ {
+ var method = Connection.CreateProxy(name, "/inputmethod");
+ var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) });
+ var proxy = Connection.CreateProxy(name, resp.path);
+ _context = new FcitxICWrapper(proxy);
+ }
+
+ AddDisposable(await _context.WatchCommitStringAsync(OnCommitString));
+ AddDisposable(await _context.WatchForwardKeyAsync(OnForward));
+ return true;
+ }
+
+ protected override Task Disconnect() => _context.DestroyICAsync();
+
+ protected override void OnDisconnected() => _context = null;
+
+ protected override void Reset()
+ {
+ _lastReportedFlags = null;
+ base.Reset();
+ }
+
+ protected override Task SetCursorRectCore(PixelRect cursorRect) =>
+ _context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width),
+ Math.Max(1, cursorRect.Height));
+
+ protected override Task SetActiveCore(bool active)
+ {
+ if (active)
+ return _context.FocusInAsync();
+ else
+ return _context.FocusOutAsync();
+ }
+
+ protected override Task ResetContextCore() => _context.ResetAsync();
+
+ protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
+ {
+ FcitxKeyState state = default;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Control))
+ state |= FcitxKeyState.FcitxKeyState_Ctrl;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Alt))
+ state |= FcitxKeyState.FcitxKeyState_Alt;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Shift))
+ state |= FcitxKeyState.FcitxKeyState_Shift;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Meta))
+ state |= FcitxKeyState.FcitxKeyState_Super;
+
+ var type = args.Type == RawKeyEventType.KeyDown ?
+ FcitxKeyEventType.FCITX_PRESS_KEY :
+ FcitxKeyEventType.FCITX_RELEASE_KEY;
+
+ return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type,
+ (uint)args.Timestamp).ConfigureAwait(false);
+ }
+
+ public override void SetOptions(TextInputOptionsQueryEventArgs options) =>
+ Enqueue(async () =>
+ {
+ if(_context == null)
+ return;
+ FcitxCapabilityFlags flags = default;
+ if (options.Lowercase)
+ flags |= FcitxCapabilityFlags.CAPACITY_LOWERCASE;
+ if (options.Uppercase)
+ flags |= FcitxCapabilityFlags.CAPACITY_UPPERCASE;
+ if (!options.AutoCapitalization)
+ flags |= FcitxCapabilityFlags.CAPACITY_NOAUTOUPPERCASE;
+ if (options.ContentType == TextInputContentType.Email)
+ flags |= FcitxCapabilityFlags.CAPACITY_EMAIL;
+ else if (options.ContentType == TextInputContentType.Number)
+ flags |= FcitxCapabilityFlags.CAPACITY_NUMBER;
+ else if (options.ContentType == TextInputContentType.Password)
+ flags |= FcitxCapabilityFlags.CAPACITY_PASSWORD;
+ else if (options.ContentType == TextInputContentType.Phone)
+ flags |= FcitxCapabilityFlags.CAPACITY_DIALABLE;
+ else if (options.ContentType == TextInputContentType.Url)
+ flags |= FcitxCapabilityFlags.CAPACITY_URL;
+ if (flags != _lastReportedFlags)
+ {
+ _lastReportedFlags = flags;
+ await _context.SetCapacityAsync((uint)flags);
+ }
+ });
+
+ private void OnForward((uint keyval, uint state, int type) ev)
+ {
+ var state = (FcitxKeyState)ev.state;
+ KeyModifiers mods = default;
+ if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Ctrl))
+ mods |= KeyModifiers.Control;
+ if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Alt))
+ mods |= KeyModifiers.Alt;
+ if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Shift))
+ mods |= KeyModifiers.Shift;
+ if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Super))
+ mods |= KeyModifiers.Meta;
+ FireForward(new X11InputMethodForwardedKey
+ {
+ Modifiers = mods,
+ KeyVal = (int)ev.keyval,
+ Type = ev.type == (int)FcitxKeyEventType.FCITX_PRESS_KEY ?
+ RawKeyEventType.KeyDown :
+ RawKeyEventType.KeyUp
+ });
+ }
+
+ private void OnCommitString(string s) => FireCommit(s);
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
new file mode 100644
index 0000000000..26c0d249f3
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Tmds.DBus;
+
+[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
+namespace Avalonia.FreeDesktop.DBusIme.IBus
+{
+ [DBusInterface("org.freedesktop.IBus.InputContext")]
+ interface IIBusInputContext : IDBusObject
+ {
+ Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State);
+ Task SetCursorLocationAsync(int X, int Y, int W, int H);
+ Task FocusInAsync();
+ Task FocusOutAsync();
+ Task ResetAsync();
+ Task SetCapabilitiesAsync(uint Caps);
+ Task PropertyActivateAsync(string Name, int State);
+ Task SetEngineAsync(string Name);
+ Task GetEngineAsync();
+ Task DestroyAsync();
+ Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos);
+ Task WatchCommitTextAsync(Action cb, Action onError = null);
+ Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action onError = null);
+ Task WatchRequireSurroundingTextAsync(Action handler, Action onError = null);
+ Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action onError = null);
+ Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action onError = null);
+ Task WatchShowPreeditTextAsync(Action handler, Action onError = null);
+ Task WatchHidePreeditTextAsync(Action handler, Action onError = null);
+ Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action onError = null);
+ Task WatchShowAuxiliaryTextAsync(Action handler, Action onError = null);
+ Task WatchHideAuxiliaryTextAsync(Action handler, Action onError = null);
+ Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action onError = null);
+ Task WatchShowLookupTableAsync(Action handler, Action onError = null);
+ Task WatchHideLookupTableAsync(Action handler, Action onError = null);
+ Task WatchPageUpLookupTableAsync(Action handler, Action onError = null);
+ Task WatchPageDownLookupTableAsync(Action handler, Action onError = null);
+ Task WatchCursorUpLookupTableAsync(Action handler, Action onError = null);
+ Task WatchCursorDownLookupTableAsync(Action handler, Action onError = null);
+ Task WatchRegisterPropertiesAsync(Action handler, Action onError = null);
+ Task WatchUpdatePropertyAsync(Action handler, Action onError = null);
+ }
+
+
+ [DBusInterface("org.freedesktop.IBus.Portal")]
+ interface IIBusPortal : IDBusObject
+ {
+ Task CreateInputContextAsync(string Name);
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs
new file mode 100644
index 0000000000..3070f51a8e
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace Avalonia.FreeDesktop.DBusIme.IBus
+{
+ [Flags]
+ internal enum IBusModifierMask
+ {
+ ShiftMask = 1 << 0,
+ LockMask = 1 << 1,
+ ControlMask = 1 << 2,
+ Mod1Mask = 1 << 3,
+ Mod2Mask = 1 << 4,
+ Mod3Mask = 1 << 5,
+ Mod4Mask = 1 << 6,
+ Mod5Mask = 1 << 7,
+ Button1Mask = 1 << 8,
+ Button2Mask = 1 << 9,
+ Button3Mask = 1 << 10,
+ Button4Mask = 1 << 11,
+ Button5Mask = 1 << 12,
+
+ HandledMask = 1 << 24,
+ ForwardMask = 1 << 25,
+ IgnoredMask = ForwardMask,
+
+ SuperMask = 1 << 26,
+ HyperMask = 1 << 27,
+ MetaMask = 1 << 28,
+
+ ReleaseMask = 1 << 30,
+
+ ModifierMask = 0x5c001fff
+ }
+
+ [Flags]
+ internal enum IBusCapability
+ {
+ CapPreeditText = 1 << 0,
+ CapAuxiliaryText = 1 << 1,
+ CapLookupTable = 1 << 2,
+ CapFocus = 1 << 3,
+ CapProperty = 1 << 4,
+ CapSurroundingText = 1 << 5,
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
new file mode 100644
index 0000000000..a73de9dae8
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
@@ -0,0 +1,105 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme.IBus
+{
+ internal class IBusX11TextInputMethod : DBusTextInputMethodBase
+ {
+ private IIBusInputContext _context;
+
+ public IBusX11TextInputMethod(Connection connection) : base(connection,
+ "org.freedesktop.portal.IBus")
+ {
+ }
+
+ protected override async Task Connect(string name)
+ {
+ var path =
+ await Connection.CreateProxy(name, "/org/freedesktop/IBus")
+ .CreateInputContextAsync(GetAppName());
+
+ _context = Connection.CreateProxy(name, path);
+ AddDisposable(await _context.WatchCommitTextAsync(OnCommitText));
+ AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey));
+ Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus));
+ return true;
+ }
+
+ private void OnForwardKey((uint keyval, uint keycode, uint state) k)
+ {
+ var state = (IBusModifierMask)k.state;
+ KeyModifiers mods = default;
+ if (state.HasAllFlags(IBusModifierMask.ControlMask))
+ mods |= KeyModifiers.Control;
+ if (state.HasAllFlags(IBusModifierMask.Mod1Mask))
+ mods |= KeyModifiers.Alt;
+ if (state.HasAllFlags(IBusModifierMask.ShiftMask))
+ mods |= KeyModifiers.Shift;
+ if (state.HasAllFlags(IBusModifierMask.Mod4Mask))
+ mods |= KeyModifiers.Meta;
+ FireForward(new X11InputMethodForwardedKey
+ {
+ KeyVal = (int)k.keyval,
+ Type = state.HasAllFlags(IBusModifierMask.ReleaseMask) ? RawKeyEventType.KeyUp : RawKeyEventType.KeyDown,
+ Modifiers = mods
+ });
+ }
+
+
+ private void OnCommitText(object wtf)
+ {
+ // Hello darkness, my old friend
+ var prop = wtf.GetType().GetField("Item3");
+ if (prop != null)
+ {
+ var text = (string)prop.GetValue(wtf);
+ if (!string.IsNullOrEmpty(text))
+ FireCommit(text);
+ }
+ }
+
+ protected override Task Disconnect() => _context.DestroyAsync();
+
+ protected override void OnDisconnected()
+ {
+ _context = null;
+ base.OnDisconnected();
+ }
+
+ protected override Task SetCursorRectCore(PixelRect rect)
+ => _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height);
+
+ protected override Task SetActiveCore(bool active)
+ => active ? _context.FocusInAsync() : _context.FocusOutAsync();
+
+ protected override Task ResetContextCore()
+ => _context.ResetAsync();
+
+ protected override Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
+ {
+ IBusModifierMask state = default;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Control))
+ state |= IBusModifierMask.ControlMask;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Alt))
+ state |= IBusModifierMask.Mod1Mask;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Shift))
+ state |= IBusModifierMask.ShiftMask;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Meta))
+ state |= IBusModifierMask.Mod4Mask;
+
+ if (args.Type == RawKeyEventType.KeyUp)
+ state |= IBusModifierMask.ReleaseMask;
+
+ return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state);
+ }
+
+ public override void SetOptions(TextInputOptionsQueryEventArgs options)
+ {
+ // No-op, because ibus
+ }
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
new file mode 100644
index 0000000000..7f71ecf0ff
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.FreeDesktop.DBusIme.Fcitx;
+using Avalonia.FreeDesktop.DBusIme.IBus;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme
+{
+ public class X11DBusImeHelper
+ {
+ private static readonly Dictionary> KnownMethods =
+ new Dictionary>
+ {
+ ["fcitx"] = conn =>
+ new DBusInputMethodFactory(_ => new FcitxX11TextInputMethod(conn)),
+ ["ibus"] = conn =>
+ new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn))
+ };
+
+ static Func