diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 00000000000..36c4a4a3def --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,51 @@ +name: Build and Test + +on: + push: + branches: [ master, wtg-net10 ] + pull_request: + branches: [ master, wtg-net10 ] + workflow_dispatch: + +jobs: + build: + name: Build - ${{ matrix.configuration }} + + strategy: + matrix: + configuration: [ Debug ] + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 10.0.x + + - name: Install dependencies + run: dotnet restore WinformsLegacy.sln + + - name: Build + run: dotnet build WinformsLegacy.sln --configuration ${{ matrix.configuration }} --no-restore + + - name: Test + if: matrix.configuration == 'Debug' + run: > + dotnet test + src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/System.Windows.Forms.Legacy.Tests.csproj + --configuration Debug + --no-build + --logger trx + --results-directory TestResults + + - name: Upload packages + if: matrix.configuration == 'Release' + uses: actions/upload-artifact@v4 + with: + name: package + path: Bin/Release/**/*.nupkg + retention-days: 5 diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 00000000000..78ca43f09a3 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,64 @@ +name: Create Release + +on: + workflow_dispatch: + inputs: + version: + description: Package version to publish, for example 1.2.3 or 1.2.3-preview.1 + required: true + type: string + +env: + PACKAGE_PATH: Bin/Release/**/*.nupkg + +jobs: + release: + name: Build - Release + + runs-on: windows-latest + + steps: + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 10.0.x + + - name: Validate version + shell: pwsh + run: | + $version = '${{ inputs.version }}' + + if ($version -notmatch '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$') { + throw "The version '$version' is not a valid SemVer 2.0.0 version." + } + + - name: Install dependencies + run: dotnet restore WinformsLegacy.sln + + # Pacakge is created on build + - name: Build + run: dotnet build WinformsLegacy.sln --configuration Release --no-restore /p:PackageVersion=${{ inputs.version }} + + # Upload package as an atrifact to the GitHub action + - name: Upload packages artifact + uses: actions/upload-artifact@v4 + with: + name: package + path: ${{ env.PACKAGE_PATH }} + retention-days: 5 + + # Create a new release and upload the package to the release + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: ${{ env.PACKAGE_PATH }} + name: ${{ inputs.version }} + tag_name: ${{ inputs.version }} + + - name: Push NuGet Package to NuGet Gallery + run: dotnet nuget push ${{ env.PACKAGE_PATH }} --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/Directory.Build.targets b/Directory.Build.targets index 85d104b510a..6f9674f797f 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -91,7 +91,9 @@ - $(MicrosoftNETCoreAppRefPackageVersion) + + + $(MicrosoftNETCoreAppRefPackageVersion) diff --git a/Directory.Packages.props b/Directory.Packages.props index 233aeb64347..5f1f5b0243a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -34,6 +34,7 @@ + diff --git a/WinformsLegacy.sln b/WinformsLegacy.sln new file mode 100644 index 00000000000..0301a00b9e5 --- /dev/null +++ b/WinformsLegacy.sln @@ -0,0 +1,140 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11520.95 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Windows.Forms", "src\System.Windows.Forms\System.Windows.Forms.csproj", "{0D23A41B-2626-4703-9E4A-87C07F69B0B2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Windows.Forms.Design", "src\System.Windows.Forms.Design\src\System.Windows.Forms.Design.csproj", "{61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Windows.Forms.Primitives", "src\System.Windows.Forms.Primitives\src\System.Windows.Forms.Primitives.csproj", "{90B27178-F535-43F7-886E-0AB75203F246}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Windows.Forms.PrivateSourceGenerators", "src\System.Windows.Forms.PrivateSourceGenerators\src\System.Windows.Forms.PrivateSourceGenerators.csproj", "{3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Windows.Forms.Legacy.Demo", "src\System.Windows.Forms.Legacy\System.Windows.Forms.Legacy.Demo\System.Windows.Forms.Legacy.Demo.csproj", "{313D5215-7E76-4864-AEBF-0C15819D0B34}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Windows.Forms.Legacy.Tests", "src\System.Windows.Forms.Legacy\System.Windows.Forms.Legacy.Tests\System.Windows.Forms.Legacy.Tests.csproj", "{EAF018D1-D51E-4F67-BC7F-6F7F5702C411}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "winforms", "winforms", "{FFD82D6B-78FA-4D85-A19D-FF0A12FC47F2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|arm64 = Debug|arm64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|arm64 = Release|arm64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Debug|arm64.ActiveCfg = Debug|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Debug|arm64.Build.0 = Debug|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Debug|x64.ActiveCfg = Debug|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Debug|x64.Build.0 = Debug|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Debug|x86.ActiveCfg = Debug|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Debug|x86.Build.0 = Debug|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Release|Any CPU.Build.0 = Release|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Release|arm64.ActiveCfg = Release|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Release|arm64.Build.0 = Release|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Release|x64.ActiveCfg = Release|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Release|x64.Build.0 = Release|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Release|x86.ActiveCfg = Release|Any CPU + {0D23A41B-2626-4703-9E4A-87C07F69B0B2}.Release|x86.Build.0 = Release|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Debug|arm64.ActiveCfg = Debug|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Debug|arm64.Build.0 = Debug|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Debug|x64.ActiveCfg = Debug|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Debug|x64.Build.0 = Debug|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Debug|x86.ActiveCfg = Debug|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Debug|x86.Build.0 = Debug|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Release|Any CPU.Build.0 = Release|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Release|arm64.ActiveCfg = Release|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Release|arm64.Build.0 = Release|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Release|x64.ActiveCfg = Release|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Release|x64.Build.0 = Release|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Release|x86.ActiveCfg = Release|Any CPU + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B}.Release|x86.Build.0 = Release|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Debug|arm64.ActiveCfg = Debug|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Debug|arm64.Build.0 = Debug|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Debug|x64.ActiveCfg = Debug|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Debug|x64.Build.0 = Debug|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Debug|x86.ActiveCfg = Debug|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Debug|x86.Build.0 = Debug|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Release|Any CPU.Build.0 = Release|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Release|arm64.ActiveCfg = Release|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Release|arm64.Build.0 = Release|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Release|x64.ActiveCfg = Release|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Release|x64.Build.0 = Release|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Release|x86.ActiveCfg = Release|Any CPU + {90B27178-F535-43F7-886E-0AB75203F246}.Release|x86.Build.0 = Release|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Debug|arm64.ActiveCfg = Debug|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Debug|arm64.Build.0 = Debug|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Debug|x64.ActiveCfg = Debug|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Debug|x64.Build.0 = Debug|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Debug|x86.ActiveCfg = Debug|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Debug|x86.Build.0 = Debug|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Release|Any CPU.Build.0 = Release|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Release|arm64.ActiveCfg = Release|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Release|arm64.Build.0 = Release|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Release|x64.ActiveCfg = Release|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Release|x64.Build.0 = Release|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Release|x86.ActiveCfg = Release|Any CPU + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478}.Release|x86.Build.0 = Release|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Debug|arm64.ActiveCfg = Debug|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Debug|arm64.Build.0 = Debug|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Debug|x64.ActiveCfg = Debug|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Debug|x64.Build.0 = Debug|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Debug|x86.ActiveCfg = Debug|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Debug|x86.Build.0 = Debug|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Release|Any CPU.Build.0 = Release|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Release|arm64.ActiveCfg = Release|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Release|arm64.Build.0 = Release|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Release|x64.ActiveCfg = Release|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Release|x64.Build.0 = Release|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Release|x86.ActiveCfg = Release|Any CPU + {313D5215-7E76-4864-AEBF-0C15819D0B34}.Release|x86.Build.0 = Release|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Debug|arm64.ActiveCfg = Debug|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Debug|arm64.Build.0 = Debug|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Debug|x64.ActiveCfg = Debug|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Debug|x64.Build.0 = Debug|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Debug|x86.ActiveCfg = Debug|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Debug|x86.Build.0 = Debug|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Release|Any CPU.Build.0 = Release|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Release|arm64.ActiveCfg = Release|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Release|arm64.Build.0 = Release|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Release|x64.ActiveCfg = Release|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Release|x64.Build.0 = Release|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Release|x86.ActiveCfg = Release|Any CPU + {EAF018D1-D51E-4F67-BC7F-6F7F5702C411}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0D23A41B-2626-4703-9E4A-87C07F69B0B2} = {FFD82D6B-78FA-4D85-A19D-FF0A12FC47F2} + {61D06BBD-B0CF-4CE1-9139-1CC4B82F0F9B} = {FFD82D6B-78FA-4D85-A19D-FF0A12FC47F2} + {90B27178-F535-43F7-886E-0AB75203F246} = {FFD82D6B-78FA-4D85-A19D-FF0A12FC47F2} + {3BB220A9-7BC9-4D45-9DC7-B5A1D659B478} = {FFD82D6B-78FA-4D85-A19D-FF0A12FC47F2} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7B1B0433-F612-4E5A-BE7E-FCF5B9F6E136} + EndGlobalSection +EndGlobal diff --git a/eng/Versions.props b/eng/Versions.props index 6f47e8b3e52..404337fff8b 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -2,9 +2,9 @@ - 10 + 0 0 - 6 + 25 servicing diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/DataGridForm.Designer.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/DataGridForm.Designer.cs new file mode 100644 index 00000000000..73eb97fd33a --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/DataGridForm.Designer.cs @@ -0,0 +1,188 @@ +using System.Drawing; +using System.Windows.Forms; + +namespace Demo +{ + partial class DataGridForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + private GroupBox classicFeaturesGroupBox; + private Label classicFeaturesLabel; + private GroupBox classicOptionsGroupBox; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + + base.Dispose(disposing); + } + + private void InitializeStatusBar() + { + StatusBar mainStatusBar = new StatusBar(); + + mainStatusBar.Dock = DockStyle.Bottom; + mainStatusBar.Height = 70; + + StatusBarPanel statusPanel = new StatusBarPanel(); + StatusBarPanel datetimePanel = new StatusBarPanel(); + + statusPanel.BorderStyle = StatusBarPanelBorderStyle.Sunken; + statusPanel.Text = "Status Bar Example"; + statusPanel.AutoSize = StatusBarPanelAutoSize.Spring; + mainStatusBar.Panels.Add(statusPanel); + + datetimePanel.BorderStyle = StatusBarPanelBorderStyle.Raised; + datetimePanel.Text = System.DateTime.Today.ToLongDateString(); + datetimePanel.AutoSize = StatusBarPanelAutoSize.Contents; + mainStatusBar.Panels.Add(datetimePanel); + + mainStatusBar.ShowPanels = true; + + Controls.Add(mainStatusBar); + } + + private void InitializeDataGrid() + { + button1 = new System.Windows.Forms.Button(); + button2 = new System.Windows.Forms.Button(); + button3 = new System.Windows.Forms.Button(); + button4 = new System.Windows.Forms.Button(); + myDataGrid = new DataGrid(); + featureListBox = new System.Windows.Forms.ListBox(); + selectionSummaryLabel = new System.Windows.Forms.Label(); + captionVisibleCheckBox = new System.Windows.Forms.CheckBox(); + parentRowsVisibleCheckBox = new System.Windows.Forms.CheckBox(); + rowHeadersVisibleCheckBox = new System.Windows.Forms.CheckBox(); + readOnlyCheckBox = new System.Windows.Forms.CheckBox(); + allowNavigationCheckBox = new System.Windows.Forms.CheckBox(); + classicOptionsHintLabel = new System.Windows.Forms.Label(); + + button1.Location = new Point(24, 60); + button1.Size = new Size(160, 30); + button1.Text = "Change Appearance"; + button1.Click += new System.EventHandler(Button1_Click); + + button2.Location = new Point(192, 60); + button2.Size = new Size(160, 30); + button2.Text = "Get Binding Manager"; + button2.Click += new System.EventHandler(Button2_Click); + + button3.Location = new Point(360, 60); + button3.Size = new Size(140, 30); + button3.Text = "Use Flat Mode"; + button3.Click += new System.EventHandler(Button3_Click); + + button4.Location = new Point(508, 60); + button4.Size = new Size(116, 30); + button4.Text = "Next Customer"; + button4.Click += new System.EventHandler(Button4_Click); + + myDataGrid.Location = new Point(24, 100); + myDataGrid.Size = new Size(620, 440); + myDataGrid.CaptionText = "Microsoft DataGrid Control"; + myDataGrid.MouseUp += new MouseEventHandler(Grid_MouseUp); + + selectionSummaryLabel.BorderStyle = BorderStyle.FixedSingle; + selectionSummaryLabel.Location = new Point(24, 548); + selectionSummaryLabel.Size = new Size(620, 52); + selectionSummaryLabel.Text = "Classic demo summary will appear after the grid is initialized."; + selectionSummaryLabel.TextAlign = ContentAlignment.MiddleLeft; + + featureListBox.BorderStyle = BorderStyle.None; + featureListBox.Dock = DockStyle.Fill; + featureListBox.IntegralHeight = false; + + classicFeaturesGroupBox = new GroupBox(); + classicFeaturesLabel = new Label(); + classicOptionsGroupBox = new GroupBox(); + classicFeaturesGroupBox.Location = new Point(668, 100); + classicFeaturesGroupBox.Size = new Size(320, 252); + classicFeaturesGroupBox.Text = "Basic DataGrid Features"; + + classicFeaturesLabel.Dock = DockStyle.Top; + classicFeaturesLabel.Height = 54; + classicFeaturesLabel.Text = "This panel lists the classic behaviors demonstrated by the legacy DataGrid sample."; + + classicFeaturesGroupBox.Controls.Add(featureListBox); + classicFeaturesGroupBox.Controls.Add(classicFeaturesLabel); + + classicOptionsGroupBox.Location = new Point(668, 364); + classicOptionsGroupBox.Size = new Size(320, 236); + classicOptionsGroupBox.Text = "Try Classic Options"; + + captionVisibleCheckBox.Location = new Point(16, 28); + captionVisibleCheckBox.Size = new Size(160, 24); + captionVisibleCheckBox.Text = "Caption Visible"; + captionVisibleCheckBox.CheckedChanged += new System.EventHandler(CaptionVisibleCheckBox_CheckedChanged); + + parentRowsVisibleCheckBox.Location = new Point(16, 58); + parentRowsVisibleCheckBox.Size = new Size(160, 24); + parentRowsVisibleCheckBox.Text = "Parent Rows Visible"; + parentRowsVisibleCheckBox.CheckedChanged += new System.EventHandler(ParentRowsVisibleCheckBox_CheckedChanged); + + rowHeadersVisibleCheckBox.Location = new Point(16, 88); + rowHeadersVisibleCheckBox.Size = new Size(160, 24); + rowHeadersVisibleCheckBox.Text = "Row Headers Visible"; + rowHeadersVisibleCheckBox.CheckedChanged += new System.EventHandler(RowHeadersVisibleCheckBox_CheckedChanged); + + readOnlyCheckBox.Location = new Point(16, 118); + readOnlyCheckBox.Size = new Size(160, 24); + readOnlyCheckBox.Text = "Read Only"; + readOnlyCheckBox.CheckedChanged += new System.EventHandler(ReadOnlyCheckBox_CheckedChanged); + + allowNavigationCheckBox.Location = new Point(16, 148); + allowNavigationCheckBox.Size = new Size(160, 24); + allowNavigationCheckBox.Text = "Allow Navigation"; + allowNavigationCheckBox.CheckedChanged += new System.EventHandler(AllowNavigationCheckBox_CheckedChanged); + + classicOptionsHintLabel.Location = new Point(16, 180); + classicOptionsHintLabel.Size = new Size(288, 42); + classicOptionsHintLabel.Text = "Toggle caption, parent rows, row headers, read-only mode, and relation navigation to exercise the classic DataGrid surface live."; + + classicOptionsGroupBox.Controls.Add(captionVisibleCheckBox); + classicOptionsGroupBox.Controls.Add(parentRowsVisibleCheckBox); + classicOptionsGroupBox.Controls.Add(rowHeadersVisibleCheckBox); + classicOptionsGroupBox.Controls.Add(readOnlyCheckBox); + classicOptionsGroupBox.Controls.Add(allowNavigationCheckBox); + classicOptionsGroupBox.Controls.Add(classicOptionsHintLabel); + + Controls.Add(button1); + Controls.Add(button2); + Controls.Add(button3); + Controls.Add(button4); + Controls.Add(myDataGrid); + Controls.Add(selectionSummaryLabel); + Controls.Add(classicFeaturesGroupBox); + Controls.Add(classicOptionsGroupBox); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + SuspendLayout(); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(1024, 680); + Text = "DataGrid Demo"; + ResumeLayout(false); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/DataGridForm.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/DataGridForm.cs new file mode 100644 index 00000000000..57e60031b9c --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/DataGridForm.cs @@ -0,0 +1,299 @@ +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Windows.Forms; + +#nullable disable + +namespace Demo +{ + /// + /// Phase 4 DataGrid demo surface. + /// + public partial class DataGridForm : Form + { + private DataGrid myDataGrid; + private DataSet myDataSet; + private bool TablesAlreadyAdded; + private Button button1; + private Button button2; + private Button button3; + private Button button4; + private ListBox featureListBox; + private Label selectionSummaryLabel; + private CheckBox captionVisibleCheckBox; + private CheckBox parentRowsVisibleCheckBox; + private CheckBox rowHeadersVisibleCheckBox; + private CheckBox readOnlyCheckBox; + private CheckBox allowNavigationCheckBox; + private Label classicOptionsHintLabel; + + public DataGridForm() + { + InitializeComponent(); + + Shown += DataGridForm_Shown; + } + + private void DataGridForm_Shown(object sender, EventArgs e) + { + InitializeDataGrid(); + SetUp(); + PopulateFeatureList(); + UpdateFeatureToggleStates(); + UpdateSelectionSummary(); + InitializeStatusBar(); + } + + private void SetUp() + { + MakeDataSet(); + myDataGrid.SetDataBinding(myDataSet, "Customers"); + BindingContext[myDataSet, "Customers"].PositionChanged += CustomersBindingManager_PositionChanged; + } + + private void Button1_Click(object sender, EventArgs e) + { + if (TablesAlreadyAdded) + { + return; + } + + AddCustomDataTableStyle(); + } + + private void AddCustomDataTableStyle() + { + DataGridTableStyle ts1 = new() + { + MappingName = "Customers", + AlternatingBackColor = Color.LightGray + }; + + DataGridColumnStyle boolCol = new DataGridBoolColumn + { + MappingName = "Current", + HeaderText = "IsCurrent Customer", + Width = 150 + }; + ts1.GridColumnStyles.Add(boolCol); + + DataGridColumnStyle textCol = new DataGridTextBoxColumn + { + MappingName = "custName", + HeaderText = "Customer Name", + Width = 250 + }; + ts1.GridColumnStyles.Add(textCol); + + DataGridTableStyle ts2 = new() + { + MappingName = "Orders", + AlternatingBackColor = Color.LightBlue + }; + + DataGridTextBoxColumn cOrderDate = new(); + cOrderDate.MappingName = "OrderDate"; + cOrderDate.HeaderText = "Order Date"; + cOrderDate.Width = 100; + ts2.GridColumnStyles.Add(cOrderDate); + + PropertyDescriptorCollection pcol = BindingContext[myDataSet, "Customers.custToOrders"].GetItemProperties(); + + DataGridTextBoxColumn csOrderAmount = new(pcol["OrderAmount"], "c", true) + { + MappingName = "OrderAmount", + HeaderText = "Total", + Width = 100 + }; + ts2.GridColumnStyles.Add(csOrderAmount); + + myDataGrid.TableStyles.Add(ts1); + myDataGrid.TableStyles.Add(ts2); + + TablesAlreadyAdded = true; + } + + private void Button2_Click(object sender, EventArgs e) + { + BindingManagerBase bmGrid = BindingContext[myDataSet, "Customers"]; + MessageBox.Show("Current BindingManager Position: " + bmGrid.Position); + } + + private void Button3_Click(object sender, EventArgs e) + { + myDataGrid.FlatMode = !myDataGrid.FlatMode; + button3.Text = myDataGrid.FlatMode ? "Use 3D Borders" : "Use Flat Mode"; + UpdateSelectionSummary(); + } + + private void Button4_Click(object sender, EventArgs e) + { + CurrencyManager customersManager = (CurrencyManager)BindingContext[myDataSet, "Customers"]; + int nextPosition = customersManager.Position + 1; + + if (nextPosition >= customersManager.Count) + { + nextPosition = 0; + } + + customersManager.Position = nextPosition; + UpdateSelectionSummary(); + } + + private void CaptionVisibleCheckBox_CheckedChanged(object sender, EventArgs e) + { + myDataGrid.CaptionVisible = captionVisibleCheckBox.Checked; + UpdateSelectionSummary(); + } + + private void ParentRowsVisibleCheckBox_CheckedChanged(object sender, EventArgs e) + { + myDataGrid.ParentRowsVisible = parentRowsVisibleCheckBox.Checked; + UpdateSelectionSummary(); + } + + private void RowHeadersVisibleCheckBox_CheckedChanged(object sender, EventArgs e) + { + myDataGrid.RowHeadersVisible = rowHeadersVisibleCheckBox.Checked; + UpdateSelectionSummary(); + } + + private void ReadOnlyCheckBox_CheckedChanged(object sender, EventArgs e) + { + myDataGrid.ReadOnly = readOnlyCheckBox.Checked; + UpdateSelectionSummary(); + } + + private void AllowNavigationCheckBox_CheckedChanged(object sender, EventArgs e) + { + myDataGrid.AllowNavigation = allowNavigationCheckBox.Checked; + UpdateSelectionSummary(); + } + + private void CustomersBindingManager_PositionChanged(object sender, EventArgs e) + { + UpdateSelectionSummary(); + } + + private void Grid_MouseUp(object sender, MouseEventArgs e) + { + DataGrid myGrid = (DataGrid)sender; + DataGrid.HitTestInfo myHitInfo = myGrid.HitTest(e.X, e.Y); + Console.WriteLine(myHitInfo); + Console.WriteLine(myHitInfo.Type); + Console.WriteLine(myHitInfo.Row); + Console.WriteLine(myHitInfo.Column); + + UpdateSelectionSummary(); + } + + private void PopulateFeatureList() + { + featureListBox.Items.Clear(); + featureListBox.Items.Add("Parent/child navigation with Customers -> Orders relation"); + featureListBox.Items.Add("Automatic binding through DataSet and BindingContext"); + featureListBox.Items.Add("Custom table styles for Customers and Orders"); + featureListBox.Items.Add("Boolean column rendering with DataGridBoolColumn"); + featureListBox.Items.Add("Formatted currency and date columns"); + featureListBox.Items.Add("Hit testing for rows, columns, and captions"); + featureListBox.Items.Add("Row navigation using the binding manager"); + featureListBox.Items.Add("Classic flat or 3D border rendering modes"); + } + + private void UpdateFeatureToggleStates() + { + captionVisibleCheckBox.Checked = myDataGrid.CaptionVisible; + parentRowsVisibleCheckBox.Checked = myDataGrid.ParentRowsVisible; + rowHeadersVisibleCheckBox.Checked = myDataGrid.RowHeadersVisible; + readOnlyCheckBox.Checked = myDataGrid.ReadOnly; + allowNavigationCheckBox.Checked = myDataGrid.AllowNavigation; + } + + private void MakeDataSet() + { + myDataSet = new DataSet("myDataSet"); + + DataTable tCust = new DataTable("Customers"); + DataTable tOrders = new DataTable("Orders"); + + DataColumn cCustID = new DataColumn("CustID", typeof(int)); + DataColumn cCustName = new DataColumn("CustName"); + DataColumn cCurrent = new DataColumn("Current", typeof(bool)); + tCust.Columns.Add(cCustID); + tCust.Columns.Add(cCustName); + tCust.Columns.Add(cCurrent); + + DataColumn cID = new DataColumn("CustID", typeof(int)); + DataColumn cOrderDate = new DataColumn("orderDate", typeof(DateTime)); + DataColumn cOrderAmount = new DataColumn("OrderAmount", typeof(decimal)); + tOrders.Columns.Add(cOrderAmount); + tOrders.Columns.Add(cID); + tOrders.Columns.Add(cOrderDate); + + myDataSet.Tables.Add(tCust); + myDataSet.Tables.Add(tOrders); + + DataRelation dr = new DataRelation("custToOrders", cCustID, cID); + myDataSet.Relations.Add(dr); + + DataRow newRow1; + DataRow newRow2; + + for (int i = 1; i < 4; i++) + { + newRow1 = tCust.NewRow(); + newRow1["custID"] = i; + tCust.Rows.Add(newRow1); + } + + tCust.Rows[0]["custName"] = "Customer1"; + tCust.Rows[1]["custName"] = "Customer2"; + tCust.Rows[2]["custName"] = "Customer3"; + + tCust.Rows[0]["Current"] = true; + tCust.Rows[1]["Current"] = true; + tCust.Rows[2]["Current"] = false; + + for (int i = 1; i < 4; i++) + { + for (int j = 1; j < 6; j++) + { + newRow2 = tOrders.NewRow(); + newRow2["CustID"] = i; + newRow2["orderDate"] = new DateTime(2001, i, j * 2); + newRow2["OrderAmount"] = i * 10 + j * .1; + tOrders.Rows.Add(newRow2); + } + } + } + + private void UpdateSelectionSummary() + { + if (myDataSet is null) + { + selectionSummaryLabel.Text = "Classic demo summary will appear after the grid is initialized."; + + return; + } + + CurrencyManager customersManager = (CurrencyManager)BindingContext[myDataSet, "Customers"]; + + if (customersManager.Current is not DataRowView customerView) + { + selectionSummaryLabel.Text = "No active customer row."; + + return; + } + + DataRow[] relatedOrders = customerView.Row.GetChildRows("custToOrders"); + string customerName = customerView["CustName"].ToString(); + string currentFlag = customerView["Current"].ToString(); + string borderMode = myDataGrid is not null && myDataGrid.FlatMode ? "Flat" : "3D"; + string captionMode = myDataGrid.CaptionVisible ? "Caption on" : "Caption off"; + string relationMode = myDataGrid.AllowNavigation ? "Navigation on" : "Navigation off"; + + selectionSummaryLabel.Text = $"Current customer: {customerName} | Active: {currentFlag} | Orders: {relatedOrders.Length} | Border mode: {borderMode} | {captionMode} | {relationMode}"; + } + } +} \ No newline at end of file diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/Directory.Build.props b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/Directory.Build.props new file mode 100644 index 00000000000..9cffb09dc68 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/Directory.Build.props @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/Directory.Build.targets b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/Directory.Build.targets new file mode 100644 index 00000000000..9cffb09dc68 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/Directory.Build.targets @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.Designer.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.Designer.cs new file mode 100644 index 00000000000..09dfbb4f9b6 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.Designer.cs @@ -0,0 +1,113 @@ +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +#nullable disable + +namespace Demo; + +partial class MainForm +{ + private IContainer components = null; + private Label _titleLabel = null!; + private Label _descriptionLabel = null!; + private Button _menuStackButton = null!; + private Button _statusBarButton = null!; + private Button _dataGridButton = null!; + private Button _toolBarButton = null!; + + protected override void Dispose(bool disposing) + { + if (disposing && components is not null) + { + components.Dispose(); + } + + base.Dispose(disposing); + } + + private void InitializeComponent() + { + components = new Container(); + _titleLabel = new Label(); + _descriptionLabel = new Label(); + _menuStackButton = new Button(); + _statusBarButton = new Button(); + _dataGridButton = new Button(); + _toolBarButton = new Button(); + SuspendLayout(); + // + // _titleLabel + // + _titleLabel.Font = new Font("Segoe UI", 14F, FontStyle.Bold, GraphicsUnit.Point); + _titleLabel.Location = new Point(20, 20); + _titleLabel.Name = "_titleLabel"; + _titleLabel.Size = new Size(440, 30); + _titleLabel.TabIndex = 0; + _titleLabel.Text = "WinForms Legacy Controls Demo"; + _titleLabel.TextAlign = ContentAlignment.MiddleCenter; + // + // _descriptionLabel + // + _descriptionLabel.Location = new Point(20, 58); + _descriptionLabel.Name = "_descriptionLabel"; + _descriptionLabel.Size = new Size(440, 36); + _descriptionLabel.TabIndex = 1; + _descriptionLabel.Text = "Select a demo to open. Each form independently exercises one legacy control family."; + _descriptionLabel.TextAlign = ContentAlignment.MiddleCenter; + // + // _menuStackButton + // + _menuStackButton.Location = new Point(50, 108); + _menuStackButton.Name = "_menuStackButton"; + _menuStackButton.Size = new Size(160, 60); + _menuStackButton.TabIndex = 2; + _menuStackButton.Text = "Menu Stack"; + _menuStackButton.Click += MenuStackButton_Click; + // + // _statusBarButton + // + _statusBarButton.Location = new Point(270, 108); + _statusBarButton.Name = "_statusBarButton"; + _statusBarButton.Size = new Size(160, 60); + _statusBarButton.TabIndex = 3; + _statusBarButton.Text = "StatusBar"; + _statusBarButton.Click += StatusBarButton_Click; + // + // _dataGridButton + // + _dataGridButton.Location = new Point(50, 186); + _dataGridButton.Name = "_dataGridButton"; + _dataGridButton.Size = new Size(160, 60); + _dataGridButton.TabIndex = 4; + _dataGridButton.Text = "DataGrid"; + _dataGridButton.Click += DataGridButton_Click; + // + // _toolBarButton + // + _toolBarButton.Location = new Point(270, 186); + _toolBarButton.Name = "_toolBarButton"; + _toolBarButton.Size = new Size(160, 60); + _toolBarButton.TabIndex = 5; + _toolBarButton.Text = "ToolBar"; + _toolBarButton.Click += ToolBarButton_Click; + // + // MainForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(480, 274); + Controls.Add(_toolBarButton); + Controls.Add(_dataGridButton); + Controls.Add(_statusBarButton); + Controls.Add(_menuStackButton); + Controls.Add(_descriptionLabel); + Controls.Add(_titleLabel); + FormBorderStyle = FormBorderStyle.FixedSingle; + MaximizeBox = false; + Name = "MainForm"; + StartPosition = FormStartPosition.CenterScreen; + Text = "Demo Launcher"; + ResumeLayout(false); + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.cs new file mode 100644 index 00000000000..9660b4be738 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MainForm.cs @@ -0,0 +1,35 @@ +using System.Windows.Forms; + +namespace Demo; + +public partial class MainForm : Form +{ + public MainForm() + { + InitializeComponent(); + } + + private void MenuStackButton_Click(object? sender, EventArgs e) + { + MenuStackForm form = new(); + form.Show(this); + } + + private void StatusBarButton_Click(object? sender, EventArgs e) + { + StatusBarForm form = new(); + form.Show(this); + } + + private void DataGridButton_Click(object? sender, EventArgs e) + { + DataGridForm form = new(); + form.Show(this); + } + + private void ToolBarButton_Click(object? sender, EventArgs e) + { + ToolBarForm form = new(); + form.Show(this); + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MenuStackForm.Designer.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MenuStackForm.Designer.cs new file mode 100644 index 00000000000..0ee79b72412 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MenuStackForm.Designer.cs @@ -0,0 +1,147 @@ +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +#nullable disable + +namespace Demo; + +partial class MenuStackForm +{ + private IContainer components = null; + private Label _summaryLabel = null!; + private Button _showSurfaceContextMenuButton = null!; + private Button _clearLogButton = null!; + private Panel _demoSurface = null!; + private Label _surfaceMessageLabel = null!; + private TreeView _menuTreeView = null!; + private ListBox _eventLog = null!; + private Label _treeViewLabel = null!; + private Label _logLabel = null!; + + protected override void Dispose(bool disposing) + { + if (disposing && components is not null) + { + components.Dispose(); + } + + base.Dispose(disposing); + } + + private void InitializeComponent() + { + components = new Container(); + _summaryLabel = new Label(); + _showSurfaceContextMenuButton = new Button(); + _clearLogButton = new Button(); + _demoSurface = new Panel(); + _surfaceMessageLabel = new Label(); + _menuTreeView = new TreeView(); + _eventLog = new ListBox(); + _treeViewLabel = new Label(); + _logLabel = new Label(); + _demoSurface.SuspendLayout(); + SuspendLayout(); + // + // _summaryLabel + // + _summaryLabel.Location = new Point(18, 16); + _summaryLabel.Name = "_summaryLabel"; + _summaryLabel.Size = new Size(774, 40); + _summaryLabel.TabIndex = 0; + _summaryLabel.Text = "Menu stack demo exercising the legacy MainMenu, MenuItem, ContextMenu, dynamic popups, owner-draw items, and TreeNode.ContextMenu."; + // + // _showSurfaceContextMenuButton + // + _showSurfaceContextMenuButton.Location = new Point(18, 65); + _showSurfaceContextMenuButton.Name = "_showSurfaceContextMenuButton"; + _showSurfaceContextMenuButton.Size = new Size(188, 30); + _showSurfaceContextMenuButton.TabIndex = 1; + _showSurfaceContextMenuButton.Text = "Show Surface Context Menu"; + _showSurfaceContextMenuButton.Click += ShowSurfaceContextMenuButton_Click; + // + // _clearLogButton + // + _clearLogButton.Location = new Point(218, 65); + _clearLogButton.Name = "_clearLogButton"; + _clearLogButton.Size = new Size(120, 30); + _clearLogButton.TabIndex = 2; + _clearLogButton.Text = "Clear Log"; + _clearLogButton.Click += ClearLogButton_Click; + // + // _demoSurface + // + _demoSurface.BackColor = Color.WhiteSmoke; + _demoSurface.BorderStyle = BorderStyle.FixedSingle; + _demoSurface.Controls.Add(_surfaceMessageLabel); + _demoSurface.Location = new Point(18, 112); + _demoSurface.Name = "_demoSurface"; + _demoSurface.Size = new Size(384, 220); + _demoSurface.TabIndex = 5; + // + // _surfaceMessageLabel + // + _surfaceMessageLabel.Dock = DockStyle.Fill; + _surfaceMessageLabel.Location = new Point(0, 0); + _surfaceMessageLabel.Name = "_surfaceMessageLabel"; + _surfaceMessageLabel.Size = new Size(382, 218); + _surfaceMessageLabel.TabIndex = 0; + _surfaceMessageLabel.Text = "Right-click this surface or the TreeView nodes to exercise ContextMenu and TreeNode.ContextMenu support. Use the MainMenu above for menu actions."; + _surfaceMessageLabel.TextAlign = ContentAlignment.MiddleCenter; + // + // _menuTreeView + // + _menuTreeView.HideSelection = false; + _menuTreeView.Location = new Point(424, 134); + _menuTreeView.Name = "_menuTreeView"; + _menuTreeView.Size = new Size(368, 198); + _menuTreeView.TabIndex = 7; + _menuTreeView.NodeMouseClick += MenuTreeView_NodeMouseClick; + // + // _eventLog + // + _eventLog.FormattingEnabled = true; + _eventLog.HorizontalScrollbar = true; + _eventLog.Location = new Point(18, 368); + _eventLog.Name = "_eventLog"; + _eventLog.Size = new Size(774, 184); + _eventLog.TabIndex = 9; + // + // _treeViewLabel + // + _treeViewLabel.Location = new Point(424, 112); + _treeViewLabel.Name = "_treeViewLabel"; + _treeViewLabel.Size = new Size(210, 18); + _treeViewLabel.TabIndex = 6; + _treeViewLabel.Text = "TreeView and TreeNode.ContextMenu demo"; + // + // _logLabel + // + _logLabel.Location = new Point(18, 346); + _logLabel.Name = "_logLabel"; + _logLabel.Size = new Size(180, 18); + _logLabel.TabIndex = 8; + _logLabel.Text = "Menu event log"; + // + // MenuStackForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(810, 572); + Controls.Add(_logLabel); + Controls.Add(_treeViewLabel); + Controls.Add(_eventLog); + Controls.Add(_menuTreeView); + Controls.Add(_demoSurface); + Controls.Add(_clearLogButton); + Controls.Add(_showSurfaceContextMenuButton); + Controls.Add(_summaryLabel); + MinimumSize = new Size(826, 611); + Name = "MenuStackForm"; + StartPosition = FormStartPosition.CenterParent; + Text = "Phase 1: Menu Stack"; + _demoSurface.ResumeLayout(false); + ResumeLayout(false); + } +} \ No newline at end of file diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MenuStackForm.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MenuStackForm.cs new file mode 100644 index 00000000000..73e33156071 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/MenuStackForm.cs @@ -0,0 +1,376 @@ +using System.Drawing; +using System.Windows.Forms; + +namespace Demo; + +public partial class MenuStackForm : Form +{ + private readonly ContextMenu _surfaceContextMenu; + private readonly ContextMenu _treeViewContextMenu; + private int _dynamicMenuGeneration; + + public MenuStackForm() + { + InitializeComponent(); + + Menu = CreateMainMenu(); + + _surfaceContextMenu = CreateSurfaceContextMenu(); + _demoSurface.ContextMenu = _surfaceContextMenu; + + _treeViewContextMenu = CreateTreeViewContextMenu(); + _menuTreeView.ContextMenu = _treeViewContextMenu; + + InitializeTreeView(); + AppendLog("Menu stack demo ready."); + } + + private MainMenu CreateMainMenu() + { + MenuItem fileMenuItem = new("File"); + MenuItem newMenuItem = new("New"); + MenuItem newProjectItem = new("Project...", OnMenuActionClicked); + MenuItem newRepositoryItem = new("Repository...", OnMenuActionClicked); + MenuItem newFileItem = new("File...", OnMenuActionClicked); + MenuItem openMenuItem = new("Open", OnMenuActionClicked) + { + Shortcut = Shortcut.AltF12, + ShowShortcut = true + }; + MenuItem saveMenuItem = new("Save", OnMenuActionClicked, Shortcut.CtrlS) + { + Checked = true, + RadioCheck = true + }; + MenuItem exitMenuItem = new("Close Demo", (_, _) => Close()); + + newMenuItem.MenuItems.Add(newProjectItem); + newMenuItem.MenuItems.Add(newRepositoryItem); + newMenuItem.MenuItems.Add(newFileItem); + + fileMenuItem.MenuItems.Add(newMenuItem); + fileMenuItem.MenuItems.Add(openMenuItem); + fileMenuItem.MenuItems.Add(saveMenuItem); + fileMenuItem.MenuItems.Add(new MenuItem("-")); + fileMenuItem.MenuItems.Add(exitMenuItem); + + MenuItem viewMenuItem = new("View"); + viewMenuItem.MenuItems.Add(new MenuItem("Toolbox", OnMenuActionClicked)); + viewMenuItem.MenuItems.Add(new MenuItem("Terminal", OnMenuActionClicked)); + viewMenuItem.MenuItems.Add(new MenuItem("Output", OnMenuActionClicked)); + + MenuItem dynamicMenuItem = new("Dynamic"); + dynamicMenuItem.Popup += DynamicMenuItem_Popup; + dynamicMenuItem.MenuItems.Add(new MenuItem("Dynamic code has not run yet")); + + MenuItem ownerDrawMenuItem = new("Owner Draw Demo"); + ownerDrawMenuItem.MenuItems.Add(new MenuItem("Standard Item", OnMenuActionClicked)); + ownerDrawMenuItem.MenuItems.Add(new MenuItem("-")); + ownerDrawMenuItem.MenuItems.Add(CreateOwnerDrawMenuItem("Custom Draw Item 1")); + ownerDrawMenuItem.MenuItems.Add(CreateOwnerDrawMenuItem("Custom Draw Item 2", createNestedItems: true)); + + MenuItem contextActionsItem = new("Context Actions"); + contextActionsItem.MenuItems.Add(new MenuItem("Show Surface Menu", ShowSurfaceMenuMenuItem_Click)); + contextActionsItem.MenuItems.Add(new MenuItem("Clear Log", (_, _) => _eventLog.Items.Clear())); + + AttachMenuTracing(fileMenuItem); + AttachMenuTracing(viewMenuItem); + AttachMenuTracing(dynamicMenuItem); + AttachMenuTracing(ownerDrawMenuItem); + AttachMenuTracing(contextActionsItem); + + MainMenu mainMenu = new(); + mainMenu.MenuItems.Add(fileMenuItem); + mainMenu.MenuItems.Add(viewMenuItem); + mainMenu.MenuItems.Add(dynamicMenuItem); + mainMenu.MenuItems.Add(ownerDrawMenuItem); + mainMenu.MenuItems.Add(contextActionsItem); + + return mainMenu; + } + + private ContextMenu CreateSurfaceContextMenu() + { + ContextMenu contextMenu = new(); + + MenuItem inspectItem = new("Inspect Surface", OnContextMenuActionClicked); + MenuItem toggleHighlightItem = new("Toggle Highlight", ToggleSurfaceHighlightMenuItem_Click); + MenuItem timestampItem = new("Insert Timestamp", InsertTimestampMenuItem_Click); + + contextMenu.MenuItems.Add(inspectItem); + contextMenu.MenuItems.Add(toggleHighlightItem); + contextMenu.MenuItems.Add(new MenuItem("-")); + contextMenu.MenuItems.Add(timestampItem); + + contextMenu.Popup += (_, _) => AppendLog("Surface context menu opened."); + + return contextMenu; + } + + private ContextMenu CreateTreeViewContextMenu() + { + ContextMenu contextMenu = new(); + MenuItem inspectTreeItem = new("Inspect TreeView", (_, _) => AppendLog("Inspect TreeView context action executed.")); + MenuItem expandAllItem = new("Expand All Nodes", (_, _) => + { + _menuTreeView.ExpandAll(); + AppendLog("TreeView context action: expand all nodes."); + }); + MenuItem collapseAllItem = new("Collapse All Nodes", (_, _) => + { + _menuTreeView.CollapseAll(); + AppendLog("TreeView context action: collapse all nodes."); + }); + + contextMenu.MenuItems.Add(inspectTreeItem); + contextMenu.MenuItems.Add(new MenuItem("-")); + contextMenu.MenuItems.Add(expandAllItem); + contextMenu.MenuItems.Add(collapseAllItem); + contextMenu.Popup += (_, _) => AppendLog("TreeView context menu opened."); + + return contextMenu; + } + + private OwnerDrawMenuItem CreateOwnerDrawMenuItem(string text, bool createNestedItems = false) + { + OwnerDrawMenuItem item = new() + { + Text = text + }; + + item.Click += OnOwnerDrawItemClicked; + + if (!createNestedItems) + { + item.MenuItems.Add(new OwnerDrawMenuItem { Text = text + " / Sub Item A" }); + item.MenuItems.Add(new OwnerDrawMenuItem { Text = text + " / Sub Item B" }); + + foreach (MenuItem subItem in item.MenuItems) + { + subItem.Click += OnOwnerDrawItemClicked; + } + + return item; + } + + OwnerDrawMenuItem nestedItemA = new() + { + Text = "Nested Custom Item A" + }; + OwnerDrawMenuItem nestedItemB = new() + { + Text = "Nested Custom Item B" + }; + MenuItem deepSubmenu = new("Deep Submenu"); + OwnerDrawMenuItem deepItem1 = new() + { + Text = "Deep Custom Item 1" + }; + OwnerDrawMenuItem deepItem2 = new() + { + Text = "Deep Custom Item 2" + }; + + nestedItemA.Click += OnOwnerDrawItemClicked; + nestedItemB.Click += OnOwnerDrawItemClicked; + deepItem1.Click += OnOwnerDrawItemClicked; + deepItem2.Click += OnOwnerDrawItemClicked; + + deepSubmenu.MenuItems.Add(deepItem1); + deepSubmenu.MenuItems.Add(deepItem2); + + item.MenuItems.Add(nestedItemA); + item.MenuItems.Add(nestedItemB); + item.MenuItems.Add(new MenuItem("-")); + item.MenuItems.Add(deepSubmenu); + + return item; + } + + private void AttachMenuTracing(MenuItem rootItem) + { + rootItem.Select += (_, _) => AppendLog($"Selected menu item: {rootItem.Text}"); + + if (rootItem.MenuItems.Count > 0) + { + rootItem.Popup += (_, _) => AppendLog($"Popup menu opened: {rootItem.Text}"); + } + + foreach (MenuItem childItem in rootItem.MenuItems) + { + AttachMenuTracing(childItem); + } + } + + private void InitializeTreeView() + { + TreeNode rootNode = new("Menu Demo Root") + { + Nodes = + { + new TreeNode("Context Menu Node"), + new TreeNode("Owner Draw Node") + { + Nodes = + { + new TreeNode("Nested Node A"), + new TreeNode("Nested Node B") + } + }, + new TreeNode("Shortcut Node") + } + }; + + _menuTreeView.Nodes.Clear(); + _menuTreeView.Nodes.Add(rootNode); + rootNode.Expand(); + + AddContextMenusToNodes(_menuTreeView.Nodes); + } + + private void AddContextMenusToNodes(TreeNodeCollection nodes) + { + foreach (TreeNode node in nodes) + { + ContextMenu nodeContextMenu = new(); + MenuItem inspectNodeItem = new($"Inspect {node.Text}", (_, _) => + { + _surfaceMessageLabel.Text = "Inspecting node: " + node.Text; + AppendLog($"TreeNode.ContextMenu action: inspect {node.Text}"); + }); + MenuItem toggleCheckedItem = new($"Toggle check state for {node.Text}", (_, _) => + { + node.Checked = !node.Checked; + AppendLog($"TreeNode.ContextMenu action: {(node.Checked ? "checked" : "unchecked")} {node.Text}"); + }); + MenuItem runNodeItem = new($"Run action for {node.Text}", (_, _) => MessageBox.Show(this, $"Action executed for {node.Text}", Text)); + + nodeContextMenu.MenuItems.Add(inspectNodeItem); + nodeContextMenu.MenuItems.Add(toggleCheckedItem); + nodeContextMenu.MenuItems.Add(runNodeItem); + nodeContextMenu.Popup += (_, _) => AppendLog($"TreeNode.ContextMenu opened for {node.Text}"); + node.ContextMenu = nodeContextMenu; + + if (node.Nodes.Count > 0) + { + AddContextMenusToNodes(node.Nodes); + } + } + } + + private void DynamicMenuItem_Popup(object? sender, EventArgs e) + { + if (sender is not MenuItem dynamicMenuItem) + { + return; + } + + _dynamicMenuGeneration++; + dynamicMenuItem.MenuItems.Clear(); + + for (int i = 1; i <= 5; i++) + { + MenuItem item = new($"Dynamic Item {_dynamicMenuGeneration}.{i}"); + item.Click += OnMenuActionClicked; + dynamicMenuItem.MenuItems.Add(item); + } + + AppendLog($"Dynamic menu rebuilt #{_dynamicMenuGeneration}."); + } + + private void OnMenuActionClicked(object? sender, EventArgs e) + { + if (sender is not MenuItem menuItem) + { + return; + } + + menuItem.Checked = !menuItem.Checked && menuItem.Text.EndsWith("...", StringComparison.Ordinal); + AppendLog($"Menu click: {menuItem.Text}"); + MessageBox.Show(this, $"{menuItem.Text} clicked.", Text); + } + + private void OnOwnerDrawItemClicked(object? sender, EventArgs e) + { + if (sender is not MenuItem menuItem) + { + return; + } + + AppendLog($"Owner-draw click: {menuItem.Text}"); + MessageBox.Show(this, $"Owner-draw item clicked: {menuItem.Text}", Text); + } + + private void OnContextMenuActionClicked(object? sender, EventArgs e) + { + if (sender is not MenuItem menuItem) + { + return; + } + + AppendLog($"Context action: {menuItem.Text}"); + MessageBox.Show(this, $"{menuItem.Text} executed.", Text); + } + + private void ToggleSurfaceHighlightMenuItem_Click(object? sender, EventArgs e) + { + bool useHighlight = _demoSurface.BackColor != Color.LightGoldenrodYellow; + _demoSurface.BackColor = useHighlight ? Color.LightGoldenrodYellow : Color.WhiteSmoke; + + AppendLog(useHighlight ? "Surface highlight enabled." : "Surface highlight cleared."); + } + + private void InsertTimestampMenuItem_Click(object? sender, EventArgs e) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss"); + _surfaceMessageLabel.Text = "Last context action at " + timestamp; + AppendLog("Timestamp inserted: " + timestamp); + } + + private void ShowSurfaceMenuMenuItem_Click(object? sender, EventArgs e) + { + ShowSurfaceContextMenu(); + } + + private void ShowSurfaceContextMenuButton_Click(object? sender, EventArgs e) + { + ShowSurfaceContextMenu(); + } + + private void ClearLogButton_Click(object? sender, EventArgs e) + { + _eventLog.Items.Clear(); + AppendLog("Log cleared."); + } + + private void MenuTreeView_NodeMouseClick(object? sender, TreeNodeMouseClickEventArgs e) + { + if (e.Node is null) + { + return; + } + + _menuTreeView.SelectedNode = e.Node; + AppendLog($"Tree node clicked: {e.Node.Text}"); + } + + private void ShowSurfaceContextMenu() + { + Point screenPoint = _demoSurface.PointToClient(MousePosition); + Point menuPoint = screenPoint.X >= 0 && screenPoint.Y >= 0 + ? screenPoint + : new Point(_demoSurface.Width / 2, _demoSurface.Height / 2); + + _surfaceContextMenu.Show(_demoSurface, menuPoint); + } + + private void AppendLog(string message) + { + _eventLog.Items.Insert(0, $"[{DateTime.Now:HH:mm:ss}] {message}"); + + if (_eventLog.Items.Count > 200) + { + _eventLog.Items.RemoveAt(_eventLog.Items.Count - 1); + } + } +} \ No newline at end of file diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/OwnerDrawMenuItem.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/OwnerDrawMenuItem.cs new file mode 100644 index 00000000000..59fd0f3d47b --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/OwnerDrawMenuItem.cs @@ -0,0 +1,109 @@ +#if NET +using System.Drawing; +using System.Windows.Forms; +#endif + +namespace Demo +{ + internal sealed class OwnerDrawMenuItem : MenuItem + { + public OwnerDrawMenuItem() : base() + { + OwnerDraw = true; + } + + protected override void OnDrawItem(DrawItemEventArgs e) + { + // Get the drawing area + Rectangle bounds = e.Bounds; + Graphics g = e.Graphics; + + // Determine colors based on item state + Color backColor = e.BackColor; + Color textColor = e.ForeColor; + + // Custom styling for selected/highlighted state + if ((e.State & DrawItemState.Selected) == DrawItemState.Selected) + { + backColor = Color.LightBlue; + textColor = Color.DarkBlue; + } + + // Draw background + using (Brush backBrush = new SolidBrush(backColor)) + { + g.FillRectangle(backBrush, bounds); + } + + // Draw an icon area (simple colored rectangle as demo) + Rectangle iconRect = new Rectangle(bounds.X + 4, bounds.Y + 2, 16, 16); + using (Brush iconBrush = new SolidBrush(Color.Green)) + { + g.FillRectangle(iconBrush, iconRect); + } + + // Draw a simple "check mark" in the icon area + using (Pen checkPen = new Pen(Color.White, 2)) + { + Point[] checkPoints = { + new (iconRect.X + 3, iconRect.Y + 8), + new (iconRect.X + 7, iconRect.Y + 12), + new (iconRect.X + 13, iconRect.Y + 4) + }; + g.DrawLines(checkPen, checkPoints); + } + + // Calculate text area (leaving space for icon and margins) + Rectangle textRect = new Rectangle( + bounds.X + 24, // Start after icon area (4 + 16 + 4 spacing) + bounds.Y + 1, + bounds.Width - 28, // Total margin: 4 (left) + 16 (icon) + 4 (spacing) + 4 (right) = 28 + bounds.Height - 2 + ); + + // Draw the menu text + using (Brush textBrush = new SolidBrush(textColor)) + { + StringFormat format = new StringFormat() + { + Alignment = StringAlignment.Near, + LineAlignment = StringAlignment.Center + }; + + Font font = e.Font ?? SystemInformation.MenuFont; + g.DrawString(Text, font, textBrush, textRect, format); + } + + // Draw focus rectangle if the item has focus + if ((e.State & DrawItemState.Focus) == DrawItemState.Focus) + { + e.DrawFocusRectangle(); + } + + // Draw a subtle shadow effect at the bottom + using (Pen shadowPen = new Pen(Color.FromArgb(50, 0, 0, 0))) + { + g.DrawLine(shadowPen, bounds.X, bounds.Bottom - 1, bounds.Right, bounds.Bottom - 1); + } + } + + protected override void OnMeasureItem(MeasureItemEventArgs e) + { + Font font = SystemInformation.MenuFont; + + string text = Text ?? string.Empty; + if (string.IsNullOrEmpty(text)) + { + text = " "; + } + + var stringSize = e.Graphics.MeasureString(text, font); + e.ItemWidth = (int)Math.Ceiling(stringSize.Width) + 28; + + int minHeightForIcon = 20; + int textHeight = (int)Math.Ceiling(stringSize.Height) + 4; + + e.ItemHeight = Math.Max(minHeightForIcon, textHeight); + } + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/Program.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/Program.cs new file mode 100644 index 00000000000..5cae1e14eeb --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/Program.cs @@ -0,0 +1,38 @@ +#if NET +using System.Windows.Forms; +#endif + +namespace Demo +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + // Set High DPI mode to DpiUnaware, as currently there are some scaling issues when setting to other values + // https://github.com/WiseTechGlobal/Modernization.Content/blob/main/Controls/work-items/Difference%20display%20between%20migrated%20forms%20and%20original%20forms.md +#if NET + Application.SetHighDpiMode(HighDpiMode.DpiUnaware); +#endif + //ApplicationConfiguration.Initialize(); + Application.Run(new MainForm()); + } + } +} + +#if NETFRAMEWORK +namespace System.Runtime.Versioning +{ + class SupportedOSPlatformAttribute : Attribute + { + public SupportedOSPlatformAttribute(string platformName) { } + } +} +#endif diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/StatusBarForm.Designer.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/StatusBarForm.Designer.cs new file mode 100644 index 00000000000..1058ed50e41 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/StatusBarForm.Designer.cs @@ -0,0 +1,141 @@ +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; +using Timer = System.Windows.Forms.Timer; + +#nullable disable + +namespace Demo; + +partial class StatusBarForm +{ + private IContainer components = null; + private Label _summaryLabel = null!; + private Button _showPanelsButton = null!; + private Button _showSimpleTextButton = null!; + private Button _addPanelButton = null!; + private Button _removePanelButton = null!; + private Button _clearLogButton = null!; + private Label _logLabel = null!; + private ListBox _eventLog = null!; + private Timer _clockTimer = null!; + + protected override void Dispose(bool disposing) + { + if (disposing && components is not null) + { + components.Dispose(); + } + + base.Dispose(disposing); + } + + private void InitializeComponent() + { + components = new Container(); + _summaryLabel = new Label(); + _showPanelsButton = new Button(); + _showSimpleTextButton = new Button(); + _addPanelButton = new Button(); + _removePanelButton = new Button(); + _clearLogButton = new Button(); + _logLabel = new Label(); + _eventLog = new ListBox(); + _clockTimer = new Timer(components); + SuspendLayout(); + // + // _summaryLabel + // + _summaryLabel.Location = new Point(18, 16); + _summaryLabel.Name = "_summaryLabel"; + _summaryLabel.Size = new Size(654, 40); + _summaryLabel.TabIndex = 0; + _summaryLabel.Text = "Phase 2 status bar demo covering legacy StatusBar panel layout, owner-draw rendering, dynamic panel mutation, panel-region tooltips, and click events."; + // + // _showPanelsButton + // + _showPanelsButton.Location = new Point(18, 72); + _showPanelsButton.Name = "_showPanelsButton"; + _showPanelsButton.Size = new Size(120, 30); + _showPanelsButton.TabIndex = 1; + _showPanelsButton.Text = "Show Panels"; + _showPanelsButton.Click += ShowPanelsButton_Click; + // + // _showSimpleTextButton + // + _showSimpleTextButton.Location = new Point(150, 72); + _showSimpleTextButton.Name = "_showSimpleTextButton"; + _showSimpleTextButton.Size = new Size(142, 30); + _showSimpleTextButton.TabIndex = 2; + _showSimpleTextButton.Text = "Show Simple Text"; + _showSimpleTextButton.Click += ShowSimpleTextButton_Click; + // + // _addPanelButton + // + _addPanelButton.Location = new Point(304, 72); + _addPanelButton.Name = "_addPanelButton"; + _addPanelButton.Size = new Size(112, 30); + _addPanelButton.TabIndex = 3; + _addPanelButton.Text = "Add Panel"; + _addPanelButton.Click += AddPanelButton_Click; + // + // _removePanelButton + // + _removePanelButton.Location = new Point(428, 72); + _removePanelButton.Name = "_removePanelButton"; + _removePanelButton.Size = new Size(120, 30); + _removePanelButton.TabIndex = 4; + _removePanelButton.Text = "Remove Panel"; + _removePanelButton.Click += RemovePanelButton_Click; + // + // _clearLogButton + // + _clearLogButton.Location = new Point(560, 72); + _clearLogButton.Name = "_clearLogButton"; + _clearLogButton.Size = new Size(112, 30); + _clearLogButton.TabIndex = 5; + _clearLogButton.Text = "Clear Log"; + _clearLogButton.Click += ClearLogButton_Click; + // + // _logLabel + // + _logLabel.Location = new Point(18, 124); + _logLabel.Name = "_logLabel"; + _logLabel.Size = new Size(180, 18); + _logLabel.TabIndex = 6; + _logLabel.Text = "StatusBar event log"; + // + // _eventLog + // + _eventLog.FormattingEnabled = true; + _eventLog.HorizontalScrollbar = true; + _eventLog.Location = new Point(18, 146); + _eventLog.Name = "_eventLog"; + _eventLog.Size = new Size(654, 214); + _eventLog.TabIndex = 7; + // + // _clockTimer + // + _clockTimer.Interval = 1000; + _clockTimer.Tick += ClockTimer_Tick; + // + // StatusBarForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(692, 408); + Controls.Add(_eventLog); + Controls.Add(_logLabel); + Controls.Add(_clearLogButton); + Controls.Add(_removePanelButton); + Controls.Add(_addPanelButton); + Controls.Add(_showSimpleTextButton); + Controls.Add(_showPanelsButton); + Controls.Add(_summaryLabel); + MinimumSize = new Size(708, 447); + Name = "StatusBarForm"; + StartPosition = FormStartPosition.CenterParent; + Text = "Phase 2: StatusBar"; + ResumeLayout(false); + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/StatusBarForm.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/StatusBarForm.cs new file mode 100644 index 00000000000..1a9c0ba9703 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/StatusBarForm.cs @@ -0,0 +1,158 @@ +using System.Drawing; +using System.Windows.Forms; + +namespace Demo; + +public partial class StatusBarForm : Form +{ + private readonly StatusBar _statusBar; + private StatusBarPanel _modePanel; + private StatusBarPanel _detailsPanel; + private StatusBarPanel _ownerDrawPanel; + private StatusBarPanel _clockPanel; + private int _dynamicPanelCount; + + public StatusBarForm() + { + InitializeComponent(); + + _statusBar = CreateStatusBar(); + Controls.Add(_statusBar); + + _clockTimer.Start(); + AppendLog("StatusBar demo ready."); + } + + private StatusBar CreateStatusBar() + { + StatusBar statusBar = new() + { + Dock = DockStyle.Bottom, + ShowPanels = true, + SizingGrip = true + }; + + _modePanel = new StatusBarPanel + { + Text = "Panels Mode", + AutoSize = StatusBarPanelAutoSize.Contents, + ToolTipText = "The current StatusBar presentation mode." + }; + _detailsPanel = new StatusBarPanel + { + Text = "Click a panel or use the controls above to mutate the legacy StatusBar surface.", + AutoSize = StatusBarPanelAutoSize.Spring, + ToolTipText = "Spring-sized panel text." + }; + _ownerDrawPanel = new StatusBarPanel + { + Style = StatusBarPanelStyle.OwnerDraw, + Width = 150, + ToolTipText = "Owner-drawn panel that displays the current panel count." + }; + _clockPanel = new StatusBarPanel + { + Text = DateTime.Now.ToString("HH:mm:ss"), + AutoSize = StatusBarPanelAutoSize.Contents, + Alignment = HorizontalAlignment.Right, + ToolTipText = "Clock panel updated once per second." + }; + + statusBar.Panels.AddRange([_modePanel, _detailsPanel, _ownerDrawPanel, _clockPanel]); + statusBar.PanelClick += StatusBar_PanelClick; + statusBar.DrawItem += StatusBar_DrawItem; + + return statusBar; + } + + private void StatusBar_DrawItem(object sender, StatusBarDrawItemEventArgs e) + { + using SolidBrush backgroundBrush = new(Color.FromArgb(231, 238, 247)); + using Pen borderPen = new(Color.SteelBlue); + + e.Graphics.FillRectangle(backgroundBrush, e.Bounds); + Rectangle borderBounds = Rectangle.Inflate(e.Bounds, -1, -1); + e.Graphics.DrawRectangle(borderPen, borderBounds); + + TextRenderer.DrawText( + e.Graphics, + $"Panels: {_statusBar.Panels.Count}", + Font, + e.Bounds, + Color.MidnightBlue, + TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine); + } + + private void StatusBar_PanelClick(object sender, StatusBarPanelClickEventArgs e) + { + _detailsPanel.Text = "Clicked panel: " + e.StatusBarPanel.Text; + AppendLog("Panel click: " + e.StatusBarPanel.Text); + } + + private void ShowPanelsButton_Click(object? sender, EventArgs e) + { + _statusBar.ShowPanels = true; + _modePanel.Text = "Panels Mode"; + _detailsPanel.Text = "Panels restored with spring, owner-draw, and clock behavior."; + _statusBar.Invalidate(); + AppendLog("Switched StatusBar to panel mode."); + } + + private void ShowSimpleTextButton_Click(object? sender, EventArgs e) + { + _statusBar.ShowPanels = false; + _statusBar.Text = "Simple text mode at " + DateTime.Now.ToString("HH:mm:ss"); + AppendLog("Switched StatusBar to simple text mode."); + } + + private void AddPanelButton_Click(object? sender, EventArgs e) + { + _dynamicPanelCount++; + StatusBarPanel panel = new() + { + Text = "Dynamic " + _dynamicPanelCount, + AutoSize = StatusBarPanelAutoSize.Contents, + ToolTipText = "Runtime-added panel #" + _dynamicPanelCount + }; + + _statusBar.Panels.Insert(Math.Max(0, _statusBar.Panels.Count - 1), panel); + _statusBar.Invalidate(); + AppendLog("Added dynamic panel: " + panel.Text); + } + + private void RemovePanelButton_Click(object? sender, EventArgs e) + { + if (_statusBar.Panels.Count <= 4) + { + AppendLog("No dynamic panel available to remove."); + + return; + } + + StatusBarPanel panel = _statusBar.Panels[_statusBar.Panels.Count - 2]; + _statusBar.Panels.Remove(panel); + _statusBar.Invalidate(); + AppendLog("Removed dynamic panel: " + panel.Text); + } + + private void ClearLogButton_Click(object? sender, EventArgs e) + { + _eventLog.Items.Clear(); + AppendLog("Log cleared."); + } + + private void ClockTimer_Tick(object? sender, EventArgs e) + { + _clockPanel.Text = DateTime.Now.ToString("HH:mm:ss"); + } + + private void AppendLog(string message) + { + _eventLog.Items.Insert(0, $"[{DateTime.Now:HH:mm:ss}] {message}"); + + if (_eventLog.Items.Count > 200) + { + _eventLog.Items.RemoveAt(_eventLog.Items.Count - 1); + } + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/System.Windows.Forms.Legacy.Demo.csproj b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/System.Windows.Forms.Legacy.Demo.csproj new file mode 100644 index 00000000000..456847a3650 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/System.Windows.Forms.Legacy.Demo.csproj @@ -0,0 +1,34 @@ + + + + WinExe + net10.0-windows;net48 + enable + preview + enable + $(NoWarn);SA1400;CS0649;WFDEV006;NU1602 + + + + true + + + + false + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/ToolBarForm.Designer.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/ToolBarForm.Designer.cs new file mode 100644 index 00000000000..1d131f54b19 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/ToolBarForm.Designer.cs @@ -0,0 +1,131 @@ +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +#nullable disable + +namespace Demo; + +partial class ToolBarForm +{ + private IContainer components = null; + private Panel _contentPanel = null!; + private Label _summaryLabel = null!; + private Label _statusLabel = null!; + private Label _logLabel = null!; + private ListBox _eventLog = null!; + private Button _clearLogButton = null!; + private Button _toggleAppearanceButton = null!; + private Button _toggleWrappableButton = null!; + + protected override void Dispose(bool disposing) + { + if (disposing && components is not null) + { + components.Dispose(); + } + + base.Dispose(disposing); + } + + private void InitializeComponent() + { + components = new Container(); + _contentPanel = new Panel(); + _summaryLabel = new Label(); + _statusLabel = new Label(); + _logLabel = new Label(); + _eventLog = new ListBox(); + _clearLogButton = new Button(); + _toggleAppearanceButton = new Button(); + _toggleWrappableButton = new Button(); + _contentPanel.SuspendLayout(); + SuspendLayout(); + // + // _summaryLabel + // + _summaryLabel.Location = new Point(18, 8); + _summaryLabel.Name = "_summaryLabel"; + _summaryLabel.Size = new Size(554, 40); + _summaryLabel.TabIndex = 0; + _summaryLabel.Text = "ToolBar demo exercising button styles (push, toggle, separator, drop-down), appearance (Normal/Flat), and the Wrappable property."; + // + // _toggleAppearanceButton + // + _toggleAppearanceButton.Location = new Point(18, 62); + _toggleAppearanceButton.Name = "_toggleAppearanceButton"; + _toggleAppearanceButton.Size = new Size(148, 26); + _toggleAppearanceButton.TabIndex = 5; + _toggleAppearanceButton.Text = "Appearance: Normal"; + _toggleAppearanceButton.Click += ToggleAppearanceButton_Click; + // + // _toggleWrappableButton + // + _toggleWrappableButton.Location = new Point(176, 62); + _toggleWrappableButton.Name = "_toggleWrappableButton"; + _toggleWrappableButton.Size = new Size(148, 26); + _toggleWrappableButton.TabIndex = 6; + _toggleWrappableButton.Text = "Wrappable: False"; + _toggleWrappableButton.Click += ToggleWrappableButton_Click; + // + // _statusLabel + // + _statusLabel.BorderStyle = BorderStyle.FixedSingle; + _statusLabel.Location = new Point(18, 102); + _statusLabel.Name = "_statusLabel"; + _statusLabel.Size = new Size(554, 32); + _statusLabel.TabIndex = 1; + _statusLabel.Text = "Use the ToolBar buttons and the toggle controls above to exercise ToolBar behavior."; + _statusLabel.TextAlign = ContentAlignment.MiddleLeft; + // + // _logLabel + // + _logLabel.Location = new Point(18, 148); + _logLabel.Name = "_logLabel"; + _logLabel.Size = new Size(120, 18); + _logLabel.TabIndex = 2; + _logLabel.Text = "ToolBar event log"; + // + // _clearLogButton + // + _clearLogButton.Location = new Point(452, 138); + _clearLogButton.Name = "_clearLogButton"; + _clearLogButton.Size = new Size(120, 26); + _clearLogButton.TabIndex = 4; + _clearLogButton.Text = "Clear Log"; + _clearLogButton.Click += ClearLogButton_Click; + // + // _eventLog + // + _eventLog.FormattingEnabled = true; + _eventLog.HorizontalScrollbar = true; + _eventLog.Location = new Point(18, 170); + _eventLog.Name = "_eventLog"; + _eventLog.Size = new Size(554, 184); + _eventLog.TabIndex = 3; + // + // _contentPanel + // + _contentPanel.Dock = DockStyle.Fill; + _contentPanel.Controls.Add(_clearLogButton); + _contentPanel.Controls.Add(_eventLog); + _contentPanel.Controls.Add(_logLabel); + _contentPanel.Controls.Add(_statusLabel); + _contentPanel.Controls.Add(_toggleWrappableButton); + _contentPanel.Controls.Add(_toggleAppearanceButton); + _contentPanel.Controls.Add(_summaryLabel); + _contentPanel.ResumeLayout(false); + // + // ToolBarForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(592, 398); + Controls.Add(_contentPanel); + MinimumSize = new Size(608, 437); + Name = "ToolBarForm"; + StartPosition = FormStartPosition.CenterParent; + Text = "ToolBar Demo"; + ResumeLayout(false); + } +} \ No newline at end of file diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/ToolBarForm.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/ToolBarForm.cs new file mode 100644 index 00000000000..60bd52ac84f --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Demo/ToolBarForm.cs @@ -0,0 +1,134 @@ +using System.Windows.Forms; + +namespace Demo; + +public partial class ToolBarForm : Form +{ + private readonly ToolBar _demoToolBar; + + public ToolBarForm() + { + InitializeComponent(); + + _demoToolBar = CreateToolBar(); + Controls.Add(_demoToolBar); + + AppendLog("ToolBar demo ready."); + } + + private ToolBar CreateToolBar() + { + ToolBar toolBar = new() + { + TextAlign = ToolBarTextAlign.Right, + ShowToolTips = true, + DropDownArrows = true, + Wrappable = false + }; + + toolBar.Buttons.Add("1st button"); + ToolBarButton firstButton = toolBar.Buttons[0]; + firstButton.ToolTipText = "This is the first button"; + + ToolBarButton separatorButton = new("sep1") + { + Style = ToolBarButtonStyle.Separator + }; + toolBar.Buttons.Add(separatorButton); + + ToolBarButton toggleButton = new("btn2 toggle") + { + Style = ToolBarButtonStyle.ToggleButton, + ToolTipText = "This is the second button" + }; + toolBar.Buttons.Add(toggleButton); + + ToolBarButton dropDownButton = new("btn3 drop-down") + { + Style = ToolBarButtonStyle.DropDownButton, + ToolTipText = "This is the third button" + }; + + MenuItem waveMenuItem = new("Wave", WaveMenuItem_Click); + MenuItem statusMenuItem = new("Write Status", WriteStatusMenuItem_Click); + dropDownButton.DropDownMenu = new ContextMenu([waveMenuItem, statusMenuItem]); + toolBar.Buttons.Add(dropDownButton); + + toolBar.Buttons.Add(new ToolBarButton("sep2") { Style = ToolBarButtonStyle.Separator }); + + toolBar.Buttons.Add(new ToolBarButton("btn4 push") { ToolTipText = "Push button 4" }); + toolBar.Buttons.Add(new ToolBarButton("btn5 toggle") { Style = ToolBarButtonStyle.ToggleButton, ToolTipText = "Toggle button 5" }); + toolBar.Buttons.Add(new ToolBarButton("sep3") { Style = ToolBarButtonStyle.Separator }); + + ToolBarButton dropDownButton2 = new("btn6 drop-down") + { + Style = ToolBarButtonStyle.DropDownButton, + ToolTipText = "Drop-down button 6" + }; + dropDownButton2.DropDownMenu = new ContextMenu([new MenuItem("Action A", WaveMenuItem_Click), new MenuItem("Action B", WriteStatusMenuItem_Click)]); + toolBar.Buttons.Add(dropDownButton2); + + toolBar.Buttons.Add(new ToolBarButton("btn7 push") { ToolTipText = "Push button 7" }); + toolBar.Buttons.Add(new ToolBarButton("btn8 push") { ToolTipText = "Push button 8" }); + + toolBar.ButtonClick += DemoToolBar_ButtonClick; + + return toolBar; + } + + private void DemoToolBar_ButtonClick(object? sender, ToolBarButtonClickEventArgs e) + { + _statusLabel.Text = "Clicked: " + e.Button.Text; + AppendLog("Toolbar button clicked: " + e.Button.Text); + MessageBox.Show(this, "Button clicked. text = " + e.Button.Text, Text); + } + + private void WaveMenuItem_Click(object? sender, EventArgs e) + { + _statusLabel.Text = "Wave back"; + AppendLog("Toolbar drop-down menu clicked: Wave"); + MessageBox.Show(this, "Wave back", Text); + } + + private void WriteStatusMenuItem_Click(object? sender, EventArgs e) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss"); + _statusLabel.Text = "Status updated at " + timestamp; + AppendLog("Toolbar drop-down menu clicked: Write Status at " + timestamp); + } + + private void ToggleAppearanceButton_Click(object? sender, EventArgs e) + { + _demoToolBar.Appearance = _demoToolBar.Appearance == ToolBarAppearance.Normal + ? ToolBarAppearance.Flat + : ToolBarAppearance.Normal; + + _toggleAppearanceButton.Text = "Appearance: " + _demoToolBar.Appearance; + _statusLabel.Text = "ToolBar.Appearance → " + _demoToolBar.Appearance; + AppendLog("ToolBar.Appearance toggled to: " + _demoToolBar.Appearance); + } + + private void ToggleWrappableButton_Click(object? sender, EventArgs e) + { + _demoToolBar.Wrappable = !_demoToolBar.Wrappable; + _toggleWrappableButton.Text = "Wrappable: " + _demoToolBar.Wrappable; + _statusLabel.Text = "ToolBar.Wrappable → " + _demoToolBar.Wrappable; + AppendLog("ToolBar.Wrappable toggled to: " + _demoToolBar.Wrappable); + } + + private void ClearLogButton_Click(object? sender, EventArgs e) + { + _eventLog.Items.Clear(); + AppendLog("Log cleared."); + } + + private void AppendLog(string message) + { + _eventLog.Items.Insert(0, $"[{DateTime.Now:HH:mm:ss}] {message}"); + + if (_eventLog.Items.Count > 200) + { + _eventLog.Items.RemoveAt(_eventLog.Items.Count - 1); + } + } +} \ No newline at end of file diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/AdjustWindowRectTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/AdjustWindowRectTests.cs new file mode 100644 index 00000000000..c6702498507 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/AdjustWindowRectTests.cs @@ -0,0 +1,179 @@ +namespace System.Windows.Forms.Tests +{ + using System.Drawing; + + public class AdjustWindowRectTests + { + [StaFact] + public void Form_WindowRectCalculation_HandlesDpiScaling() + { + // Test that menu consideration works across different DPI scenarios + + // Arrange + using var form = new Form(); + var menu = new MainMenu(); + menu.MenuItems.Add(new MenuItem("Test")); + form.Menu = menu; + + var baseClientSize = new Size(400, 300); + + // Act + form.ClientSize = baseClientSize; + var windowSize = form.Size; + + // Change the client size again to ensure consistency + form.ClientSize = new Size(600, 450); + var newWindowSize = form.Size; + + // Assert + // The window size should scale proportionally with client size + var clientSizeChange = new Size(200, 150); // 600-400, 450-300 + var windowSizeChange = new Size(newWindowSize.Width - windowSize.Width, newWindowSize.Height - windowSize.Height); + + Assert.Equal(clientSizeChange, windowSizeChange); + } + + [StaFact] + public void Form_CreateParams_MenuConsideredInWindowRectCalculation() + { + // Test that CreateParams and window rect calculations are consistent + + // Arrange + using var formWithMenu = new Form(); + using var formWithoutMenu = new Form(); + + var menu = new MainMenu(); + menu.MenuItems.Add(new MenuItem("File")); + formWithMenu.Menu = menu; + + // Set same size and create handles to trigger CreateParams usage + var testSize = new Size(300, 200); + formWithMenu.Size = testSize; + formWithoutMenu.Size = testSize; + + // Force handle creation to trigger CreateParams + _ = formWithMenu.Handle; + _ = formWithoutMenu.Handle; + + // Act & Assert + // The forms should maintain their size relationships after handle creation + // which exercises the CreateParams path + formWithMenu.ClientSize = new Size(250, 150); + formWithoutMenu.ClientSize = new Size(250, 150); + + Assert.True(formWithMenu.Size.Height > formWithoutMenu.Size.Height, + "After handle creation, form with menu should still be taller"); + } + + [StaFact] + public void ToolStripTextBox_InFormWithMenu_IndependentSizing() + { + // Test that ToolStripTextBox in a form with menu isn't affected by the form's menu + + // Arrange + using var formWithMenu = new Form(); + using var formWithoutMenu = new Form(); + + var menu = new MainMenu(); + menu.MenuItems.Add(new MenuItem("File")); + formWithMenu.Menu = menu; + + using var toolStrip1 = new ToolStrip(); + using var toolStrip2 = new ToolStrip(); + using var textBox1 = new ToolStripTextBox(); + using var textBox2 = new ToolStripTextBox(); + + toolStrip1.Items.Add(textBox1); + toolStrip2.Items.Add(textBox2); + formWithMenu.Controls.Add(toolStrip1); + formWithoutMenu.Controls.Add(toolStrip2); + + // Act + textBox1.Size = new Size(120, 22); + textBox2.Size = new Size(120, 22); + + // Assert + Assert.Equal(textBox2.Size, textBox1.Size); + } + + [StaFact] + public void Control_SizeCalculations_ConsistentForDifferentControlTypes() + { + // Test various control types to ensure they handle menu considerations correctly + + // Arrange & Act & Assert + using var form1 = new Form(); // No menu + using var form2 = new Form(); // Menu with items + + var menuWithItems = new MainMenu(); + menuWithItems.MenuItems.Add(new MenuItem("File")); + form2.Menu = menuWithItems; + + var clientSize = new Size(400, 300); + + // Act + form1.ClientSize = clientSize; + form2.ClientSize = clientSize; + + Func[] controlTypes = + [ + () => new Button(), + () => new Label(), + () => new TextBox(), + () => new Panel(), + () => new GroupBox() + ]; + + foreach (var createControl in controlTypes) + { + using var control1 = createControl(); + using var control2 = createControl(); + + var testSize = new Size(100, 50); + control1.Size = testSize; + control2.Size = testSize; + + form1.Controls.Add(control1); + form2.Controls.Add(control2); + + Assert.Equal(control2.Size, control1.Size); + } + } + + [StaFact] + public void Form_MenuVisibility_AffectsWindowSizeCalculation() + { + // Test the specific logic: menu only affects size if it has items + + // Arrange + using var form1 = new Form(); // No menu + using var form2 = new Form(); // Empty menu + using var form3 = new Form(); // Menu with items + + var emptyMenu = new MainMenu(); + form2.Menu = emptyMenu; + + var menuWithItems = new MainMenu(); + menuWithItems.MenuItems.Add(new MenuItem("File")); + form3.Menu = menuWithItems; + + var clientSize = new Size(400, 300); + + // Act + form1.ClientSize = clientSize; + form2.ClientSize = clientSize; + form3.ClientSize = clientSize; + + // Assert + // Forms 1 and 2 should have same height (no menu or empty menu) + Assert.Equal(form2.Size.Height, form1.Size.Height); + + // Form 3 should be taller (has menu items) + Assert.True(form3.Size.Height > form1.Size.Height, + "Form with menu items should be taller than form without menu"); + + Assert.True(form3.Size.Height > form2.Size.Height, + "Form with menu items should be taller than form with empty menu"); + } + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/DataGridCellTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/DataGridCellTests.cs new file mode 100644 index 00000000000..c2c78729603 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/DataGridCellTests.cs @@ -0,0 +1,142 @@ +namespace System.Windows.Forms.Tests; + +public class DataGridCellTests +{ + [StaFact] + public void DataGridCell_Ctor_Default() + { + DataGridCell cell = default; + + Assert.Equal(0, cell.RowNumber); + Assert.Equal(0, cell.ColumnNumber); + } + + [StaFact] + public void DataGridCell_Ctor_Int_Int_Zero() + { + DataGridCell cell = new(0, 0); + + Assert.Equal(0, cell.RowNumber); + Assert.Equal(0, cell.ColumnNumber); + } + + [StaFact] + public void DataGridCell_Ctor_Int_Int_Positive() + { + DataGridCell cell = new(1, 2); + + Assert.Equal(1, cell.RowNumber); + Assert.Equal(2, cell.ColumnNumber); + } + + [StaFact] + public void DataGridCell_Ctor_Int_Int_Negative() + { + DataGridCell cell = new(-1, -2); + + Assert.Equal(-1, cell.RowNumber); + Assert.Equal(-2, cell.ColumnNumber); + } + + [StaFact] + public void DataGridCell_RowNumber_Set_GetReturnsExpected() + { + DataGridCell cell = new() + { + RowNumber = 5 + }; + + Assert.Equal(5, cell.RowNumber); + + // Set same. + cell.RowNumber = 5; + + Assert.Equal(5, cell.RowNumber); + + // Set different. + cell.RowNumber = 10; + + Assert.Equal(10, cell.RowNumber); + } + + [StaFact] + public void DataGridCell_ColumnNumber_Set_GetReturnsExpected() + { + DataGridCell cell = new() + { + ColumnNumber = 3 + }; + + Assert.Equal(3, cell.ColumnNumber); + + // Set same. + cell.ColumnNumber = 3; + + Assert.Equal(3, cell.ColumnNumber); + + // Set different. + cell.ColumnNumber = 7; + + Assert.Equal(7, cell.ColumnNumber); + } + + [StaFact] + public void DataGridCell_Equals_SameRowAndColumn_ReturnsTrue() + { + DataGridCell cell1 = new(1, 2); + DataGridCell cell2 = new(1, 2); + + Assert.True(cell1.Equals(cell2)); + Assert.Equal(cell1.GetHashCode(), cell2.GetHashCode()); + } + + [StaFact] + public void DataGridCell_Equals_DifferentRow_ReturnsFalse() + { + DataGridCell cell1 = new(1, 2); + DataGridCell cell2 = new(2, 2); + + Assert.False(cell1.Equals(cell2)); + } + + [StaFact] + public void DataGridCell_Equals_DifferentColumn_ReturnsFalse() + { + DataGridCell cell1 = new(1, 2); + DataGridCell cell2 = new(1, 3); + + Assert.False(cell1.Equals(cell2)); + } + + [StaFact] + public void DataGridCell_Equals_NonDataGridCellObject_ReturnsFalse() + { + DataGridCell cell = new(1, 2); + + Assert.False(cell.Equals(new object())); + } + + [StaFact] + public void DataGridCell_Equals_Null_ReturnsFalse() + { + DataGridCell cell = new(1, 2); + + Assert.False(cell.Equals(null)); + } + + [StaFact] + public void DataGridCell_ToString_ReturnsExpected() + { + DataGridCell cell = new(1, 2); + + Assert.Equal("DataGridCell {RowNumber = 1, ColumnNumber = 2}", cell.ToString()); + } + + [StaFact] + public void DataGridCell_ToString_DefaultValues_ReturnsExpected() + { + DataGridCell cell = default; + + Assert.Equal("DataGridCell {RowNumber = 0, ColumnNumber = 0}", cell.ToString()); + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/DataGridTableStyleTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/DataGridTableStyleTests.cs new file mode 100644 index 00000000000..5756c9f88b1 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/DataGridTableStyleTests.cs @@ -0,0 +1,373 @@ +using System.Drawing; + +namespace System.Windows.Forms.Tests; + +public class DataGridTableStyleTests +{ + [StaFact] + public void DataGridTableStyle_Ctor_Default() + { + using DataGridTableStyle style = new(); + + Assert.True(style.AllowSorting); + Assert.Equal(SystemColors.Window, style.AlternatingBackColor); + Assert.Equal(SystemColors.Window, style.BackColor); + Assert.True(style.ColumnHeadersVisible); + Assert.Null(style.DataGrid); + Assert.Equal(SystemColors.WindowText, style.ForeColor); + Assert.Empty(style.GridColumnStyles); + Assert.Same(style.GridColumnStyles, style.GridColumnStyles); + Assert.Equal(SystemColors.Control, style.GridLineColor); + Assert.Equal(DataGridLineStyle.Solid, style.GridLineStyle); + Assert.Equal(SystemColors.Control, style.HeaderBackColor); + Assert.Same(Control.DefaultFont, style.HeaderFont); + Assert.Equal(SystemColors.ControlText, style.HeaderForeColor); + Assert.Equal(SystemColors.HotTrack, style.LinkColor); + Assert.Equal(SystemColors.HotTrack, style.LinkHoverColor); + Assert.Empty(style.MappingName); + Assert.Equal(75, style.PreferredColumnWidth); + Assert.Equal(Control.DefaultFont.Height + 3, style.PreferredRowHeight); + Assert.False(style.ReadOnly); + Assert.True(style.RowHeadersVisible); + Assert.Equal(35, style.RowHeaderWidth); + Assert.Equal(SystemColors.ActiveCaption, style.SelectionBackColor); + Assert.Equal(SystemColors.ActiveCaptionText, style.SelectionForeColor); + Assert.Null(style.Site); + } + + [StaFact] + public void DataGridTableStyle_AllowSorting_Set_GetReturnsExpected() + { + using DataGridTableStyle style = new(); + + style.AllowSorting = false; + + Assert.False(style.AllowSorting); + + // Set same. + style.AllowSorting = false; + + Assert.False(style.AllowSorting); + + // Set different. + style.AllowSorting = true; + + Assert.True(style.AllowSorting); + } + + [StaFact] + public void DataGridTableStyle_AllowSorting_SetWithHandler_CallsAllowSortingChanged() + { + using DataGridTableStyle style = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(style, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + style.AllowSortingChanged += handler; + + // Set different. + style.AllowSorting = false; + + Assert.False(style.AllowSorting); + Assert.Equal(1, callCount); + + // Set same. + style.AllowSorting = false; + + Assert.False(style.AllowSorting); + Assert.Equal(1, callCount); + + // Set different. + style.AllowSorting = true; + + Assert.True(style.AllowSorting); + Assert.Equal(2, callCount); + + // Remove handler. + style.AllowSortingChanged -= handler; + style.AllowSorting = false; + + Assert.Equal(2, callCount); + } + + [StaFact] + public void DataGridTableStyle_AllowSorting_SetDefaultTableStyle_ThrowsArgumentException() + { + using DataGridTableStyle style = new(isDefaultTableStyle: true); + + Assert.Throws(() => style.AllowSorting = false); + Assert.True(style.AllowSorting); + } + + [StaFact] + public void DataGridTableStyle_AlternatingBackColor_Set_GetReturnsExpected() + { + using DataGridTableStyle style = new(); + + style.AlternatingBackColor = Color.Red; + + Assert.Equal(Color.Red, style.AlternatingBackColor); + + // Set same. + style.AlternatingBackColor = Color.Red; + + Assert.Equal(Color.Red, style.AlternatingBackColor); + + // Set different. + style.AlternatingBackColor = Color.Blue; + + Assert.Equal(Color.Blue, style.AlternatingBackColor); + } + + [StaFact] + public void DataGridTableStyle_AlternatingBackColor_SetWithHandler_CallsAlternatingBackColorChanged() + { + using DataGridTableStyle style = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(style, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + style.AlternatingBackColorChanged += handler; + + // Set different. + style.AlternatingBackColor = Color.Red; + + Assert.Equal(Color.Red, style.AlternatingBackColor); + Assert.Equal(1, callCount); + + // Set same. + style.AlternatingBackColor = Color.Red; + + Assert.Equal(Color.Red, style.AlternatingBackColor); + Assert.Equal(1, callCount); + + // Set different. + style.AlternatingBackColor = Color.Blue; + + Assert.Equal(Color.Blue, style.AlternatingBackColor); + Assert.Equal(2, callCount); + + // Remove handler. + style.AlternatingBackColorChanged -= handler; + style.AlternatingBackColor = Color.Red; + + Assert.Equal(2, callCount); + } + + [StaFact] + public void DataGridTableStyle_AlternatingBackColor_SetEmpty_ThrowsArgumentException() + { + using DataGridTableStyle style = new(); + + Assert.Throws(() => style.AlternatingBackColor = Color.Empty); + } + + [StaFact] + public void DataGridTableStyle_AlternatingBackColor_SetDefaultTableStyle_ThrowsArgumentException() + { + using DataGridTableStyle style = new(isDefaultTableStyle: true); + + Assert.Throws(() => style.AlternatingBackColor = Color.Red); + Assert.Equal(SystemColors.Window, style.AlternatingBackColor); + } + + [StaFact] + public void DataGridTableStyle_BackColor_SetWithHandler_CallsBackColorChanged() + { + using DataGridTableStyle style = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(style, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + style.BackColorChanged += handler; + + // Set different. + style.BackColor = Color.Red; + + Assert.Equal(Color.Red, style.BackColor); + Assert.Equal(1, callCount); + + // Set same. + style.BackColor = Color.Red; + + Assert.Equal(Color.Red, style.BackColor); + Assert.Equal(1, callCount); + + // Set different. + style.BackColor = Color.Blue; + + Assert.Equal(Color.Blue, style.BackColor); + Assert.Equal(2, callCount); + + // Remove handler. + style.BackColorChanged -= handler; + style.BackColor = Color.Red; + + Assert.Equal(2, callCount); + } + + [StaFact] + public void DataGridTableStyle_BackColor_SetEmpty_ThrowsArgumentException() + { + using DataGridTableStyle style = new(); + + Assert.Throws(() => style.BackColor = Color.Empty); + } + + [StaFact] + public void DataGridTableStyle_MappingName_Set_GetReturnsExpected() + { + using DataGridTableStyle style = new(); + + style.MappingName = "Customers"; + + Assert.Equal("Customers", style.MappingName); + + // Set same. + style.MappingName = "Customers"; + + Assert.Equal("Customers", style.MappingName); + + // Set different. + style.MappingName = "Orders"; + + Assert.Equal("Orders", style.MappingName); + + // Set empty. + style.MappingName = string.Empty; + + Assert.Empty(style.MappingName); + } + + [StaFact] + public void DataGridTableStyle_PreferredColumnWidth_Set_GetReturnsExpected() + { + using DataGridTableStyle style = new(); + + style.PreferredColumnWidth = 200; + + Assert.Equal(200, style.PreferredColumnWidth); + + // Set same. + style.PreferredColumnWidth = 200; + + Assert.Equal(200, style.PreferredColumnWidth); + + // Restore default. + style.PreferredColumnWidth = 75; + + Assert.Equal(75, style.PreferredColumnWidth); + } + + [StaFact] + public void DataGridTableStyle_ReadOnly_Set_GetReturnsExpected() + { + using DataGridTableStyle style = new(); + + style.ReadOnly = true; + + Assert.True(style.ReadOnly); + + // Set same. + style.ReadOnly = true; + + Assert.True(style.ReadOnly); + + // Set different. + style.ReadOnly = false; + + Assert.False(style.ReadOnly); + } + + [StaFact] + public void DataGridTableStyle_ReadOnly_SetWithHandler_CallsReadOnlyChanged() + { + using DataGridTableStyle style = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(style, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + style.ReadOnlyChanged += handler; + + // Set different. + style.ReadOnly = true; + + Assert.True(style.ReadOnly); + Assert.Equal(1, callCount); + + // Set same. + style.ReadOnly = true; + + Assert.True(style.ReadOnly); + Assert.Equal(1, callCount); + + // Set different. + style.ReadOnly = false; + + Assert.False(style.ReadOnly); + Assert.Equal(2, callCount); + + // Remove handler. + style.ReadOnlyChanged -= handler; + style.ReadOnly = true; + + Assert.Equal(2, callCount); + } + + [StaFact] + public void DataGridTableStyle_GridLineStyle_Set_GetReturnsExpected() + { + using DataGridTableStyle style = new(); + + style.GridLineStyle = DataGridLineStyle.None; + + Assert.Equal(DataGridLineStyle.None, style.GridLineStyle); + + // Set same. + style.GridLineStyle = DataGridLineStyle.None; + + Assert.Equal(DataGridLineStyle.None, style.GridLineStyle); + + // Set different. + style.GridLineStyle = DataGridLineStyle.Solid; + + Assert.Equal(DataGridLineStyle.Solid, style.GridLineStyle); + } + + [StaFact] + public void DataGridTableStyle_GridColumnStyles_AddAndRemove_UpdatesCollection() + { + using DataGridTableStyle style = new() + { + MappingName = "Items" + }; + + DataGridTextBoxColumn column = new() + { + MappingName = "Name", + HeaderText = "Item Name", + Width = 150 + }; + + style.GridColumnStyles.Add(column); + + Assert.Single(style.GridColumnStyles); + Assert.Same(column, style.GridColumnStyles["Name"]); + + style.GridColumnStyles.Remove(column); + + Assert.Empty(style.GridColumnStyles); + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/DataGridTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/DataGridTests.cs new file mode 100644 index 00000000000..bb1e1c11008 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/DataGridTests.cs @@ -0,0 +1,800 @@ +using System.ComponentModel; +using System.Data; +using System.Drawing; + +namespace System.Windows.Forms.Tests; + +public class DataGridTests +{ + [StaFact] + public void DataGrid_Ctor_Default() + { + using SubDataGrid dataGrid = new(); + + Assert.True(dataGrid.AllowNavigation); + Assert.True(dataGrid.AllowSorting); + Assert.Equal(SystemColors.Window, dataGrid.AlternatingBackColor); + Assert.Equal(SystemColors.Window, dataGrid.BackColor); + Assert.Equal(SystemColors.AppWorkspace, dataGrid.BackgroundColor); + Assert.Equal(BorderStyle.Fixed3D, dataGrid.BorderStyle); + Assert.Equal(SystemColors.ActiveCaption, dataGrid.CaptionBackColor); + Assert.Equal(SystemColors.ActiveCaptionText, dataGrid.CaptionForeColor); + Assert.Empty(dataGrid.CaptionText); + Assert.True(dataGrid.CaptionVisible); + Assert.True(dataGrid.ColumnHeadersVisible); + Assert.Equal(0, dataGrid.CurrentCell.RowNumber); + Assert.Equal(0, dataGrid.CurrentCell.ColumnNumber); + Assert.Equal(-1, dataGrid.CurrentRowIndex); + Assert.Empty(dataGrid.DataMember); + Assert.Null(dataGrid.DataSource); + Assert.Equal(0, dataGrid.FirstVisibleColumn); + Assert.False(dataGrid.FlatMode); + Assert.Equal(SystemColors.WindowText, dataGrid.ForeColor); + Assert.Equal(SystemColors.Control, dataGrid.GridLineColor); + Assert.Equal(DataGridLineStyle.Solid, dataGrid.GridLineStyle); + Assert.Equal(SystemColors.Control, dataGrid.HeaderBackColor); + Assert.Equal(SystemColors.ControlText, dataGrid.HeaderForeColor); + Assert.NotNull(dataGrid.HorizScrollBar); + Assert.Same(dataGrid.HorizScrollBar, dataGrid.HorizScrollBar); + Assert.Equal(SystemColors.HotTrack, dataGrid.LinkColor); + Assert.Equal(SystemColors.HotTrack, dataGrid.LinkHoverColor); + Assert.Null(dataGrid.ListManager); + Assert.Equal(SystemColors.Control, dataGrid.ParentRowsBackColor); + Assert.Equal(SystemColors.WindowText, dataGrid.ParentRowsForeColor); + Assert.Equal(DataGridParentRowsLabelStyle.Both, dataGrid.ParentRowsLabelStyle); + Assert.True(dataGrid.ParentRowsVisible); + Assert.Equal(75, dataGrid.PreferredColumnWidth); + Assert.Equal(Control.DefaultFont.Height + 3, dataGrid.PreferredRowHeight); + Assert.False(dataGrid.ReadOnly); + Assert.True(dataGrid.RowHeadersVisible); + Assert.Equal(35, dataGrid.RowHeaderWidth); + Assert.Equal(SystemColors.ActiveCaption, dataGrid.SelectionBackColor); + Assert.Equal(SystemColors.ActiveCaptionText, dataGrid.SelectionForeColor); + Assert.Equal(new Size(130, 80), dataGrid.Size); + Assert.Empty(dataGrid.TableStyles); + Assert.Same(dataGrid.TableStyles, dataGrid.TableStyles); + Assert.NotNull(dataGrid.VertScrollBar); + Assert.Same(dataGrid.VertScrollBar, dataGrid.VertScrollBar); + Assert.Equal(0, dataGrid.VisibleColumnCount); + Assert.Equal(0, dataGrid.VisibleRowCount); + } + + [StaFact] + public void DataGrid_AllowNavigation_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.AllowNavigation = false; + + Assert.False(dataGrid.AllowNavigation); + + // Set same. + dataGrid.AllowNavigation = false; + + Assert.False(dataGrid.AllowNavigation); + + // Set different. + dataGrid.AllowNavigation = true; + + Assert.True(dataGrid.AllowNavigation); + } + + [StaFact] + public void DataGrid_AllowNavigation_SetWithHandler_CallsAllowNavigationChanged() + { + using DataGrid dataGrid = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(dataGrid, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + dataGrid.AllowNavigationChanged += handler; + + // Set different. + dataGrid.AllowNavigation = false; + + Assert.False(dataGrid.AllowNavigation); + Assert.Equal(1, callCount); + + // Set same. + dataGrid.AllowNavigation = false; + + Assert.False(dataGrid.AllowNavigation); + Assert.Equal(1, callCount); + + // Set different. + dataGrid.AllowNavigation = true; + + Assert.True(dataGrid.AllowNavigation); + Assert.Equal(2, callCount); + + // Remove handler. + dataGrid.AllowNavigationChanged -= handler; + dataGrid.AllowNavigation = false; + + Assert.Equal(2, callCount); + } + + [StaFact] + public void DataGrid_AllowSorting_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.AllowSorting = false; + + Assert.False(dataGrid.AllowSorting); + + // Set same. + dataGrid.AllowSorting = false; + + Assert.False(dataGrid.AllowSorting); + + // Set different. + dataGrid.AllowSorting = true; + + Assert.True(dataGrid.AllowSorting); + } + + [StaFact] + public void DataGrid_AlternatingBackColor_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.AlternatingBackColor = Color.Red; + + Assert.Equal(Color.Red, dataGrid.AlternatingBackColor); + + // Set same. + dataGrid.AlternatingBackColor = Color.Red; + + Assert.Equal(Color.Red, dataGrid.AlternatingBackColor); + + // Set different. + dataGrid.AlternatingBackColor = Color.Blue; + + Assert.Equal(Color.Blue, dataGrid.AlternatingBackColor); + } + + [StaFact] + public void DataGrid_AlternatingBackColor_SetEmpty_ThrowsArgumentException() + { + using DataGrid dataGrid = new(); + + Assert.Throws(() => dataGrid.AlternatingBackColor = Color.Empty); + } + + [StaFact] + public void DataGrid_BorderStyle_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.BorderStyle = BorderStyle.FixedSingle; + + Assert.Equal(BorderStyle.FixedSingle, dataGrid.BorderStyle); + + // Set same. + dataGrid.BorderStyle = BorderStyle.FixedSingle; + + Assert.Equal(BorderStyle.FixedSingle, dataGrid.BorderStyle); + + // Set different. + dataGrid.BorderStyle = BorderStyle.None; + + Assert.Equal(BorderStyle.None, dataGrid.BorderStyle); + } + + [StaFact] + public void DataGrid_BorderStyle_SetWithHandler_CallsBorderStyleChanged() + { + using DataGrid dataGrid = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(dataGrid, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + dataGrid.BorderStyleChanged += handler; + + // Set different. + dataGrid.BorderStyle = BorderStyle.FixedSingle; + + Assert.Equal(BorderStyle.FixedSingle, dataGrid.BorderStyle); + Assert.Equal(1, callCount); + + // Set same. + dataGrid.BorderStyle = BorderStyle.FixedSingle; + + Assert.Equal(BorderStyle.FixedSingle, dataGrid.BorderStyle); + Assert.Equal(1, callCount); + + // Set different. + dataGrid.BorderStyle = BorderStyle.None; + + Assert.Equal(BorderStyle.None, dataGrid.BorderStyle); + Assert.Equal(2, callCount); + + // Remove handler. + dataGrid.BorderStyleChanged -= handler; + dataGrid.BorderStyle = BorderStyle.Fixed3D; + + Assert.Equal(2, callCount); + } + + [StaFact] + public void DataGrid_CaptionText_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.CaptionText = "My Grid"; + + Assert.Equal("My Grid", dataGrid.CaptionText); + + // Set same. + dataGrid.CaptionText = "My Grid"; + + Assert.Equal("My Grid", dataGrid.CaptionText); + + // Set different. + dataGrid.CaptionText = string.Empty; + + Assert.Empty(dataGrid.CaptionText); + } + + [StaFact] + public void DataGrid_CaptionVisible_SetWithHandler_CallsCaptionVisibleChanged() + { + using DataGrid dataGrid = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(dataGrid, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + dataGrid.CaptionVisibleChanged += handler; + + // Set different. + dataGrid.CaptionVisible = false; + + Assert.False(dataGrid.CaptionVisible); + Assert.Equal(1, callCount); + + // Set same. + dataGrid.CaptionVisible = false; + + Assert.False(dataGrid.CaptionVisible); + Assert.Equal(1, callCount); + + // Set different. + dataGrid.CaptionVisible = true; + + Assert.True(dataGrid.CaptionVisible); + Assert.Equal(2, callCount); + + // Remove handler. + dataGrid.CaptionVisibleChanged -= handler; + dataGrid.CaptionVisible = false; + + Assert.Equal(2, callCount); + } + + [StaFact] + public void DataGrid_FlatMode_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.FlatMode = true; + + Assert.True(dataGrid.FlatMode); + + // Set same. + dataGrid.FlatMode = true; + + Assert.True(dataGrid.FlatMode); + + // Set different. + dataGrid.FlatMode = false; + + Assert.False(dataGrid.FlatMode); + } + + [StaFact] + public void DataGrid_FlatMode_SetWithHandler_CallsFlatModeChanged() + { + using DataGrid dataGrid = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(dataGrid, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + dataGrid.FlatModeChanged += handler; + + // Set different. + dataGrid.FlatMode = true; + + Assert.True(dataGrid.FlatMode); + Assert.Equal(1, callCount); + + // Set same. + dataGrid.FlatMode = true; + + Assert.True(dataGrid.FlatMode); + Assert.Equal(1, callCount); + + // Set different. + dataGrid.FlatMode = false; + + Assert.False(dataGrid.FlatMode); + Assert.Equal(2, callCount); + + // Remove handler. + dataGrid.FlatModeChanged -= handler; + dataGrid.FlatMode = true; + + Assert.Equal(2, callCount); + } + + [StaFact] + public void DataGrid_GridLineStyle_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.GridLineStyle = DataGridLineStyle.None; + + Assert.Equal(DataGridLineStyle.None, dataGrid.GridLineStyle); + + // Set same. + dataGrid.GridLineStyle = DataGridLineStyle.None; + + Assert.Equal(DataGridLineStyle.None, dataGrid.GridLineStyle); + + // Set different. + dataGrid.GridLineStyle = DataGridLineStyle.Solid; + + Assert.Equal(DataGridLineStyle.Solid, dataGrid.GridLineStyle); + } + + [StaFact] + public void DataGrid_ParentRowsLabelStyle_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.ParentRowsLabelStyle = DataGridParentRowsLabelStyle.None; + + Assert.Equal(DataGridParentRowsLabelStyle.None, dataGrid.ParentRowsLabelStyle); + + dataGrid.ParentRowsLabelStyle = DataGridParentRowsLabelStyle.TableName; + + Assert.Equal(DataGridParentRowsLabelStyle.TableName, dataGrid.ParentRowsLabelStyle); + + dataGrid.ParentRowsLabelStyle = DataGridParentRowsLabelStyle.ColumnName; + + Assert.Equal(DataGridParentRowsLabelStyle.ColumnName, dataGrid.ParentRowsLabelStyle); + + // Set same. + dataGrid.ParentRowsLabelStyle = DataGridParentRowsLabelStyle.ColumnName; + + Assert.Equal(DataGridParentRowsLabelStyle.ColumnName, dataGrid.ParentRowsLabelStyle); + + // Restore default. + dataGrid.ParentRowsLabelStyle = DataGridParentRowsLabelStyle.Both; + + Assert.Equal(DataGridParentRowsLabelStyle.Both, dataGrid.ParentRowsLabelStyle); + } + + [StaFact] + public void DataGrid_ParentRowsVisible_SetWithHandler_CallsParentRowsVisibleChanged() + { + using DataGrid dataGrid = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(dataGrid, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + dataGrid.ParentRowsVisibleChanged += handler; + + // Set different. + dataGrid.ParentRowsVisible = false; + + Assert.False(dataGrid.ParentRowsVisible); + Assert.Equal(1, callCount); + + // Set same. + dataGrid.ParentRowsVisible = false; + + Assert.False(dataGrid.ParentRowsVisible); + Assert.Equal(1, callCount); + + // Set different. + dataGrid.ParentRowsVisible = true; + + Assert.True(dataGrid.ParentRowsVisible); + Assert.Equal(2, callCount); + + // Remove handler. + dataGrid.ParentRowsVisibleChanged -= handler; + dataGrid.ParentRowsVisible = false; + + Assert.Equal(2, callCount); + } + + [StaFact] + public void DataGrid_ReadOnly_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.ReadOnly = true; + + Assert.True(dataGrid.ReadOnly); + + // Set same. + dataGrid.ReadOnly = true; + + Assert.True(dataGrid.ReadOnly); + + // Set different. + dataGrid.ReadOnly = false; + + Assert.False(dataGrid.ReadOnly); + } + + [StaFact] + public void DataGrid_ReadOnly_SetWithHandler_CallsReadOnlyChanged() + { + using DataGrid dataGrid = new(); + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(dataGrid, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + dataGrid.ReadOnlyChanged += handler; + + // Set different. + dataGrid.ReadOnly = true; + + Assert.True(dataGrid.ReadOnly); + Assert.Equal(1, callCount); + + // Set same. + dataGrid.ReadOnly = true; + + Assert.True(dataGrid.ReadOnly); + Assert.Equal(1, callCount); + + // Set different. + dataGrid.ReadOnly = false; + + Assert.False(dataGrid.ReadOnly); + Assert.Equal(2, callCount); + + // Remove handler. + dataGrid.ReadOnlyChanged -= handler; + dataGrid.ReadOnly = true; + + Assert.Equal(2, callCount); + } + + [StaFact] + public void DataGrid_SelectionBackColor_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.SelectionBackColor = Color.Navy; + + Assert.Equal(Color.Navy, dataGrid.SelectionBackColor); + + // Set same. + dataGrid.SelectionBackColor = Color.Navy; + + Assert.Equal(Color.Navy, dataGrid.SelectionBackColor); + + // Set different. + dataGrid.SelectionBackColor = Color.Red; + + Assert.Equal(Color.Red, dataGrid.SelectionBackColor); + } + + [StaFact] + public void DataGrid_SelectionBackColor_SetEmpty_ThrowsArgumentException() + { + using DataGrid dataGrid = new(); + + Assert.Throws(() => dataGrid.SelectionBackColor = Color.Empty); + } + + [StaFact] + public void DataGrid_SelectionForeColor_Set_GetReturnsExpected() + { + using DataGrid dataGrid = new(); + + dataGrid.SelectionForeColor = Color.White; + + Assert.Equal(Color.White, dataGrid.SelectionForeColor); + + // Set same. + dataGrid.SelectionForeColor = Color.White; + + Assert.Equal(Color.White, dataGrid.SelectionForeColor); + + // Set different. + dataGrid.SelectionForeColor = Color.Black; + + Assert.Equal(Color.Black, dataGrid.SelectionForeColor); + } + + [StaFact] + public void DataGrid_SelectionForeColor_SetEmpty_ThrowsArgumentException() + { + using DataGrid dataGrid = new(); + + Assert.Throws(() => dataGrid.SelectionForeColor = Color.Empty); + } + + [StaFact] + public void DataGrid_SetDataBinding_SetsDataSourceAndDataMember() + { + using Form form = new(); + using DataGrid dataGrid = new(); + using DataSet dataSet = CreateDemoDataSet(); + + form.Controls.Add(dataGrid); + dataGrid.SetDataBinding(dataSet, "Customers"); + + Assert.Same(dataSet, dataGrid.DataSource); + Assert.Equal("Customers", dataGrid.DataMember); + } + + [StaFact] + public void DataGrid_SetDataBinding_CurrentRowIndexStaysInSyncWithBindingManager() + { + using Form form = new(); + using DataGrid dataGrid = new(); + using DataSet dataSet = CreateDemoDataSet(); + + form.Controls.Add(dataGrid); + dataGrid.SetDataBinding(dataSet, "Customers"); + + CurrencyManager customersManager = Assert.IsAssignableFrom(form.BindingContext[dataSet, "Customers"]); + + Assert.Equal(0, dataGrid.CurrentRowIndex); + + customersManager.Position = 2; + + Assert.Equal(2, dataGrid.CurrentRowIndex); + + DataRowView customerAtPositionTwo = Assert.IsType(customersManager.Current); + Assert.Equal("Customer3", customerAtPositionTwo["CustName"]); + + dataGrid.CurrentRowIndex = 1; + + Assert.Equal(1, customersManager.Position); + + DataRowView currentCustomer = Assert.IsType(customersManager.Current); + Assert.Equal("Customer2", currentCustomer["CustName"]); + Assert.Equal(5, currentCustomer.Row.GetChildRows("custToOrders").Length); + } + + [StaFact] + public void DataGrid_SetDataBinding_EmptyTable_CurrentRowIndexIsMinusOne() + { + using Form form = new(); + using DataGrid dataGrid = new(); + using DataSet dataSet = new("EmptySet"); + + DataTable emptyTable = new("Items"); + emptyTable.Columns.Add("ID", typeof(int)); + emptyTable.Columns.Add("Name", typeof(string)); + dataSet.Tables.Add(emptyTable); + + form.Controls.Add(dataGrid); + dataGrid.SetDataBinding(dataSet, "Items"); + + CurrencyManager manager = Assert.IsAssignableFrom(form.BindingContext[dataSet, "Items"]); + + Assert.Equal(0, manager.Count); + Assert.Equal(-1, dataGrid.CurrentRowIndex); + } + + [StaFact] + public void DataGrid_DataSourceChanged_SetWithHandler_CallsDataSourceChanged() + { + using Form form = new(); + using DataGrid dataGrid = new(); + using DataSet dataSet = CreateDemoDataSet(); + + form.Controls.Add(dataGrid); + + int callCount = 0; + EventHandler handler = (sender, e) => + { + Assert.Same(dataGrid, sender); + Assert.Same(EventArgs.Empty, e); + callCount++; + }; + dataGrid.DataSourceChanged += handler; + + dataGrid.SetDataBinding(dataSet, "Customers"); + + Assert.Equal(1, callCount); + + // Set same source. + dataGrid.SetDataBinding(dataSet, "Customers"); + + Assert.Equal(1, callCount); + + // Remove handler. + dataGrid.DataSourceChanged -= handler; + dataGrid.SetDataBinding(dataSet, "Orders"); + + Assert.Equal(1, callCount); + } + + [StaFact] + public void DataGrid_TableStyles_AddDemoStyles_MapsCustomersAndOrdersColumns() + { + using Form form = new(); + using DataGrid dataGrid = new(); + using DataSet dataSet = CreateDemoDataSet(); + + form.Controls.Add(dataGrid); + dataGrid.SetDataBinding(dataSet, "Customers"); + + AddDemoTableStyles(form, dataGrid, dataSet); + + Assert.Equal(2, dataGrid.TableStyles.Count); + + DataGridTableStyle customersStyle = Assert.IsType(dataGrid.TableStyles["Customers"]); + DataGridTableStyle ordersStyle = Assert.IsType(dataGrid.TableStyles["Orders"]); + + Assert.Equal(Color.LightGray, customersStyle.AlternatingBackColor); + Assert.Equal(Color.LightBlue, ordersStyle.AlternatingBackColor); + + DataGridBoolColumn boolColumn = Assert.IsType(customersStyle.GridColumnStyles["Current"]); + Assert.Equal("IsCurrent Customer", boolColumn.HeaderText); + Assert.Equal(150, boolColumn.Width); + + DataGridTextBoxColumn customerNameColumn = Assert.IsType(customersStyle.GridColumnStyles["custName"]); + Assert.Equal("Customer Name", customerNameColumn.HeaderText); + Assert.Equal(250, customerNameColumn.Width); + + DataGridTextBoxColumn orderAmountColumn = Assert.IsType(ordersStyle.GridColumnStyles["OrderAmount"]); + Assert.Equal("c", orderAmountColumn.Format); + Assert.Equal("Total", orderAmountColumn.HeaderText); + Assert.Equal(100, orderAmountColumn.Width); + } + + [StaFact] + public void DataGrid_TableStyles_Clear_RemovesAllStyles() + { + using Form form = new(); + using DataGrid dataGrid = new(); + using DataSet dataSet = CreateDemoDataSet(); + + form.Controls.Add(dataGrid); + dataGrid.SetDataBinding(dataSet, "Customers"); + + AddDemoTableStyles(form, dataGrid, dataSet); + + Assert.Equal(2, dataGrid.TableStyles.Count); + + dataGrid.TableStyles.Clear(); + + Assert.Empty(dataGrid.TableStyles); + } + + private sealed class SubDataGrid : DataGrid + { + public new ScrollBar HorizScrollBar => base.HorizScrollBar; + + public new ScrollBar VertScrollBar => base.VertScrollBar; + } + + private static DataSet CreateDemoDataSet() + { + DataSet dataSet = new("myDataSet"); + + DataTable customersTable = new("Customers"); + DataTable ordersTable = new("Orders"); + + DataColumn customerIdColumn = new("CustID", typeof(int)); + DataColumn customerNameColumn = new("CustName", typeof(string)); + DataColumn currentColumn = new("Current", typeof(bool)); + customersTable.Columns.Add(customerIdColumn); + customersTable.Columns.Add(customerNameColumn); + customersTable.Columns.Add(currentColumn); + + DataColumn orderCustomerIdColumn = new("CustID", typeof(int)); + DataColumn orderDateColumn = new("orderDate", typeof(DateTime)); + DataColumn orderAmountColumn = new("OrderAmount", typeof(decimal)); + ordersTable.Columns.Add(orderAmountColumn); + ordersTable.Columns.Add(orderCustomerIdColumn); + ordersTable.Columns.Add(orderDateColumn); + + dataSet.Tables.Add(customersTable); + dataSet.Tables.Add(ordersTable); + dataSet.Relations.Add(new DataRelation("custToOrders", customerIdColumn, orderCustomerIdColumn)); + + for (int i = 1; i < 4; i++) + { + DataRow customerRow = customersTable.NewRow(); + customerRow["CustID"] = i; + customerRow["CustName"] = $"Customer{i}"; + customerRow["Current"] = i < 3; + customersTable.Rows.Add(customerRow); + + for (int j = 1; j < 6; j++) + { + DataRow orderRow = ordersTable.NewRow(); + orderRow["CustID"] = i; + orderRow["orderDate"] = new DateTime(2001, i, j * 2); + orderRow["OrderAmount"] = (decimal)(i * 10) + ((decimal)j / 10); + ordersTable.Rows.Add(orderRow); + } + } + + return dataSet; + } + + private static void AddDemoTableStyles(Form form, DataGrid dataGrid, DataSet dataSet) + { + DataGridTableStyle customersStyle = new() + { + MappingName = "Customers", + AlternatingBackColor = Color.LightGray + }; + + DataGridColumnStyle currentColumn = new DataGridBoolColumn + { + MappingName = "Current", + HeaderText = "IsCurrent Customer", + Width = 150 + }; + customersStyle.GridColumnStyles.Add(currentColumn); + + DataGridColumnStyle customerNameColumn = new DataGridTextBoxColumn + { + MappingName = "custName", + HeaderText = "Customer Name", + Width = 250 + }; + customersStyle.GridColumnStyles.Add(customerNameColumn); + + DataGridTableStyle ordersStyle = new() + { + MappingName = "Orders", + AlternatingBackColor = Color.LightBlue + }; + + DataGridTextBoxColumn orderDateColumn = new() + { + MappingName = "OrderDate", + HeaderText = "Order Date", + Width = 100 + }; + ordersStyle.GridColumnStyles.Add(orderDateColumn); + + PropertyDescriptorCollection properties = form.BindingContext[dataSet, "Customers.custToOrders"].GetItemProperties(); + PropertyDescriptor? orderAmountProperty = properties["OrderAmount"]; + Assert.NotNull(orderAmountProperty); + + DataGridTextBoxColumn orderAmountColumn = new(orderAmountProperty, "c", true) + { + MappingName = "OrderAmount", + HeaderText = "Total", + Width = 100 + }; + ordersStyle.GridColumnStyles.Add(orderAmountColumn); + + dataGrid.TableStyles.Add(customersStyle); + dataGrid.TableStyles.Add(ordersStyle); + } +} \ No newline at end of file diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/GlobalUsings.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/GlobalUsings.cs new file mode 100644 index 00000000000..9da52515e59 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/GlobalUsings.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +global using Xunit; diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MainMenuTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MainMenuTests.cs new file mode 100644 index 00000000000..e5a1630526e --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MainMenuTests.cs @@ -0,0 +1,143 @@ +namespace System.Windows.Forms.Tests +{ + using System.Drawing; + using System.Runtime.InteropServices; + + public class MainMenuTests + { + [StaFact] + public void MainMenu_SetAndGet_ReturnsCorrectValue() + { + // Arrange + using Form form = new(); + MainMenu mainMenu = new(); + + // Act + form.Menu = mainMenu; + + // Assert + Assert.Same(mainMenu, form.Menu); + } + + [StaFact] + public void MainMenu_AddMenuItem_ReturnsCorrectMenuItem() + { + // Arrange + MainMenu mainMenu = new(); + MenuItem menuItem = new("Test Item"); + + // Act + mainMenu.MenuItems.Add(menuItem); + + // Assert + Assert.Single(mainMenu.MenuItems); + Assert.Same(menuItem, mainMenu.MenuItems[0]); + } + + [StaFact] + public void MainMenu_FileMenuPopup_AddsMenuItemOnPopup() + { + // Arrange + using var form = new Form(); + var mainMenu = new MainMenu(); + var fileMenuItem = new MenuItem("File"); + + // Add popup event handler that adds a menu item when fired + bool popupEventFired = false; + MenuItem? addedMenuItem = null; + + fileMenuItem.Popup += (sender, e) => + { + popupEventFired = true; + addedMenuItem = new MenuItem("Dynamic Item"); + fileMenuItem.MenuItems.Add(addedMenuItem); + }; + + mainMenu.MenuItems.Add(fileMenuItem); + form.Menu = mainMenu; + form.Size = new Size(400, 300); + + // Initially, the File menu should have no items + Assert.Empty(fileMenuItem.MenuItems); + + // Create the handle so we can send Windows messages + var handle = form.Handle; // Forces handle creation + + // Act - Simulate WM_INITMENUPOPUP message to trigger popup event + // This is what Windows sends when a menu is about to be displayed + const uint WM_INITMENUPOPUP = 0x0117; + + // Send the message to trigger the popup event + // The wParam contains the handle to the menu, lParam contains position info + SendMessage(handle, WM_INITMENUPOPUP, fileMenuItem.Handle, IntPtr.Zero); + + // Assert + Assert.True(popupEventFired, "Popup event should have been fired"); + Assert.Single(fileMenuItem.MenuItems); + + if (addedMenuItem is not null) + { + Assert.Same(addedMenuItem, fileMenuItem.MenuItems[0]); + Assert.Equal("Dynamic Item", fileMenuItem.MenuItems[0].Text); + } + + // Clean up + form.Dispose(); + } + + [StaFact] + public void MainMenu_FileMenuWithSubmenu_PopupAddsItemToFileMenu() + { + // Arrange + using Form form = new(); + MainMenu mainMenu = new(); + MenuItem fileMenuItem = new("File"); + MenuItem submenu = new("Submenu"); + + // Add the submenu to the File menu first + fileMenuItem.MenuItems.Add(submenu); + + // Add popup event handler to the File menu (not the submenu) + bool popupEventFired = false; + MenuItem? addedMenuItem = null; + + fileMenuItem.Popup += (sender, e) => + { + popupEventFired = true; + addedMenuItem = new MenuItem("Dynamic Item Added to File"); + fileMenuItem.MenuItems.Add(addedMenuItem); + }; + + mainMenu.MenuItems.Add(fileMenuItem); + form.Menu = mainMenu; + form.Size = new Size(400, 300); + + // Initially, the File menu should have 1 item (the submenu) + Assert.Single(fileMenuItem.MenuItems); + Assert.Same(submenu, fileMenuItem.MenuItems[0]); + + // Create the handle so we can send Windows messages + var handle = form.Handle; // Forces handle creation + + // Act - Simulate WM_INITMENUPOPUP message to the File menu + const uint WM_INITMENUPOPUP = 0x0117; + + // Send the message to trigger the popup event on the File menu + SendMessage(handle, WM_INITMENUPOPUP, fileMenuItem.Handle, IntPtr.Zero); + + // Assert + Assert.True(popupEventFired, "Popup event should have been fired"); + Assert.Equal(2, fileMenuItem.MenuItems.Count); + Assert.Same(submenu, fileMenuItem.MenuItems[0]); + + if (addedMenuItem is not null) + { + Assert.Same(addedMenuItem, fileMenuItem.MenuItems[1]); + Assert.Equal("Dynamic Item Added to File", fileMenuItem.MenuItems[1].Text); + } + } + + [DllImport("user32.dll")] + private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MenuItemTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MenuItemTests.cs new file mode 100644 index 00000000000..c99c0d30c03 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MenuItemTests.cs @@ -0,0 +1,138 @@ +namespace System.Windows.Forms.Tests +{ + public class MenuItemTests + { +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + [StaFact] + public void MenuItem_OnDrawItem_Invoke_Success() + { + var menuItem = new SubMenuItem(); + + // No handler. + menuItem.OnDrawItem(null); + + // Handler. + int callCount = 0; + DrawItemEventHandler handler = (sender, e) => + { + Assert.Same(menuItem, sender); + callCount++; + }; + + menuItem.DrawItem += handler; + menuItem.OnDrawItem(null); + Assert.Equal(1, callCount); + + // Should not call if the handler is removed. + menuItem.DrawItem -= handler; + menuItem.OnDrawItem(null); + Assert.Equal(1, callCount); + } + + [StaFact] + public void MenuItem_OnDrawItem_Disposed_ThrowsObjectDisposedException() + { + var menuItem = new SubMenuItem(); + menuItem.Dispose(); + Assert.Throws(() => menuItem.OnDrawItem(null)); + } + + [StaFact] + public void MenuItem_DrawItem_Disposed_ThrowsObjectDisposedException() + { + var menuItem = new SubMenuItem(); + menuItem.Dispose(); + DrawItemEventHandler handler = (sender, e) => { }; + Assert.Throws(() => menuItem.DrawItem += handler); + Assert.Throws(() => menuItem.DrawItem -= handler); + } + + [StaFact] + public void MenuItem_OnMeasureItem_Invoke_Success() + { + var menuItem = new SubMenuItem(); + + // No handler. + menuItem.OnMeasureItem(null); + + // Handler. + int callCount = 0; + MeasureItemEventHandler handler = (sender, e) => + { + Assert.Same(menuItem, sender); + callCount++; + }; + + menuItem.MeasureItem += handler; + menuItem.OnMeasureItem(null); + Assert.Equal(1, callCount); + + // Should not call if the handler is removed. + menuItem.MeasureItem -= handler; + menuItem.OnMeasureItem(null); + Assert.Equal(1, callCount); + } + + [StaFact] + public void MenuItem_OnMeasureItem_Disposed_ThrowsObjectDisposedException() + { + var menuItem = new SubMenuItem(); + menuItem.Dispose(); + Assert.Throws(() => menuItem.OnMeasureItem(null)); + } + + [StaFact] + public void MenuItem_MeasureItem_Disposed_ThrowsObjectDisposedException() + { + var menuItem = new SubMenuItem(); + menuItem.Dispose(); + MeasureItemEventHandler handler = (sender, e) => { }; + Assert.Throws(() => menuItem.MeasureItem += handler); + Assert.Throws(() => menuItem.MeasureItem -= handler); + } +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + + public class SubMenuItem : MenuItem + { + public SubMenuItem() + { + } + + public SubMenuItem(string text) : base(text) + { + } + + public SubMenuItem(string text, EventHandler onClick) : base(text, onClick) + { + } + + public SubMenuItem(string text, MenuItem[] items) : base(text, items) + { + } + + public SubMenuItem(string text, EventHandler onClick, Shortcut shortcut) : base(text, onClick, shortcut) + { + } + + public SubMenuItem(MenuMerge mergeType, int mergeOrder, Shortcut shortcut, string text, EventHandler onClick, EventHandler onPopup, EventHandler onSelect, MenuItem[] items) : base(mergeType, mergeOrder, shortcut, text, onClick, onPopup, onSelect, items) + { + } + + public new int MenuID => base.MenuID; + + public new void OnClick(EventArgs e) => base.OnClick(e); + + public new void OnDrawItem(DrawItemEventArgs e) => base.OnDrawItem(e); + + public new void OnInitMenuPopup(EventArgs e) => base.OnInitMenuPopup(e); + + public new void OnMeasureItem(MeasureItemEventArgs e) => base.OnMeasureItem(e); + + public new void OnPopup(EventArgs e) => base.OnPopup(e); + + public new void OnSelect(EventArgs e) => base.OnSelect(e); + + public new void CloneMenu(Menu menuSrc) => base.CloneMenu(menuSrc); + } + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MenuSizeCalculationTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MenuSizeCalculationTests.cs new file mode 100644 index 00000000000..129d4098004 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MenuSizeCalculationTests.cs @@ -0,0 +1,286 @@ +namespace System.Windows.Forms.Tests +{ + using System.Drawing; + + public class MenuSizeCalculationTests + { + [StaFact] + public void Form_WithMenu_HasCorrectWindowSize() + { + // Arrange + using var formWithMenu = new Form(); + using var formWithoutMenu = new Form(); + + var menu = new MainMenu(); + menu.MenuItems.Add(new MenuItem("File")); + formWithMenu.Menu = menu; + + var clientSize = new Size(400, 300); + + // Act + formWithMenu.ClientSize = clientSize; + formWithoutMenu.ClientSize = clientSize; + + // Assert + Assert.Equal(clientSize, formWithMenu.ClientSize); + Assert.Equal(clientSize, formWithoutMenu.ClientSize); + + // The key test: form with menu should be taller due to menu bar + Assert.True(formWithMenu.Size.Height > formWithoutMenu.Size.Height, + "Form with menu should be taller than form without menu (accounts for menu bar height)"); + + // Width should be the same (menu doesn't affect width) + Assert.Equal(formWithoutMenu.Size.Width, formWithMenu.Size.Width); + } + + [StaFact] + public void Form_WithEmptyMenu_SameHeightAsFormWithoutMenu() + { + // Arrange + using var formWithEmptyMenu = new Form(); + using var formWithoutMenu = new Form(); + + var emptyMenu = new MainMenu(); // No menu items + formWithEmptyMenu.Menu = emptyMenu; + + var clientSize = new Size(400, 300); + + // Act + formWithEmptyMenu.ClientSize = clientSize; + formWithoutMenu.ClientSize = clientSize; + + // Assert + // According to the implementation, empty menus should not affect window height + Assert.Equal(formWithoutMenu.Size.Height, formWithEmptyMenu.Size.Height); + } + + [StaFact] + public void Form_MenuAddedAfterCreation_AdjustsSize() + { + // Arrange + using var form = new Form(); + var clientSize = new Size(400, 300); + form.ClientSize = clientSize; + + var initialHeight = form.Size.Height; + + // Act - Add menu with items + var menu = new MainMenu(); + menu.MenuItems.Add(new MenuItem("File")); + form.Menu = menu; + form.ClientSize = clientSize; // Trigger recalculation + + // Assert + Assert.True(form.Size.Height > initialHeight, + "Form height should increase when menu with items is added"); + Assert.Equal(clientSize, form.ClientSize); + } + + [StaFact] + public void Form_MenuRemovedAfterCreation_AdjustsSize() + { + // Arrange + using var form = new Form(); + var menu = new MainMenu(); + menu.MenuItems.Add(new MenuItem("File")); + form.Menu = menu; + + var clientSize = new Size(400, 300); + form.ClientSize = clientSize; + var heightWithMenu = form.Size.Height; + + // Act - Remove menu + form.Menu = null; + form.ClientSize = clientSize; // Trigger recalculation + + // Assert + Assert.True(form.Size.Height < heightWithMenu, + "Form height should decrease when menu is removed"); + Assert.Equal(clientSize, form.ClientSize); + } + + [StaFact] + public void Form_NonTopLevel_MenuDoesNotAffectSize() + { + // Arrange + using var parentForm = new Form(); + using var childForm = new Form(); + + var menu = new MainMenu(); + menu.MenuItems.Add(new MenuItem("File")); + childForm.Menu = menu; + childForm.TopLevel = false; + childForm.Parent = parentForm; + + var clientSize = new Size(200, 150); + + // Create a comparable form without menu but also non-toplevel + using var childFormNoMenu = new Form(); + childFormNoMenu.TopLevel = false; + childFormNoMenu.Parent = parentForm; + + // Act + childForm.ClientSize = clientSize; + childFormNoMenu.ClientSize = clientSize; + + // Assert + // Non-top-level forms should not account for menus in sizing + Assert.Equal(childFormNoMenu.Size.Height, childForm.Size.Height); + } + + [StaFact] + public void MDIChild_MenuDoesNotAffectSize() + { + // Arrange + using var parentForm = new Form(); + parentForm.IsMdiContainer = true; + + using var mdiChild1 = new Form(); + using var mdiChild2 = new Form(); + + var menu = new MainMenu(); + menu.MenuItems.Add(new MenuItem("File")); + mdiChild1.Menu = menu; + + mdiChild1.MdiParent = parentForm; + mdiChild2.MdiParent = parentForm; + + var clientSize = new Size(200, 150); + + // Act + mdiChild1.ClientSize = clientSize; + mdiChild2.ClientSize = clientSize; + + // Assert + // MDI children should not account for menus in sizing + Assert.Equal(mdiChild2.Size.Height, mdiChild1.Size.Height); + } + + [StaFact] + public void Form_MenuWithMultipleItems_SameHeightAsMenuWithOneItem() + { + // Arrange + using var formWithOneMenuItem = new Form(); + using var formWithMultipleMenuItems = new Form(); + + var menuOne = new MainMenu(); + menuOne.MenuItems.Add(new MenuItem("File")); + formWithOneMenuItem.Menu = menuOne; + + var menuMultiple = new MainMenu(); + menuMultiple.MenuItems.Add(new MenuItem("File")); + menuMultiple.MenuItems.Add(new MenuItem("Edit")); + menuMultiple.MenuItems.Add(new MenuItem("View")); + formWithMultipleMenuItems.Menu = menuMultiple; + + var clientSize = new Size(400, 300); + + // Act + formWithOneMenuItem.ClientSize = clientSize; + formWithMultipleMenuItems.ClientSize = clientSize; + + // Assert + // Number of menu items shouldn't affect form height (all in same menu bar) + Assert.Equal(formWithOneMenuItem.Size.Height, formWithMultipleMenuItems.Size.Height); + } + + [StaFact] + public void Form_MenuWithSubmenus_SameHeightAsMenuWithoutSubmenus() + { + // Arrange + using var formWithSubmenu = new Form(); + using var formWithoutSubmenu = new Form(); + + var menuWithSubmenu = new MainMenu(); + var fileMenu = new MenuItem("File"); + fileMenu.MenuItems.Add(new MenuItem("New")); + fileMenu.MenuItems.Add(new MenuItem("Open")); + menuWithSubmenu.MenuItems.Add(fileMenu); + formWithSubmenu.Menu = menuWithSubmenu; + + var menuWithoutSubmenu = new MainMenu(); + menuWithoutSubmenu.MenuItems.Add(new MenuItem("File")); + formWithoutSubmenu.Menu = menuWithoutSubmenu; + + var clientSize = new Size(400, 300); + + // Act + formWithSubmenu.ClientSize = clientSize; + formWithoutSubmenu.ClientSize = clientSize; + + // Assert + // Submenus shouldn't affect form height (they're dropdowns) + Assert.Equal(formWithoutSubmenu.Size.Height, formWithSubmenu.Size.Height); + } + + [StaFact] + public void Form_SetClientSizeMultipleTimes_ConsistentBehavior() + { + // Test that the HasMenu logic is consistently applied + + // Arrange + using var form = new Form(); + var menu = new MainMenu(); + menu.MenuItems.Add(new MenuItem("File")); + form.Menu = menu; + + var clientSize1 = new Size(300, 200); + var clientSize2 = new Size(500, 400); + + // Act & Assert + form.ClientSize = clientSize1; + Assert.Equal(clientSize1, form.ClientSize); + var height1 = form.Size.Height; + + form.ClientSize = clientSize2; + Assert.Equal(clientSize2, form.ClientSize); + var height2 = form.Size.Height; + + // The height difference should be proportional to client size difference + var clientHeightDiff = clientSize2.Height - clientSize1.Height; + var windowHeightDiff = height2 - height1; + Assert.Equal(clientHeightDiff, windowHeightDiff); + } + + [StaFact] + public void Form_MenuRemovedBeforeHandleCreated_SizeUpdatesImmediately() + { + // This test verifies the fix for the issue where setting Menu to null + // before handle creation didn't update the form size immediately + + // Arrange + using var form = new Form(); + + // Create menu with items + var menu = new MainMenu(); + menu.MenuItems.Add(new MenuItem("File")); + menu.MenuItems.Add(new MenuItem("Edit")); + + // Set menu first + form.Menu = menu; + + // Set client size - this should trigger FormStateSetClientSize + var targetClientSize = new Size(400, 300); + form.ClientSize = targetClientSize; + + // Capture size with menu (before handle is created) + var sizeWithMenu = form.Size; + + // Act - Remove menu before handle is created + // This should trigger the fix in line 760: if FormStateSetClientSize == 1 && !IsHandleCreated + form.Menu = null; + + // Assert + var sizeWithoutMenu = form.Size; + + // The form size should be updated immediately when menu is removed + Assert.True(sizeWithoutMenu.Height < sizeWithMenu.Height, + "Form height should decrease immediately when menu is removed before handle creation"); + + Assert.Equal(targetClientSize, form.ClientSize); + + // Width should remain the same + Assert.Equal(sizeWithMenu.Width, sizeWithoutMenu.Width); + } + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MouseOperations.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MouseOperations.cs new file mode 100644 index 00000000000..cfb7c1dac39 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/MouseOperations.cs @@ -0,0 +1,24 @@ +namespace System.Windows.Forms.Tests +{ + using System.Runtime.InteropServices; + + public static class MouseOperations + { + [Flags] + public enum MouseEventFlags + { + LeftDown = 0x00000002, + LeftUp = 0x00000004, + RightDown = 0x00000008, + RightUp = 0x00000010, + } + + [DllImport("user32.dll")] + private static extern void mouse_event(int dwFlags, uint dx, uint dy, int dwData, int dwExtraInfo); + + public static void MouseEvent(MouseEventFlags value, uint dx, uint dy) + { + mouse_event((int)value, dx, dy, 0, 0); + } + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/StatusBarTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/StatusBarTests.cs new file mode 100644 index 00000000000..af644845561 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/StatusBarTests.cs @@ -0,0 +1,344 @@ +using System.Drawing; + +namespace System.Windows.Forms.Tests; + +public class StatusBarTests +{ + [StaFact] + public void StatusBar_DefaultProperties_MatchExpectedValues() + { + using StatusBar statusBar = new(); + + Assert.Equal(DockStyle.Bottom, statusBar.Dock); + Assert.False(statusBar.ShowPanels); + Assert.True(statusBar.SizingGrip); + Assert.False(statusBar.TabStop); + Assert.Equal(string.Empty, statusBar.Text); + Assert.Empty(statusBar.Panels); + } + + [StaFact] + public void StatusBar_Panels_AddRange_SetsPanelCountAndParent() + { + using StatusBar statusBar = new(); + StatusBarPanel panel1 = new() { Text = "Panel 1" }; + StatusBarPanel panel2 = new() { Text = "Panel 2" }; + StatusBarPanel panel3 = new() { Text = "Panel 3" }; + + statusBar.Panels.AddRange([panel1, panel2, panel3]); + + Assert.Equal(3, statusBar.Panels.Count); + Assert.Same(panel1, statusBar.Panels[0]); + Assert.Same(panel2, statusBar.Panels[1]); + Assert.Same(panel3, statusBar.Panels[2]); + Assert.Same(statusBar, panel1.Parent); + Assert.Same(statusBar, panel2.Parent); + Assert.Same(statusBar, panel3.Parent); + } + + [StaFact] + public void StatusBar_Panels_AddByText_CreatesAndReturnsPanel() + { + using StatusBar statusBar = new(); + + StatusBarPanel panel = statusBar.Panels.Add("Ready"); + + Assert.Single(statusBar.Panels); + Assert.Equal("Ready", panel.Text); + Assert.Same(statusBar, panel.Parent); + } + + [StaFact] + public void StatusBar_Panels_Insert_PlacesAtCorrectIndex() + { + using StatusBar statusBar = new(); + StatusBarPanel first = new() { Name = "first", Text = "First" }; + StatusBarPanel last = new() { Name = "last", Text = "Last" }; + StatusBarPanel inserted = new() { Name = "inserted", Text = "Inserted" }; + + statusBar.Panels.AddRange([first, last]); + + statusBar.Panels.Insert(1, inserted); + + Assert.Equal(3, statusBar.Panels.Count); + Assert.Same(first, statusBar.Panels[0]); + Assert.Same(inserted, statusBar.Panels[1]); + Assert.Same(last, statusBar.Panels[2]); + Assert.Same(statusBar, inserted.Parent); + } + + [StaFact] + public void StatusBar_Panels_Remove_ClearsParentReference() + { + using StatusBar statusBar = new(); + StatusBarPanel panel = new() { Text = "Removable" }; + statusBar.Panels.Add(panel); + + statusBar.Panels.Remove(panel); + + Assert.Empty(statusBar.Panels); + Assert.Null(panel.Parent); + } + + [StaFact] + public void StatusBar_Panels_RemoveAt_RemovesByIndexAndClearsParent() + { + using StatusBar statusBar = new(); + StatusBarPanel keep = new() { Text = "Keep" }; + StatusBarPanel remove = new() { Text = "Remove" }; + statusBar.Panels.AddRange([keep, remove]); + + statusBar.Panels.RemoveAt(1); + + Assert.Single(statusBar.Panels); + Assert.Same(keep, statusBar.Panels[0]); + Assert.Null(remove.Parent); + } + + [StaFact] + public void StatusBar_Panels_Clear_RemovesAllPanelsAndClearsParents() + { + using StatusBar statusBar = new(); + StatusBarPanel panel1 = new() { Text = "A" }; + StatusBarPanel panel2 = new() { Text = "B" }; + statusBar.Panels.AddRange([panel1, panel2]); + + statusBar.Panels.Clear(); + + Assert.Empty(statusBar.Panels); + Assert.Null(panel1.Parent); + Assert.Null(panel2.Parent); + } + + [StaFact] + public void StatusBar_Panels_Contains_ReturnsTrueForAddedPanel_FalseAfterRemoval() + { + using StatusBar statusBar = new(); + StatusBarPanel panel = new() { Text = "Test" }; + + statusBar.Panels.Add(panel); + + Assert.True(statusBar.Panels.Contains(panel)); + + statusBar.Panels.Remove(panel); + + Assert.False(statusBar.Panels.Contains(panel)); + } + + [StaFact] + public void StatusBar_Panels_ContainsKey_FindsByName() + { + using StatusBar statusBar = new(); + statusBar.Panels.Add(new StatusBarPanel { Name = "clock", Text = "12:00" }); + + Assert.True(statusBar.Panels.ContainsKey("clock")); + Assert.False(statusBar.Panels.ContainsKey("missing")); + } + + [StaFact] + public void StatusBar_Panels_IndexOf_ReturnsCorrectPosition() + { + using StatusBar statusBar = new(); + StatusBarPanel panel1 = new() { Name = "mode" }; + StatusBarPanel panel2 = new() { Name = "detail" }; + statusBar.Panels.AddRange([panel1, panel2]); + + Assert.Equal(0, statusBar.Panels.IndexOf(panel1)); + Assert.Equal(1, statusBar.Panels.IndexOf(panel2)); + Assert.Equal(0, statusBar.Panels.IndexOfKey("mode")); + Assert.Equal(1, statusBar.Panels.IndexOfKey("detail")); + Assert.Equal(-1, statusBar.Panels.IndexOfKey("missing")); + } + + [StaFact] + public void StatusBar_ShowPanels_ToggleToFalse_SetsSimpleTextWithoutLosingPanels() + { + using Form form = new() { ClientSize = new Size(600, 300) }; + using StatusBar statusBar = new() { ShowPanels = true }; + StatusBarPanel panel = new() { Text = "Status" }; + statusBar.Panels.Add(panel); + form.Controls.Add(statusBar); + _ = form.Handle; + + statusBar.ShowPanels = false; + statusBar.Text = "Ready"; + + Assert.False(statusBar.ShowPanels); + Assert.Equal("Ready", statusBar.Text); + Assert.Single(statusBar.Panels); + Assert.Same(statusBar, panel.Parent); + } + + [StaFact] + public void StatusBar_ShowPanels_RestoreToTrue_ReexposesExistingPanels() + { + using Form form = new() { ClientSize = new Size(600, 300) }; + using StatusBar statusBar = new() { ShowPanels = true }; + StatusBarPanel panel = new() { Name = "mode", Text = "Panels Mode" }; + statusBar.Panels.Add(panel); + form.Controls.Add(statusBar); + _ = form.Handle; + + statusBar.ShowPanels = false; + statusBar.ShowPanels = true; + panel.Text = "Panels Mode"; + + Assert.True(statusBar.ShowPanels); + Assert.Single(statusBar.Panels); + Assert.Equal("Panels Mode", statusBar.Panels[0].Text); + } + + [StaFact] + public void StatusBar_ToString_IncludesPanelCountAndFirstPanelText() + { + using StatusBar statusBar = new(); + statusBar.Panels.AddRange([ + new StatusBarPanel { Text = "Ready" }, + new StatusBarPanel { Text = "12:00" } + ]); + + string result = statusBar.ToString(); + + Assert.Contains("Panels.Count: 2", result); + Assert.Contains("StatusBarPanel: {Ready}", result); + } + + [StaFact] + public void StatusBarPanel_Alignment_DefaultIsLeft_CanBeChangedToRightAndCenter() + { + StatusBarPanel panel = new(); + + Assert.Equal(HorizontalAlignment.Left, panel.Alignment); + + panel.Alignment = HorizontalAlignment.Right; + Assert.Equal(HorizontalAlignment.Right, panel.Alignment); + + panel.Alignment = HorizontalAlignment.Center; + Assert.Equal(HorizontalAlignment.Center, panel.Alignment); + } + + [StaFact] + public void StatusBarPanel_AutoSize_DefaultIsNone_AllValuesSupported() + { + StatusBarPanel panel = new(); + + Assert.Equal(StatusBarPanelAutoSize.None, panel.AutoSize); + + panel.AutoSize = StatusBarPanelAutoSize.Spring; + Assert.Equal(StatusBarPanelAutoSize.Spring, panel.AutoSize); + + panel.AutoSize = StatusBarPanelAutoSize.Contents; + Assert.Equal(StatusBarPanelAutoSize.Contents, panel.AutoSize); + } + + [StaFact] + public void StatusBarPanel_BorderStyle_DefaultIsSunken_AllValuesSupported() + { + StatusBarPanel panel = new(); + + Assert.Equal(StatusBarPanelBorderStyle.Sunken, panel.BorderStyle); + + panel.BorderStyle = StatusBarPanelBorderStyle.Raised; + Assert.Equal(StatusBarPanelBorderStyle.Raised, panel.BorderStyle); + + panel.BorderStyle = StatusBarPanelBorderStyle.None; + Assert.Equal(StatusBarPanelBorderStyle.None, panel.BorderStyle); + } + + [StaFact] + public void StatusBarPanel_Style_DefaultIsText_OwnerDrawSupported() + { + StatusBarPanel panel = new(); + + Assert.Equal(StatusBarPanelStyle.Text, panel.Style); + + panel.Style = StatusBarPanelStyle.OwnerDraw; + + Assert.Equal(StatusBarPanelStyle.OwnerDraw, panel.Style); + } + + [StaFact] + public void StatusBarPanel_Width_ThrowsWhenSetBelowMinWidth() + { + StatusBarPanel panel = new() { MinWidth = 50 }; + + Assert.Throws(() => panel.Width = 30); + } + + [StaFact] + public void StatusBarPanel_MinWidth_WhenRaisedAboveCurrentWidth_ExpandsWidth() + { + StatusBarPanel panel = new() { Width = 60, MinWidth = 10 }; + + panel.MinWidth = 80; + + Assert.Equal(80, panel.MinWidth); + Assert.Equal(80, panel.Width); + } + + [StaFact] + public void StatusBarPanel_ToolTipText_DefaultIsEmpty_CanBeSet() + { + StatusBarPanel panel = new(); + + Assert.Equal(string.Empty, panel.ToolTipText); + + panel.ToolTipText = "Shows the current connection status."; + + Assert.Equal("Shows the current connection status.", panel.ToolTipText); + } + + [StaFact] + public void StatusBarPanel_ToString_IncludesText() + { + StatusBarPanel panel = new() { Text = "Ready" }; + + Assert.Equal("StatusBarPanel: {Ready}", panel.ToString()); + } + + [StaFact] + public void StatusBar_PanelClick_Event_FiredThroughOnPanelClick() + { + using SubStatusBar statusBar = new(); + StatusBarPanel panel = new() { Text = "Clickable" }; + statusBar.Panels.Add(panel); + + StatusBarPanelClickEventArgs? received = null; + statusBar.PanelClick += (_, e) => received = e; + + StatusBarPanelClickEventArgs args = new(panel, MouseButtons.Left, 1, 10, 5); + statusBar.RaisePanelClick(args); + + Assert.NotNull(received); + Assert.Same(panel, received.StatusBarPanel); + Assert.Equal(MouseButtons.Left, received.Button); + Assert.Equal(1, received.Clicks); + } + + [StaFact] + public void StatusBar_DrawItem_Event_FiredThroughOnDrawItem() + { + using SubStatusBar statusBar = new(); + StatusBarPanel panel = new() { Style = StatusBarPanelStyle.OwnerDraw, Width = 100 }; + statusBar.Panels.Add(panel); + + StatusBarDrawItemEventArgs? received = null; + statusBar.DrawItem += (_, e) => received = e; + + using Bitmap bitmap = new(10, 10); + using Graphics g = Graphics.FromImage(bitmap); + StatusBarDrawItemEventArgs args = new(g, SystemFonts.DefaultFont!, Rectangle.Empty, 0, DrawItemState.None, panel); + statusBar.RaiseDrawItem(args); + + Assert.NotNull(received); + Assert.Same(panel, received.Panel); + Assert.Equal(DrawItemState.None, received.State); + } + + private sealed class SubStatusBar : StatusBar + { + public void RaisePanelClick(StatusBarPanelClickEventArgs e) => OnPanelClick(e); + + public void RaiseDrawItem(StatusBarDrawItemEventArgs e) => OnDrawItem(e); + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/System.Windows.Forms.Legacy.Tests.csproj b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/System.Windows.Forms.Legacy.Tests.csproj new file mode 100644 index 00000000000..2d809de49a8 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/System.Windows.Forms.Legacy.Tests.csproj @@ -0,0 +1,23 @@ + + + + $(NetCurrent)-windows7.0 + true + true + enable + enable + $(NoWarn);SA1005;SA1028;WFDEV006 + + System.Windows.Forms.Tests + false + true + $([System.IO.Path]::GetFullPath('$(RepoRoot)Bin\$(OutDirName)\')) + + + + + + + + + diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/TabControlTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/TabControlTests.cs new file mode 100644 index 00000000000..5050e89093f --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/TabControlTests.cs @@ -0,0 +1,52 @@ +namespace System.Windows.Forms.Tests +{ + public class TabControlTests + { + [StaFact] + public void TabControl_ClearTabsWhileSelected_DoesNotThrowNullReferenceException() + { + Exception? capturedException = null; + string? failureMessage = null; + ThreadExceptionEventHandler handler = (_, e) => capturedException = e.Exception; + + Application.ThreadException += handler; + try + { + using Form form = new(); + using TabControl control = new(); + + control.TabPages.Add(new TabPage("Tab 1")); + control.TabPages.Add(new TabPage("Tab 2")); + control.TabPages.Add(new TabPage("Tab 3")); + + form.Controls.Add(control); + form.Show(); + _ = control.AccessibilityObject; + + control.SelectedIndex = 1; + Application.DoEvents(); + + control.TabPages.Clear(); + + Application.DoEvents(); + System.Threading.Thread.Sleep(10); + + Assert.Empty(control.TabPages); + Assert.Null(control.SelectedTab); + } + finally + { + Application.ThreadException -= handler; + if (capturedException is not null) + { + failureMessage = $"Unhandled exception: {capturedException.GetType().Name}\n{capturedException.Message}\n{capturedException.StackTrace}"; + } + } + + if (failureMessage is not null) + { + throw new Xunit.Sdk.XunitException(failureMessage); + } + } + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ToolBarTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ToolBarTests.cs new file mode 100644 index 00000000000..cef6bf352a4 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ToolBarTests.cs @@ -0,0 +1,268 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Drawing; + +namespace System.Windows.Forms.Tests; + +public class ToolBarTests +{ + [StaFact] + public void ToolBar_DefaultConfiguration_MatchesLegacyBehavior() + { + using ToolBar toolBar = new(); + + Assert.Equal(ToolBarAppearance.Normal, toolBar.Appearance); + Assert.Equal(BorderStyle.None, toolBar.BorderStyle); + Assert.Equal(DockStyle.Top, toolBar.Dock); + Assert.Equal(ToolBarTextAlign.Underneath, toolBar.TextAlign); + Assert.True(toolBar.AutoSize); + Assert.True(toolBar.Divider); + Assert.True(toolBar.DropDownArrows); + Assert.True(toolBar.ShowToolTips); + Assert.True(toolBar.Wrappable); + Assert.False(toolBar.TabStop); + Assert.Empty(toolBar.Buttons); + Assert.Equal(new Size(39, 36), toolBar.ButtonSize); + } + + [StaFact] + public void ToolBar_ButtonSize_DefaultTracksTextAlignment() + { + using ToolBar toolBar = new(); + + toolBar.TextAlign = ToolBarTextAlign.Right; + + Assert.Equal(new Size(23, 22), toolBar.ButtonSize); + } + + [StaFact] + public void ToolBar_Buttons_AddRange_PreservesOrderAndSetsParent() + { + using ToolBar toolBar = new(); + ToolBarButton openButton = new() { Name = "open", Text = "Open" }; + ToolBarButton saveButton = new() { Name = "save", Text = "Save" }; + ToolBarButton printButton = new() { Name = "print", Text = "Print" }; + + toolBar.Buttons.AddRange([openButton, saveButton, printButton]); + + Assert.Equal(3, toolBar.Buttons.Count); + Assert.Same(openButton, toolBar.Buttons[0]); + Assert.Same(saveButton, toolBar.Buttons[1]); + Assert.Same(printButton, toolBar.Buttons[2]); + Assert.Same(toolBar, openButton.Parent); + Assert.Same(toolBar, saveButton.Parent); + Assert.Same(toolBar, printButton.Parent); + } + + [StaFact] + public void ToolBar_Buttons_Insert_UpdatesOrderAndKeyLookup() + { + using ToolBar toolBar = new(); + ToolBarButton firstButton = new() { Name = "first", Text = "First" }; + ToolBarButton lastButton = new() { Name = "last", Text = "Last" }; + ToolBarButton insertedButton = new() { Name = "inserted", Text = "Inserted" }; + + toolBar.Buttons.AddRange([firstButton, lastButton]); + + toolBar.Buttons.Insert(1, insertedButton); + + Assert.Same(firstButton, toolBar.Buttons[0]); + Assert.Same(insertedButton, toolBar.Buttons[1]); + Assert.Same(lastButton, toolBar.Buttons[2]); + Assert.Equal(1, toolBar.Buttons.IndexOfKey("inserted")); + Assert.Same(insertedButton, toolBar.Buttons["INSERTED"]); + } + + [StaFact] + public void ToolBar_Buttons_SetIndexer_ReplacesButtonAndUpdatesParents() + { + using ToolBar toolBar = new(); + ToolBarButton originalButton = new() { Name = "original", Text = "Original" }; + ToolBarButton replacementButton = new() { Name = "replacement", Text = "Replacement" }; + + toolBar.Buttons.Add(originalButton); + + toolBar.Buttons[0] = replacementButton; + + Assert.Null(originalButton.Parent); + Assert.Same(replacementButton, toolBar.Buttons[0]); + Assert.Same(toolBar, replacementButton.Parent); + } + + [StaFact] + public void ToolBar_Buttons_RemoveAt_DetachesRemovedButton() + { + using ToolBar toolBar = new(); + ToolBarButton keepButton = new() { Name = "keep", Text = "Keep" }; + ToolBarButton removeButton = new() { Name = "remove", Text = "Remove" }; + + toolBar.Buttons.AddRange([keepButton, removeButton]); + + toolBar.Buttons.RemoveAt(1); + + Assert.Single(toolBar.Buttons); + Assert.Same(keepButton, toolBar.Buttons[0]); + Assert.Null(removeButton.Parent); + } + + [StaFact] + public void ToolBar_Buttons_Clear_DetachesAllButtons() + { + using ToolBar toolBar = new(); + ToolBarButton firstButton = new() { Text = "First" }; + ToolBarButton secondButton = new() { Text = "Second" }; + + toolBar.Buttons.AddRange([firstButton, secondButton]); + + toolBar.Buttons.Clear(); + + Assert.Empty(toolBar.Buttons); + Assert.Null(firstButton.Parent); + Assert.Null(secondButton.Parent); + } + + [StaFact] + public void ToolBar_Buttons_KeyLookup_IsCaseInsensitive_AndEmptyKeysAreIgnored() + { + using ToolBar toolBar = new(); + ToolBarButton saveButton = new() { Name = "Save", Text = "Save" }; + + toolBar.Buttons.Add(saveButton); + + Assert.True(toolBar.Buttons.ContainsKey("save")); + Assert.Equal(0, toolBar.Buttons.IndexOfKey("SAVE")); + Assert.Same(saveButton, toolBar.Buttons["sAvE"]); + Assert.False(toolBar.Buttons.ContainsKey(string.Empty)); + Assert.Equal(-1, toolBar.Buttons.IndexOfKey(string.Empty)); + Assert.Null(toolBar.Buttons[string.Empty]); + Assert.Null(toolBar.Buttons[(string)null!]); + } + + [StaFact] + public void ToolBar_Buttons_RemoveByKey_MissingKey_IsNoOp() + { + using ToolBar toolBar = new(); + ToolBarButton button = new() { Name = "open", Text = "Open" }; + + toolBar.Buttons.Add(button); + + toolBar.Buttons.RemoveByKey("missing"); + + Assert.Single(toolBar.Buttons); + Assert.Same(button, toolBar.Buttons[0]); + Assert.Same(toolBar, button.Parent); + } + + [StaFact] + public void ToolBar_ButtonSize_NegativeDimension_ThrowsArgumentOutOfRangeException() + { + using ToolBar toolBar = new(); + + Assert.Throws(() => toolBar.ButtonSize = new Size(-1, 10)); + Assert.Throws(() => toolBar.ButtonSize = new Size(10, -1)); + } +} + +public class ToolBarButtonTests +{ + [StaFact] + public void ToolBarButton_DefaultState_MatchesToolbarButtonBehavior() + { + using ToolBarButton button = new(); + + Assert.Equal(string.Empty, button.Text); + Assert.Equal(string.Empty, button.ToolTipText); + Assert.Equal(ToolBarButtonStyle.PushButton, button.Style); + Assert.True(button.Enabled); + Assert.True(button.Visible); + Assert.False(button.Pushed); + Assert.False(button.PartialPush); + Assert.Equal(Rectangle.Empty, button.Rectangle); + Assert.Null(button.Parent); + Assert.Null(button.DropDownMenu); + Assert.Null(button.Tag); + } + + [StaFact] + public void ToolBarButton_Text_NormalizesNullAndEmptyToEmptyString() + { + using ToolBarButton button = new("Initial"); + + button.Text = null; + Assert.Equal(string.Empty, button.Text); + + button.Text = string.Empty; + Assert.Equal(string.Empty, button.Text); + } + + [StaFact] + public void ToolBarButton_DropDownMenu_RequiresContextMenu() + { + using ToolBarButton button = new(); + using ContextMenu contextMenu = new(); + using MainMenu mainMenu = new(); + + button.DropDownMenu = contextMenu; + + Assert.Same(contextMenu, button.DropDownMenu); + Assert.Throws(() => button.DropDownMenu = mainMenu); + } + + [StaFact] + public void ToolBarButton_ImageIndex_LessThanMinusOne_ThrowsArgumentOutOfRangeException() + { + using ToolBarButton button = new(); + + Assert.Throws(() => button.ImageIndex = -2); + } + + [StaFact] + public void ToolBarButton_StateProperties_RoundTripWithoutParentHandle() + { + using ToolBarButton button = new(); + object tag = new(); + + button.Enabled = false; + button.Visible = false; + button.Pushed = true; + button.PartialPush = true; + button.Style = ToolBarButtonStyle.ToggleButton; + button.ToolTipText = "Click to toggle"; + button.Tag = tag; + + Assert.False(button.Enabled); + Assert.False(button.Visible); + Assert.True(button.Pushed); + Assert.True(button.PartialPush); + Assert.Equal(ToolBarButtonStyle.ToggleButton, button.Style); + Assert.Equal("Click to toggle", button.ToolTipText); + Assert.Same(tag, button.Tag); + } +} + +public class ToolBarButtonClickEventArgsTests +{ + [StaFact] + public void ToolBarButtonClickEventArgs_Constructor_StoresButtonReference() + { + using ToolBarButton button = new() { Text = "Open" }; + + ToolBarButtonClickEventArgs eventArgs = new(button); + + Assert.Same(button, eventArgs.Button); + } + + [StaFact] + public void ToolBarButtonClickEventArgs_Button_CanBeReassigned() + { + using ToolBarButton originalButton = new() { Text = "Original" }; + using ToolBarButton replacementButton = new() { Text = "Replacement" }; + ToolBarButtonClickEventArgs eventArgs = new(originalButton); + + eventArgs.Button = replacementButton; + + Assert.Same(replacementButton, eventArgs.Button); + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ToolBarToolTipTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ToolBarToolTipTests.cs new file mode 100644 index 00000000000..c05917b81f5 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/ToolBarToolTipTests.cs @@ -0,0 +1,255 @@ +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Controls; +using Windows.Win32.UI.WindowsAndMessaging; +using DrawingPoint = System.Drawing.Point; +using DrawingSize = System.Drawing.Size; + +namespace System.Windows.Forms.Tests +{ + public class ToolBarToolTipTests + { + // Minimal interop needed for this test (use internal wrappers/constants where available) + + // Hot item flags + [Flags] + private enum HICF : uint + { + ENTERING = 0x0010, + } + + private const uint TbnHotItemChange = unchecked((uint)-713); + + [StructLayout(LayoutKind.Sequential)] + private struct NMTBHOTITEM + { + public NMHDR hdr; + public int idOld; + public int idNew; + public HICF dwFlags; + } + + // High DPI thread awareness helpers (User32) + [DllImport("user32.dll", EntryPoint = "SetThreadDpiAwarenessContext", ExactSpelling = true)] + private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext); + + [DllImport("user32.dll", EntryPoint = "GetThreadDpiAwarenessContext", ExactSpelling = true)] + private static extern IntPtr GetThreadDpiAwarenessContext(); + + // Known DPI_AWARENESS_CONTEXT values (casted from macros): + // https://learn.microsoft.com/windows/win32/hidpi/dpi-awareness-context + private static readonly IntPtr s_dpiAwarenessContextPerMonitorAwareV2 = new(-4); + + private sealed class DpiAwarenessScope : IDisposable + { + private readonly IntPtr _old; + private readonly bool _enabled; + + public DpiAwarenessScope(IntPtr newContext) + { + try + { + _old = GetThreadDpiAwarenessContext(); + _enabled = SetThreadDpiAwarenessContext(newContext) != IntPtr.Zero; + } + catch (EntryPointNotFoundException) + { + _enabled = false; // OS doesn't support per-thread DPI; proceed without changing + } + } + + public void Dispose() + { + if (_enabled) + { + try + { + SetThreadDpiAwarenessContext(_old); + } + catch (EntryPointNotFoundException) + { + /* ignore */ + } + } + } + } + + [StaFact] + public void ToolBar_ToolTip_Show_DoesNotMove_ToolBar() + { + using var form = new Form + { + StartPosition = FormStartPosition.Manual, + Location = new DrawingPoint(100, 100), + Size = new DrawingSize(400, 200) + }; + + var toolBar = new ToolBar + { + ShowToolTips = true, + Dock = DockStyle.None, + Location = new DrawingPoint(0, 0) + }; + + toolBar.Buttons.Add(new ToolBarButton("Btn") { ToolTipText = "Tip" }); + form.Controls.Add(toolBar); + + form.Show(); + Application.DoEvents(); + + // Precondition: toolbar starts at 0,0 + Assert.Equal(new DrawingPoint(0, 0), toolBar.Location); + + // Get the native tooltip HWND created by the toolbar + HWND tooltipHwnd = (HWND)PInvokeCore.SendMessage(toolBar, PInvoke.TB_GETTOOLTIPS); + Assert.True(tooltipHwnd != HWND.Null, "Expected native tooltip window"); + + // Force the tooltip window top-left at (0,0) so TTN_SHOW logic will try to reposition it + PInvoke.SetWindowPos(tooltipHwnd, HWND.Null, 0, 0, 0, 0, + SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + + // 1) Simulate hot item change so internal hotItem != -1 + var hot = new NMTBHOTITEM + { + hdr = new NMHDR { hwndFrom = (HWND)toolBar.Handle, idFrom = 0, code = TbnHotItemChange }, + idOld = -1, + idNew = 0, + dwFlags = HICF.ENTERING + }; + PInvokeCore.SendMessage(toolBar, MessageId.WM_REFLECT_NOTIFY, 0, ref hot); + + Application.DoEvents(); + + // 2) Simulate TTN_SHOW from the tooltip window + var nmhdr = new NMHDR { hwndFrom = tooltipHwnd, idFrom = 0, code = PInvoke.TTN_SHOW }; + PInvokeCore.SendMessage(toolBar, MessageId.WM_REFLECT_NOTIFY, 0, ref nmhdr); + + Application.DoEvents(); + + // Assertion: Showing the tooltip must NOT move the toolbar. + // This would fail under the original bug where SetWindowPos targeted the toolbar HWND. + Assert.Equal(new DrawingPoint(0, 0), toolBar.Location); + + form.Close(); + } + + [StaFact] + public void ToolBar_ToolTip_Show_Returns_1_When_Tooltip_Is_At_0_0() + { + using var form = new Form + { + StartPosition = FormStartPosition.Manual, + Location = new DrawingPoint(200, 200), + Size = new DrawingSize(400, 200) + }; + + var toolBar = new ToolBar + { + ShowToolTips = true, + Dock = DockStyle.None, + Location = new DrawingPoint(10, 10) + }; + + toolBar.Buttons.Add(new ToolBarButton("Btn") { ToolTipText = "Tip" }); + form.Controls.Add(toolBar); + + form.Show(); + Application.DoEvents(); + + // Ensure toolbar is not at 0,0 so wrong GetWindowPlacement handle avoids the reposition branch + Assert.Equal(new DrawingPoint(10, 10), toolBar.Location); + + // Acquire native tooltip HWND + HWND tooltipHwnd = (HWND)PInvokeCore.SendMessage(toolBar, PInvoke.TB_GETTOOLTIPS); + Assert.NotEqual(HWND.Null, tooltipHwnd); + + // Force the tooltip at (0,0) to trigger the reposition path when correct handle is used + PInvoke.SetWindowPos(tooltipHwnd, HWND.Null, 0, 0, 0, 0, + SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + + // Simulate hot item change so hotItem != -1 + var hot = new NMTBHOTITEM + { + hdr = new NMHDR { hwndFrom = (HWND)toolBar.Handle, idFrom = 0, code = TbnHotItemChange }, + idOld = -1, + idNew = 0, + dwFlags = HICF.ENTERING + }; + PInvokeCore.SendMessage(toolBar, MessageId.WM_REFLECT_NOTIFY, 0, ref hot); + + Application.DoEvents(); + + // Simulate TTN_SHOW and capture the return value from WndProc + var nmhdr = new NMHDR { hwndFrom = tooltipHwnd, idFrom = 0, code = PInvoke.TTN_SHOW }; + nint ret = PInvokeCore.SendMessage(toolBar, MessageId.WM_REFLECT_NOTIFY, 0, ref nmhdr); + + // Expect: when the correct window (tooltip) is queried for placement, TTN_SHOW repositions and returns 1. + // With the buggy code that queries the toolbar's placement, the condition won't trigger and ret will be 0. + Assert.Equal((nint)1, ret); + + form.Close(); + } + + [StaFact] + public void ToolBar_ToolTip_TTN_SHOW_PerMonitorV2_DoesNotMove_And_Returns_1() + { + // Enter Per-Monitor V2 DPI context for the thread (best effort; no-op on older OS) + using var scope = new DpiAwarenessScope(s_dpiAwarenessContextPerMonitorAwareV2); + + using var form = new Form + { + StartPosition = FormStartPosition.Manual, + Location = new DrawingPoint(300, 300), + Size = new DrawingSize(500, 300) + }; + + var toolBar = new ToolBar + { + ShowToolTips = true, + Dock = DockStyle.None, + Location = new DrawingPoint(12, 12) + }; + + toolBar.Buttons.Add(new ToolBarButton("Btn") { ToolTipText = "Tip" }); + form.Controls.Add(toolBar); + + form.Show(); + Application.DoEvents(); + + var originalLocation = toolBar.Location; + + // Acquire native tooltip HWND + HWND tooltipHwnd = (HWND)PInvokeCore.SendMessage(toolBar, PInvoke.TB_GETTOOLTIPS); + Assert.NotEqual(HWND.Null, tooltipHwnd); + + // Force tooltip to (0,0) so TTN_SHOW reposition path is taken + PInvoke.SetWindowPos(tooltipHwnd, HWND.Null, 0, 0, 0, 0, + SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + + // Simulate hot item change so hotItem != -1 + var hot = new NMTBHOTITEM + { + hdr = new NMHDR { hwndFrom = (HWND)toolBar.Handle, idFrom = 0, code = TbnHotItemChange }, + idOld = -1, + idNew = 0, + dwFlags = HICF.ENTERING + }; + PInvokeCore.SendMessage(toolBar, MessageId.WM_REFLECT_NOTIFY, 0, ref hot); + + Application.DoEvents(); + + // Simulate TTN_SHOW and capture return value + var nmhdr = new NMHDR { hwndFrom = tooltipHwnd, idFrom = 0, code = PInvoke.TTN_SHOW }; + nint ret = PInvokeCore.SendMessage(toolBar, MessageId.WM_REFLECT_NOTIFY, 0, ref nmhdr); + + Application.DoEvents(); + + // Assertions: TTN_SHOW is handled (ret==1) and the toolbar itself does not move + Assert.Equal((nint)1, ret); + Assert.Equal(originalLocation, toolBar.Location); + + form.Close(); + } + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/TreeNodeTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/TreeNodeTests.cs new file mode 100644 index 00000000000..c1f942c25c5 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/TreeNodeTests.cs @@ -0,0 +1,137 @@ +namespace System.Windows.Forms.Tests +{ + public class TreeNodeTests + { + [StaFact] + public void Text_SetAndGet_ReturnsCorrectValue() + { + // Arrange + var treeNode = new TreeNode(); + var text = "Test Node"; + + // Act + treeNode.Text = text; + + // Assert + Assert.Equal(text, treeNode.Text); + } + + [StaFact] + public void Nodes_AddAndGet_ReturnsCorrectNodes() + { + // Arrange + var parentNode = new TreeNode(); + var childNode = new TreeNode("Child Node"); + + // Act + parentNode.Nodes.Add(childNode); + + // Assert + Assert.Single(parentNode.Nodes); + Assert.Same(childNode, parentNode.Nodes[0]); + } + + [StaFact] + public void Parent_SetAndGet_ReturnsCorrectParent() + { + // Arrange + var parentNode = new TreeNode("Parent Node"); + var childNode = new TreeNode("Child Node"); + + // Act + parentNode.Nodes.Add(childNode); + + // Assert + Assert.Same(parentNode, childNode.Parent); + } + + [StaFact] + public void Remove_RemovesNodeFromParent() + { + // Arrange + var parentNode = new TreeNode("Parent Node"); + var childNode = new TreeNode("Child Node"); + parentNode.Nodes.Add(childNode); + + // Act + parentNode.Nodes.Remove(childNode); + + // Assert + Assert.Empty(parentNode.Nodes); + Assert.Null(childNode.Parent); + } + + [StaFact] + public void TreeView_SetAndGet_ReturnsCorrectTreeView() + { + // Arrange + var treeView = new TreeView(); + var treeNode = new TreeNode("Test Node"); + + // Act + treeView.Nodes.Add(treeNode); + + // Assert + Assert.Same(treeView, treeNode.TreeView); + } + + // Commenting out this test, as it doesn't work in DAT + //[Test] + //public void ContextMenu_ShowsOnRightClick() + //{ + // // Arrange + // var form = new Form(); + // var treeView = new TreeView(); + // var treeNode = new TreeNode("Test Node"); + // var contextMenu = new ContextMenu(); + // contextMenu.MenuItems.Add(new MenuItem("Test Item")); + // treeNode.ContextMenu = contextMenu; + // treeView.Nodes.Add(treeNode); + // treeView.Bounds = new Rectangle(10, 10, 200, 200); + // form.Controls.Add(treeView); + + // bool contextMenuShown = false; + // contextMenu.Popup += (sender, e) => contextMenuShown = true; + + // // Ensure the Form and TreeView are created and visible + // form.Load += async (sender, e) => + // { + // // Need to wait for the form to be created and visible + // await Task.Delay(500); + + // // Get the bounds of the TreeNode + // var nodeBounds = treeNode.Bounds; + // var clickPointRelativeToTreeView = nodeBounds.Location + new Size(nodeBounds.Width / 2, nodeBounds.Y + nodeBounds.Height / 2); + // var clickPointRelativeToScreen = treeView.PointToScreen(clickPointRelativeToTreeView); + + // // Simulate right-click event on the TreeNode + // Cursor.Position = clickPointRelativeToScreen; + // MouseOperations.MouseEvent(MouseOperations.MouseEventFlags.RightDown, (uint)clickPointRelativeToScreen.X, (uint)clickPointRelativeToScreen.Y); + // await Task.Delay(100); + // MouseOperations.MouseEvent(MouseOperations.MouseEventFlags.RightUp, (uint)clickPointRelativeToScreen.X, (uint)clickPointRelativeToScreen.Y); + + // // Wait 1 second for the context menu to show + // await Task.Delay(1000); + + // var clickPointOutsideContextMenuRelative = new Point(50, 50); + // var clickPointOutsideContextMenuAbsolute = treeView.PointToScreen(clickPointOutsideContextMenuRelative); + + // Cursor.Position = clickPointOutsideContextMenuAbsolute; + // MouseOperations.MouseEvent(MouseOperations.MouseEventFlags.LeftDown, (uint)clickPointOutsideContextMenuAbsolute.X, (uint)clickPointOutsideContextMenuAbsolute.Y); + // await Task.Delay(100); + // MouseOperations.MouseEvent(MouseOperations.MouseEventFlags.LeftUp, (uint)clickPointOutsideContextMenuAbsolute.X, (uint)clickPointOutsideContextMenuAbsolute.Y); + + // // Wait 1 second for the context menu to close + // await Task.Delay(1000); + + // // Assert + // Assert.IsTrue(contextMenuShown); + // form.Close(); + // return; + // }; + + // // Show the form + // form.ShowDialog(); + //} + } +} diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/TreeViewTests.cs b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/TreeViewTests.cs new file mode 100644 index 00000000000..f9687b82f29 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Legacy.Tests/TreeViewTests.cs @@ -0,0 +1,92 @@ +namespace System.Windows.Forms.Tests +{ + public class TreeViewTests + { + [StaFact] + public void ContextMenu_SetAndGet_ReturnsCorrectValue() + { + // Arrange + var treeView = new TreeView(); + var contextMenu = new ContextMenu(); + + // Act + treeView.ContextMenu = contextMenu; + + // Assert + Assert.Same(contextMenu, treeView.ContextMenu); + } + + // Commenting out this test, as it doesn't work in DAT + //[Test] + //public void ContextMenu_ShowsOnRightClick() + //{ + // // Arrange + // var form = new Form(); + // var treeView = new TreeView(); + // var contextMenu = new ContextMenu(); + // contextMenu.MenuItems.Add(new MenuItem("Test Item")); + // treeView.ContextMenu = contextMenu; + // treeView.Bounds = new Rectangle(10, 10, 200, 200); + // form.Controls.Add(treeView); + + // bool contextMenuShown = false; + // contextMenu.Popup += (sender, e) => contextMenuShown = true; + + // // Ensure the Form and TreeView are created and visible + // form.Load += async (sender, e) => + // { + // // Wait for the tree view to be created + // await Task.Delay(500); + + // var clickPointRelativeToTreeView = treeView.Bounds.Location + new Size(treeView.Bounds.Width / 2, treeView.Bounds.Y + treeView.Bounds.Height / 2); + // var clickPointRelativeToScreen = treeView.PointToScreen(clickPointRelativeToTreeView); + + // // Simulate right-click event on the TreeNode + // Cursor.Position = clickPointRelativeToScreen; + // MouseOperations.MouseEvent(MouseOperations.MouseEventFlags.RightDown, (uint)clickPointRelativeToScreen.X, (uint)clickPointRelativeToScreen.Y); + // await Task.Delay(100); + // MouseOperations.MouseEvent(MouseOperations.MouseEventFlags.RightUp, (uint)clickPointRelativeToScreen.X, (uint)clickPointRelativeToScreen.Y); + + // // Wait 1 second for the context menu to show + // await Task.Delay(1000); + + // var clickPointOutsideContextMenuRelative = new Point(50, 50); + // var clickPointOutsideContextMenuAbsolute = treeView.PointToScreen(clickPointOutsideContextMenuRelative); + + // Cursor.Position = clickPointOutsideContextMenuAbsolute; + // MouseOperations.MouseEvent(MouseOperations.MouseEventFlags.LeftDown, (uint)clickPointOutsideContextMenuAbsolute.X, (uint)clickPointOutsideContextMenuAbsolute.Y); + // await Task.Delay(100); + // MouseOperations.MouseEvent(MouseOperations.MouseEventFlags.LeftUp, (uint)clickPointOutsideContextMenuAbsolute.X, (uint)clickPointOutsideContextMenuAbsolute.Y); + + // // Wait 1 second for the context menu to close + // await Task.Delay(1000); + + // // Assert + // Assert.IsTrue(contextMenuShown); + // form.Close(); + // return; + // }; + + // // Show the form + // form.ShowDialog(); + //} + + [StaFact] + public void ContextMenu_ContainsExpectedItems() + { + // Arrange + var treeView = new TreeView(); + var contextMenu = new ContextMenu(); + var menuItem = new MenuItem("Test Item"); + contextMenu.MenuItems.Add(menuItem); + treeView.ContextMenu = contextMenu; + + // Act + var items = treeView.ContextMenu.MenuItems; + + // Assert + Assert.Single(items); + Assert.Equal("Test Item", items[0].Text); + } + } +} diff --git a/src/System.Windows.Forms.Legacy/datagrid-port-plan.md b/src/System.Windows.Forms.Legacy/datagrid-port-plan.md new file mode 100644 index 00000000000..f0026584655 --- /dev/null +++ b/src/System.Windows.Forms.Legacy/datagrid-port-plan.md @@ -0,0 +1,198 @@ +# DataGrid Port Plan + +## Scope + +Source of truth for legacy implementations: + +`C:\git\github\WiseTechGlobal\CargoWise.Controls.WinForms.Legacy\src\System.Windows.Forms\src\System\Windows\Forms\` + +Target directory: + +`src\System.Windows.Forms\System\Windows\Forms\Controls\Unsupported\DataGrid\` + +This plan exists to port the legacy `DataGrid` family with the smallest practical amount of translation from the legacy source. + +## Working Rules + +Apply these rules to every file unless there is a strong reason not to: + +1. Start from the legacy implementation, not the current shim. +2. Convert to file-scoped `namespace System.Windows.Forms;` when the file can be converted cleanly. +3. Keep `#nullable disable` unless the file is actually nullable-clean. +4. Keep the .NET Foundation license header. +5. Keep `[Obsolete(Obsoletions.DataGridMessage, ...)]`, `[EditorBrowsable(EditorBrowsableState.Never)]`, and `[Browsable(false)]` on public unsupported types. +6. Do not do broad C# 13 modernization in the first pass. Get the file compiling first. +7. Keep split partial files only where they preserve the legacy structure cleanly. + +## Unsupported Compat Rule + +When current-repo helpers, interop wrappers, or utility APIs differ from the legacy source, prefer a small local `*Compat.cs` adapter in `Controls/Unsupported/DataGrid/`. + +That compat layer should adapt the current repo to the legacy code shape, not rewrite the legacy code to fit modern helpers. + +Use a local compat file when: + +- the mismatch is mechanical rather than architectural +- the need is specific to the Unsupported `DataGrid` port +- a narrow wrapper preserves the legacy flow with minimal translation +- changing shared infrastructure would broaden impact for little value + +Typical cases: + +- interop signatures that differ only by wrapper shape +- helper methods that moved or changed overload form +- small aliases needed to preserve near-verbatim legacy code + +Avoid: + +- changing shared interop just to make `DataGrid` compile +- rewriting large legacy call sites to match modern helper conventions +- spreading Unsupported-only compatibility concerns into shared layers + +Example: if legacy code expects `User32.ScrollWindow(..., ref RECT, ref RECT)` and the current repo exposes a different wrapper shape, add a narrow local compat adapter instead of changing shared interop. + +## Current Inventory + +Already correct enough to leave alone until the main family compiles: + +- `DataGridLineStyle.cs` +- `DataGridParentRowsLabel.cs` + +Not in scope: + +- `DataGridView.cs` and all `DataGridView.*.cs` files +- all `DataGridView*` column, cell, band, and related types + +## Port Order + +### Step 1: Small replacements + +Replace the current shims with the legacy implementations for: + +- `DataGridCell.cs` +- `IDataGridEditingService.cs` +- `GridTablesFactory.cs` + +Acceptance: each file compiles in isolation. + +### Step 2: Missing support files + +Add: + +- `DataGridAddNewRow.cs` +- `DataGridToolTip.cs` +- `DataGridState.cs` +- `DataGridCaption.cs` + +`DataGridState.cs` remains `internal`. + +Acceptance: all files compile without expanding the intended public API. + +### Step 3: Type converter and editor control + +Port: + +- `DataGridPreferredColumnWidthTypeConverter.cs` +- `DataGridTextBox.cs` + +Acceptance: `DataGridTextBox` is ready for `DataGridTextBoxColumn`. + +### Step 4: Column styles + +Replace and reconcile: + +- `DataGridColumnStyle.cs` +- `DataGridColumnStyle.CompModSwitches.cs` +- `DataGridColumnStyle.DataGridColumnHeaderAccessibleObject.cs` +- `GridColumnStylesCollection.cs` + +Acceptance: `DataGridBoolColumn` and `DataGridTextBoxColumn` can consume `DataGridColumnStyle` without compile errors. + +### Step 5: Column implementations + +Port: + +- `DataGridBoolColumn.cs` +- `DataGridTextBoxColumn.cs` + +Acceptance: both compile after Step 4. + +### Step 6: Table styles + +Port and reconcile: + +- `DataGridTableStyle.cs` +- `GridTableStylesCollection.cs` + +Acceptance: `DataGrid.cs` can consume the table-style graph. + +### Step 7: Internal row and rendering types + +Add: + +- `DataGridParentRows.cs` +- `DataGridRelationshipRow.cs` +- `DataGridRow.cs` + +These remain internal implementation types. + +Acceptance: row and layout types compile without changing public API. + +### Step 8: Main control + +Port `DataGrid.cs`. + +Also reconcile `HitTestInfo` and `HitTestType`, which currently exist as split shim files. + +Expect first-pass errors around: + +- missing `using` directives +- interop wrapper differences +- resource or `SRDescription` name drift +- moved accessibility APIs +- removed feature switches such as older accessibility-improvement toggles + +Acceptance: the project builds cleanly and focused tests pass. + +## Files To Restore Or Add + +Main public or surface-adjacent files: + +- `DataGrid.cs` +- `DataGridBoolColumn.cs` +- `DataGridCell.cs` +- `DataGridColumnStyle.cs` +- `DataGridPreferredColumnWidthTypeConverter.cs` +- `DataGridTableStyle.cs` +- `DataGridTextBox.cs` +- `DataGridTextBoxColumn.cs` +- `GridTablesFactory.cs` +- `IDataGridEditingService.cs` + +Missing support files: + +- `DataGridAddNewRow.cs` +- `DataGridCaption.cs` +- `DataGridColumnCollection.cs` +- `DataGridParentRows.cs` +- `DataGridRelationshipRow.cs` +- `DataGridRow.cs` +- `DataGridState.cs` +- `DataGridTableCollection.cs` +- `DataGridToolTip.cs` + +Split files to reconcile as part of the port: + +- `DataGridColumnStyle.CompModSwitches.cs` +- `DataGridColumnStyle.DataGridColumnHeaderAccessibleObject.cs` +- `GridColumnStylesCollection.cs` +- `GridTableStylesCollection.cs` +- `DataGrid.HitTestInfo.cs` +- `DataGrid.HitTestType.cs` + +## Deferred Follow-Up + +Do not block the initial port on broader cleanup. Leave these for a later pass unless they directly prevent the build: + +- resource and `SRDescription` metadata differences versus the legacy source +- deeper legacy-alignment issues that need broader review diff --git a/src/System.Windows.Forms.Legacy/unsupported-controls-support.md b/src/System.Windows.Forms.Legacy/unsupported-controls-support.md new file mode 100644 index 00000000000..f084323f2ac --- /dev/null +++ b/src/System.Windows.Forms.Legacy/unsupported-controls-support.md @@ -0,0 +1,431 @@ +# Supporting Legacy WinForms Controls + +## Purpose + +This document explains what would be required to support the old WinForms controls that were removed after .NET Core 3.0 and later reintroduced in this repository as binary-compatibility shims. + +The short version is: + +- The .NET `release/3.0` branch contained real implementations of these controls. +- This repository currently contains mostly hollow compatibility types for the same public surface. +- If the requirement is actual runtime support, the current `Unsupported` area is the wrong abstraction layer to build on incrementally. +- The lowest-risk path is to restore the 3.0 implementations and then adapt them to the current codebase, rather than trying to evolve the shims into working controls member-by-member. + +## Historical Baseline + +In the upstream `c:\git\github\dotnet\winforms` checkout on `release/3.0`, the legacy controls were still present as normal runtime implementations under: + +`src/System.Windows.Forms/src/System/Windows/Forms/` + +Representative examples from that branch include: + +- `ContextMenu.cs` +- `Menu.cs` +- `MenuItem.cs` +- `MainMenu.cs` +- `DataGrid.cs` +- `DataGridTableStyle.cs` +- `DataGridTextBoxColumn.cs` +- `StatusBar.cs` +- `StatusBarPanel.cs` +- `ToolBar.cs` +- `ToolBarButton.cs` + +Those files were not stubs. They contained state, rendering behavior, event wiring, message processing, layout logic, interop, and designer-facing metadata. + +## Current State In This Repository + +The current repository contains the old surface area under: + +`src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/` + +The families currently present are: + +- `ContextMenu/` +- `DataGrid/` +- `MainMenu/` +- `StatusBar/` +- `ToolBar/` + +The implementation model is documented in `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/readme.md`. + +That readme is explicit about the intent: + +- the types exist for binary compatibility +- they are not intended to be executed +- constructors throw `PlatformNotSupportedException` +- non-void members typically `throw null` +- void members do nothing +- inherited members are only reintroduced where metadata requires it + +This means the current code is designed to let old assemblies load, not to let old controls work. + +## Evidence That The Current Surface Is Hollow + +Representative examples in this repository: + +- `ContextMenu` throws from both public constructors and exposes no real event or display behavior. +- `DataGrid` throws from its constructor and exposes a large metadata-shaped surface with inert getters, setters, and events. +- `MainMenu`, `StatusBar`, and `ToolBar` follow the same pattern. +- `Obsoletions.cs` marks these APIs as binary compatibility only and points developers to modern replacements. + +There are also important host integration gaps: + +- `Control.ContextMenu` is currently just a compatibility property/event shell. +- `Form.Menu` and `Form.MergedMenu` are currently compatibility properties, not real menu plumbing. + +That difference matters more than the type definitions themselves. Even if the legacy controls were fully implemented, they would still not behave correctly until the owning `Control` and `Form` integration was restored. + +## Gap Versus The 3.0 Implementation + +The divergence from the 3.0 implementation is large, not cosmetic. + +Two representative comparisons: + +- `ContextMenu.cs`: the current shim is roughly a few dozen lines of metadata and throwing members, while the 3.0 implementation contained full popup, collapse, RTL, command-key, and rendering behavior. +- `DataGrid.cs`: a direct no-index diff against the upstream 3.0 file shows that the current version removes roughly ten thousand lines of implementation logic. + +This is the central architectural fact for any recovery plan: + +> We are not missing a few handlers or a few constructors. We are missing the actual control implementations. + +## Why The Existing Unsupported Folder Should Not Be Evolved In Place + +The local `Unsupported/readme.md` says the folder should not be modified. Even without that warning, the current design makes it a poor place to grow real support: + +- the code structure is intentionally metadata-first, not behavior-first +- the nullability mode is intentionally disabled for compatibility +- members are shaped to preserve reflection and JIT behavior, not runtime semantics +- the folder encodes an explicit product position: load old binaries, do not support the controls + +If the business requirement has changed from compatibility to support, that change should be reflected in the architecture instead of slowly weakening the shim model. + +## Recommended Strategy + +If actual support is required, use this strategy. + +### 1. Decide The Support Contract First + +Be explicit about which of these is required: + +- binary compatibility only +- runtime support for legacy applications +- designer support +- accessibility parity with current WinForms expectations +- long-term supported public API, or temporary migration bridge + +Without this decision, the implementation will drift between two incompatible goals. + +### 2. Treat `release/3.0` As The Behavioral Source Of Truth + +Use the upstream `release/3.0` implementation as the starting point for behavior, not the current `Unsupported` folder. + +That means importing or porting the 3.0 versions of: + +- `ContextMenu`, `Menu`, `MenuItem`, `MainMenu` +- `DataGrid` and its dependent types +- `StatusBar` and panel/event types +- `ToolBar` and button/event types + +The current shim files are still useful as a quick inventory of required public API and obsoletion metadata, but they are not a viable base for runtime work. + +### 3. Restore Host Integration At The Same Time + +Do not port the controls in isolation. The owning framework types must be wired back up in the same effort. + +At minimum this includes: + +- `Control.ContextMenu` +- `Control.ContextMenuChanged` +- `Control.OnContextMenuChanged` +- `Form.Menu` +- `Form.MergedMenu` +- `Form.MenuStart` +- `Form.MenuComplete` +- menu handle update logic and message processing paths that interact with legacy menus + +The upstream 3.0 branch already shows these members participating in runtime behavior. In this repository they are currently compatibility shells. + +### 4. Port By Control Family, Not By Individual File + +The safest sequence is: + +1. Menu stack: `Menu`, `MenuItem`, `ContextMenu`, `MainMenu`, plus `Control` and `Form` integration. +2. `StatusBar` family. +3. `ToolBar` family. +4. `DataGrid` family. + +This order is deliberate: + +- the menu stack has the smallest conceptual surface and exposes whether host integration is correct +- `StatusBar` and `ToolBar` are self-contained compared to `DataGrid` +- `DataGrid` is the highest-risk port because of its size, layout logic, binding behavior, and accessibility implications + +### 5. Preserve Public Shape, Reevaluate Product Semantics + +Once a family becomes truly supported, revisit whether these should still: + +- throw `PlatformNotSupportedException` +- remain hidden with `EditorBrowsable(Never)` +- remain marked with the current `WFDEV006` guidance +- remain described as binary compatibility only in docs and package metadata + +If the runtime behavior is restored but the warnings still tell users not to use the types, the product story becomes internally inconsistent. + +## Expected Porting Work + +Porting the 3.0 code is not a straight file copy. Expect adaptation work in these areas. + +### API And Type System Drift + +- current repository coding style is significantly newer than the 3.0 code style +- nullable reference types have evolved, but the legacy surface intentionally disables nullability in some areas +- helper APIs, interop wrappers, and internal utility names may have changed + +### Rendering And Platform Drift + +- current WinForms code has moved on in DPI handling, theming, and message processing +- any imported rendering path must be checked against the current visual style and DPI model +- old controls may assume older system metrics or menu handle behavior + +### Accessibility + +The original reason for removing these controls was not arbitrary. They lagged in accessibility and modern platform expectations. + +If support is required, decide whether the target is: + +- strict behavioral restoration of 3.0 behavior, or +- restoration plus modern accessibility fixes + +That is a product decision, not just an implementation detail. + +### Designer And Serialization + +Many of these types carry designer attributes and serializer-facing metadata. Runtime support without designer validation is incomplete for many WinForms applications. + +At minimum, validate: + +- designer load behavior +- property grid visibility and serialization +- event hookup serialization +- resource serialization +- form/menu merge scenarios + +### DataGrid Complexity + +`DataGrid` is the hardest family by far. + +Reasons: + +- very large implementation footprint +- custom layout and painting +- binding manager interactions +- editing service interactions +- accessibility subtypes +- table and column style object graph + +If schedule is tight, `DataGrid` should be scoped separately from the menu, `StatusBar`, and `ToolBar` work. + +## Suggested Execution Plan + +## Execution Checklist + +### Phase 1: Menu Stack + +- [x] Restore `release/3.0` `Menu.cs` into `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.cs`. +- [x] Restore `release/3.0` `MenuItem.cs` into `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuItem.cs`. +- [x] Restore `release/3.0` `ContextMenu.cs` into `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/ContextMenu.cs`. +- [x] Restore `release/3.0` `MainMenu.cs` into `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/MainMenu/MainMenu.cs`. +- [x] Remove the old split `Menu.MenuItemCollection.cs` shim because the real implementation now lives in `Menu.cs`. +- [x] Rewire `Control.ContextMenu`, `Control.ContextMenuChanged`, `Control.OnContextMenuChanged`, `Control.WmContextMenu`, and `Control.ProcessCmdKey`. +- [x] Rewire `Form.Menu`, `Form.MergedMenu`, `Form.ProcessCmdKey`, `Form.UpdateMenuHandles`, `WM_INITMENUPOPUP`, `WM_MENUCHAR`, and `WM_UNINITMENUPOPUP`. +- [ ] Run focused menu-stack behavior tests. +- [ ] Reconcile product semantics for the restored menu stack: obsolete warnings, IntelliSense visibility, and package messaging. + +### Phase 2: StatusBar + +- [x] Port `release/3.0` `StatusBar.cs`. +- [x] Port `release/3.0` `StatusBarDrawItemEventArgs.cs`. +- [x] Port `release/3.0` `StatusBarDrawItemEventHandler.cs`. +- [x] Port `release/3.0` `StatusBarPanel.cs`. +- [x] Port `release/3.0` `StatusBarPanelAutoSize.cs`. +- [x] Port `release/3.0` `StatusBarPanelBorderStyle.cs`. +- [x] Port `release/3.0` `StatusBarPanelClickEventArgs.cs`. +- [x] Port `release/3.0` `StatusBarPanelClickEventHandler.cs`. +- [x] Port `release/3.0` `StatusBarPanelStyle.cs`. +- [x] Collapse or replace the current split `StatusBar.StatusBarPanelCollection.cs` if the real collection implementation is restored inside `StatusBar.cs`. + +### Phase 3: ToolBar + +- [x] Port `release/3.0` `ToolBar.cs`. +- [x] Port `release/3.0` `ToolBarAppearance.cs`. +- [x] Port `release/3.0` `ToolBarButton.cs`. +- [x] Port `release/3.0` `ToolBarButtonClickEventArgs.cs`. +- [x] Port `release/3.0` `ToolBarButtonClickEventHandler.cs`. +- [x] Port `release/3.0` `ToolBarButtonStyle.cs`. +- [x] Port `release/3.0` `ToolBarTextAlign.cs`. +- [x] Collapse or replace the current split `ToolBar.ToolBarButtonCollection.cs` if the real collection implementation is restored inside `ToolBar.cs`. + +### Phase 4: DataGrid + +- [ ] Port `release/3.0` `DataGrid.cs`. +- [ ] Port `release/3.0` `DataGridAddNewRow.cs`. +- [ ] Port `release/3.0` `DataGridBoolColumn.cs`. +- [ ] Port `release/3.0` `DataGridCaption.cs`. +- [ ] Port `release/3.0` `DataGridCell.cs`. +- [ ] Port `release/3.0` `DataGridColumnCollection.cs`. +- [ ] Port `release/3.0` `DataGridColumnStyle.cs` and reconcile the current split `DataGridColumnStyle.CompModSwitches.cs` and `DataGridColumnStyle.DataGridColumnHeaderAccessibleObject.cs` layout. +- [ ] Port `release/3.0` `DataGridDefaultColumnWidthTypeConverter.cs` and reconcile the current `DataGridPreferredColumnWidthTypeConverter.cs` naming difference. +- [ ] Port `release/3.0` `DataGridLineStyle.cs`. +- [ ] Port `release/3.0` `DataGridParentRows.cs`. +- [ ] Port `release/3.0` `DataGridParentRowsLabel.cs`. +- [ ] Port `release/3.0` `DataGridRelationshipRow.cs`. +- [ ] Port `release/3.0` `DataGridRow.cs`. +- [ ] Port `release/3.0` `DataGridState.cs`. +- [ ] Port `release/3.0` `DataGridTableCollection.cs`. +- [ ] Port `release/3.0` `DataGridTableStyle.cs`. +- [ ] Port `release/3.0` `DataGridTextBox.cs`. +- [ ] Port `release/3.0` `DataGridTextBoxColumn.cs`. +- [ ] Port `release/3.0` `DataGridToolTip.cs`. +- [ ] Port `release/3.0` `GridTablesFactory.cs`. +- [ ] Port `release/3.0` `IDataGridEditingService.cs`. +- [ ] Add the currently missing supporting files under `Controls/Unsupported/DataGrid/` rather than continuing to expand the shim surface ad hoc. + +## File-By-File Port Map + +### Menu Stack + +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/Menu.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.cs` + Status: started + Note: the real `MenuItemCollection` now lives inside `Menu.cs`; the old split shim file should stay deleted. +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/MenuItem.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuItem.cs` + Status: started + Note: this also carries `MenuItemData` and `MdiListUserData` helper types. +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/ContextMenu.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/ContextMenu.cs` + Status: started +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/MainMenu.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/MainMenu/MainMenu.cs` + Status: started +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/MenuMerge.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuMerge.cs` + Status: pending review + Note: the local enum file already matches the required public shape closely and may not need replacement. +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/Control.cs` classic menu members -> `src/System.Windows.Forms/System/Windows/Forms/Control.cs` + Status: started + Scope: `ContextMenu`, `ContextMenuChanged`, `OnContextMenuChanged`, `ProcessCmdKey`, `WmContextMenu` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/Form.cs` classic menu members -> `src/System.Windows.Forms/System/Windows/Forms/Form.cs` + Status: started + Scope: `Menu`, `MergedMenu`, cached current menu state, `UpdateMenuHandles`, `ProcessCmdKey`, `WM_INITMENUPOPUP`, `WM_MENUCHAR`, `WM_UNINITMENUPOPUP` + +### StatusBar + +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/StatusBar.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBar.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/StatusBarDrawItemEventArgs.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarDrawItemEventArgs.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/StatusBarDrawItemEventHandler.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarDrawItemEventHandler.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/StatusBarPanel.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanel.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/StatusBarPanelAutoSize.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelAutoSize.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/StatusBarPanelBorderStyle.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelBorderStyle.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/StatusBarPanelClickEventArgs.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelClickEventArgs.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/StatusBarPanelClickEventHandler.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelClickEventHandler.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/StatusBarPanelStyle.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelStyle.cs` +- `release/3.0` embedded collection implementation in `StatusBar.cs` -> current split `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBar.StatusBarPanelCollection.cs` + Note: likely collapse this split once the real `StatusBar.cs` is ported. + +### ToolBar + +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/ToolBar.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBar.cs` + Status: completed +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/ToolBarAppearance.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarAppearance.cs` + Status: completed +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/ToolBarButton.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButton.cs` + Status: completed +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/ToolBarButtonClickEventArgs.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonClickEventArgs.cs` + Status: completed +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/ToolBarButtonClickEventHandler.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonClickEventHandler.cs` + Status: completed +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/ToolBarButtonStyle.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonStyle.cs` + Status: completed +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/ToolBarTextAlign.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarTextAlign.cs` + Status: completed +- `release/3.0` embedded collection implementation in `ToolBar.cs` -> current split `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBar.ToolBarButtonCollection.cs` + Status: completed + Note: the split shim has been removed because the runtime collection implementation now lives inside `ToolBar.cs`. + +### DataGrid + +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGrid.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridAddNewRow.cs` -> add new file under `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridBoolColumn.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridBoolColumn.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridCaption.cs` -> add new file under `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridCell.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridCell.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridColumnCollection.cs` -> add new file under `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridColumnStyle.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.cs` +- `release/3.0` supporting pieces inside `DataGridColumnStyle.cs` -> current split `DataGridColumnStyle.CompModSwitches.cs` and `DataGridColumnStyle.DataGridColumnHeaderAccessibleObject.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridDefaultColumnWidthTypeConverter.cs` -> reconcile with `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridPreferredColumnWidthTypeConverter.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridLineStyle.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridLineStyle.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridParentRows.cs` -> add new file under `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridParentRowsLabel.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridParentRowsLabel.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridRelationshipRow.cs` -> add new file under `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridRow.cs` -> add new file under `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridState.cs` -> add new file under `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridTableCollection.cs` -> add new file under `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridTableStyle.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTableStyle.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridTextBox.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTextBox.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridTextBoxColumn.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTextBoxColumn.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/DataGridToolTip.cs` -> add new file under `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/GridTablesFactory.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridTablesFactory.cs` +- `release/3.0` `src/System.Windows.Forms/src/System/Windows/Forms/IDataGridEditingService.cs` -> `src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/IDataGridEditingService.cs` + +### Phase 1: Source Import And Inventory + +- map every current shim type to its `release/3.0` implementation file +- identify supporting internal helpers required by each family +- decide whether to port by cherry-pick, manual copy, or staged diff application + +### Phase 2: Menu Stack Recovery + +- restore `Menu`, `MenuItem`, `ContextMenu`, and `MainMenu` +- restore `Control` and `Form` integration +- add smoke tests for popup display, merge behavior, and menu events + +### Phase 3: StatusBar And ToolBar Recovery + +- restore the runtime implementations +- validate layout, painting, events, image handling, and designer serialization + +### Phase 4: DataGrid Recovery + +- port the full family together +- add binding, editing, painting, and accessibility tests before considering the work stable + +### Phase 5: Product Cleanup + +- revisit `WFDEV006` messaging +- update package description and docs to match the actual support level +- decide whether any APIs should remain hidden from IntelliSense + +## Test Strategy + +Minimum test coverage should include: + +- construction and disposal of each restored control family +- Win32 handle creation and recreation +- keyboard and mouse interaction +- menu popup and collapse behavior +- form menu merge behavior +- status bar panel rendering and click events +- toolbar button rendering and click events +- `DataGrid` data binding, edit commit, and navigation +- designer serialization round-trip for representative scenarios +- accessibility smoke tests for each family + +## Practical Recommendation + +If the requirement is truly to support these old controls, do not continue from the current hollow implementation model. + +Instead: + +1. Keep the current shim code only as a temporary compatibility layer and API inventory. +2. Use upstream `release/3.0` as the behavioral baseline. +3. Restore one control family at a time, together with the owning `Control` and `Form` integration. +4. Treat `DataGrid` as a separate project with its own risk and test budget. + +That is the only approach that aligns the codebase with the actual requirement. Anything smaller will likely produce a half-working surface that compiles, loads, and still fails in real applications. \ No newline at end of file diff --git a/src/System.Windows.Forms.PrivateSourceGenerators/src/System.Windows.Forms.PrivateSourceGenerators.csproj b/src/System.Windows.Forms.PrivateSourceGenerators/src/System.Windows.Forms.PrivateSourceGenerators.csproj index edba9ee5458..08702b3d5bf 100644 --- a/src/System.Windows.Forms.PrivateSourceGenerators/src/System.Windows.Forms.PrivateSourceGenerators.csproj +++ b/src/System.Windows.Forms.PrivateSourceGenerators/src/System.Windows.Forms.PrivateSourceGenerators.csproj @@ -5,8 +5,14 @@ Preview enable true + $(DefaultItemExcludes);bin\**;obj\** + $(DefaultItemExcludesInProjectFolder);bin\**;obj\** + + + + diff --git a/src/System.Windows.Forms/System.Windows.Forms.csproj b/src/System.Windows.Forms/System.Windows.Forms.csproj index 7c7a3233561..d2d2da2bd86 100644 --- a/src/System.Windows.Forms/System.Windows.Forms.csproj +++ b/src/System.Windows.Forms/System.Windows.Forms.csproj @@ -20,6 +20,24 @@ true true ECMA + + + $([System.IO.Path]::GetFullPath('$(RepoRoot)Bin\$(OutDirName)\')) + true + true + WTG.System.Windows.Forms + WiseTech Global forked version of System.Windows.Forms that includes controls that were deprecated in .NET Core 3.1 and .NET 5, like DataGrid, Menu, ToolBar and StatusBar. + WiseTech Global + © WiseTech Global Limited. All rights reserved. diff --git a/src/System.Windows.Forms/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/System/Windows/Forms/Control.cs index 9253cce66ec..5c3e1259832 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Control.cs @@ -138,6 +138,7 @@ public unsafe partial class Control : private static readonly object s_enabledEvent = new(); private static readonly object s_dockEvent = new(); private static readonly object s_cursorEvent = new(); + private static readonly object s_contextMenuEvent = new(); private static readonly object s_contextMenuStripEvent = new(); private static readonly object s_causesValidationEvent = new(); private static readonly object s_regionChangedEvent = new(); @@ -211,6 +212,7 @@ public unsafe partial class Control : private static readonly int s_controlVersionInfoProperty = PropertyStore.CreateKey(); private static readonly int s_backgroundImageLayoutProperty = PropertyStore.CreateKey(); + private static readonly int s_contextMenuProperty = PropertyStore.CreateKey(); private static readonly int s_contextMenuStripProperty = PropertyStore.CreateKey(); private static readonly int s_autoScrollOffsetProperty = PropertyStore.CreateKey(); private static readonly int s_useCompatibleTextRenderingProperty = PropertyStore.CreateKey(); @@ -1687,6 +1689,10 @@ public static Font DefaultFont /// protected virtual Size DefaultSize => Size.Empty; + internal virtual bool HasMenu => false; + + private void DetachContextMenu(object? sender, EventArgs e) => ContextMenu = null; + private void DetachContextMenuStrip(object? sender, EventArgs e) => ContextMenuStrip = null; /// @@ -5405,11 +5411,11 @@ Site is { } site CreateParams cp = CreateParams; // We would need to get adornments metrics for both (old and new) Dpi in case application is in PerMonitorV2 mode and Dpi changed. - AdjustWindowRectExForControlDpi(ref adornmentsAfterDpiChange, (WINDOW_STYLE)cp.Style, bMenu: false, (WINDOW_EX_STYLE)cp.ExStyle); + AdjustWindowRectExForControlDpi(ref adornmentsAfterDpiChange, (WINDOW_STYLE)cp.Style, bMenu: HasMenu, (WINDOW_EX_STYLE)cp.ExStyle); if (OriginalDeviceDpiInternal != DeviceDpiInternal && OsVersion.IsWindows10_1703OrGreater()) { - AdjustWindowRectExForDpi(ref adornmentsBeforeDpiChange, (WINDOW_STYLE)cp.Style, bMenu: false, (WINDOW_EX_STYLE)cp.ExStyle, OriginalDeviceDpiInternal); + AdjustWindowRectExForDpi(ref adornmentsBeforeDpiChange, (WINDOW_STYLE)cp.Style, bMenu: HasMenu, (WINDOW_EX_STYLE)cp.ExStyle, OriginalDeviceDpiInternal); } else { @@ -8786,8 +8792,18 @@ internal static PreProcessControlState PreProcessControlMessageInternal(Control? /// key. Modifier keys include the SHIFT, CTRL, and ALT keys. /// /// - protected virtual bool ProcessCmdKey(ref Message msg, Keys keyData) => - _parent?.ProcessCmdKey(ref msg, keyData) ?? false; + protected virtual bool ProcessCmdKey(ref Message msg, Keys keyData) + { +#pragma warning disable WFDEV006 // Type or member is obsolete + if (Properties.TryGetValue(s_contextMenuProperty, out ContextMenu? contextMenu) + && contextMenu.ProcessCmdKey(ref msg, keyData, this)) + { + return true; + } +#pragma warning restore WFDEV006 + + return _parent?.ProcessCmdKey(ref msg, keyData) ?? false; + } private unsafe void PrintToMetaFile(HDC hDC, IntPtr lParam) { @@ -9803,7 +9819,7 @@ protected virtual void ScaleControl(SizeF factor, BoundsSpecified specified) { CreateParams cp = CreateParams; RECT adornments = default; - AdjustWindowRectExForControlDpi(ref adornments, (WINDOW_STYLE)cp.Style, false, (WINDOW_EX_STYLE)cp.ExStyle); + AdjustWindowRectExForControlDpi(ref adornments, (WINDOW_STYLE)cp.Style, HasMenu, (WINDOW_EX_STYLE)cp.ExStyle); Size minSize = MinimumSize; Size maxSize = MaximumSize; @@ -10248,7 +10264,7 @@ internal Size SizeFromClientSizeInternal(Size size) { RECT rect = new(size); CreateParams cp = CreateParams; - AdjustWindowRectExForControlDpi(ref rect, (WINDOW_STYLE)cp.Style, false, (WINDOW_EX_STYLE)cp.ExStyle); + AdjustWindowRectExForControlDpi(ref rect, (WINDOW_STYLE)cp.Style, HasMenu, (WINDOW_EX_STYLE)cp.ExStyle); return rect.Size; } @@ -11208,6 +11224,28 @@ internal virtual void WmContextMenu(ref Message m) /// internal void WmContextMenu(ref Message m, Control sourceControl) { + if (Properties.TryGetValue(s_contextMenuProperty, out ContextMenu? contextMenu)) + { + int legacyX = PARAM.SignedLOWORD(m.LParamInternal); + int legacyY = PARAM.SignedHIWORD(m.LParamInternal); + Point legacyClient = m.LParamInternal == -1 + ? new Point(Width / 2, Height / 2) + : PointToClient(new Point(legacyX, legacyY)); + + if (ClientRectangle.Contains(legacyClient)) + { +#pragma warning disable WFDEV006 // Type or member is obsolete + contextMenu.Show(sourceControl, legacyClient); +#pragma warning restore WFDEV006 + } + else + { + DefWndProc(ref m); + } + + return; + } + if (!Properties.TryGetValue(s_contextMenuStripProperty, out ContextMenuStrip? contextMenuStrip)) { DefWndProc(ref m); @@ -12958,52 +12996,53 @@ internal ToolStripControlHost? ToolStripControlHost // Unsupported types don't support nullability. #nullable disable - /// - /// This property is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.ContextMenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] + [SRCategory(nameof(SR.CatBehavior))] + [DefaultValue(null)] + [SRDescription(nameof(SR.ControlContextMenuDescr))] [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual ContextMenu ContextMenu { - get; - set; - } + get => Properties.GetValueOrDefault(s_contextMenuProperty); + set + { + ContextMenu oldValue = Properties.AddOrRemoveValue(s_contextMenuProperty, value); - /// - /// This event is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.ContextMenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] + if (oldValue == value) + { + return; + } - [EditorBrowsable(EditorBrowsableState.Never)] + if (oldValue is not null) + { + oldValue.Disposed -= DetachContextMenu; + } + + if (value is not null) + { + value.Disposed += DetachContextMenu; + } + + OnContextMenuChanged(EventArgs.Empty); + } + } + + [SRCategory(nameof(SR.CatPropertyChanged))] + [SRDescription(nameof(SR.ControlOnContextMenuChangedDescr))] [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public event EventHandler ContextMenuChanged { - add { } - remove { } + add => Events.AddHandler(s_contextMenuEvent, value); + remove => Events.RemoveHandler(s_contextMenuEvent, value); } - /// - /// This method is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.ContextMenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - protected virtual void OnContextMenuChanged(EventArgs e) { } + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void OnContextMenuChanged(EventArgs e) + { + if (Events[s_contextMenuEvent] is EventHandler eh) + { + eh(this, e); + } + } #nullable enable } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripTextBox.ToolStripTextBoxControl.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripTextBox.ToolStripTextBoxControl.cs index 4c378e10e48..0d425d6b440 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripTextBox.ToolStripTextBoxControl.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripTextBox.ToolStripTextBoxControl.cs @@ -30,7 +30,7 @@ private RECT AbsoluteClientRECT RECT rect = default; CreateParams cp = CreateParams; - AdjustWindowRectExForControlDpi(ref rect, (WINDOW_STYLE)cp.Style, false, (WINDOW_EX_STYLE)cp.ExStyle); + AdjustWindowRectExForControlDpi(ref rect, (WINDOW_STYLE)cp.Style, HasMenu, (WINDOW_EX_STYLE)cp.ExStyle); // the coordinates we get back are negative, we need to translate this back to positive. int offsetX = -rect.left; // one to get back to 0,0, another to translate diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeNode.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeNode.cs index d97e3d70b82..45f76766018 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeNode.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/TreeView/TreeNode.cs @@ -49,6 +49,9 @@ public partial class TreeNode : MarshalByRefObject, ICloneable, ISerializable private TreeNodeImageIndexer? _stateImageIndexer; private string _toolTipText = string.Empty; +#nullable disable + private ContextMenu _contextMenu; +#nullable enable private ContextMenuStrip? _contextMenuStrip; internal bool _nodesCleared; @@ -361,6 +364,23 @@ public bool Checked } } + /// + /// The context menu associated with this tree node. The context menu + /// will be shown when the user right clicks the mouse on the control. + /// +#pragma warning disable RS0016 + [SRCategory(nameof(SR.CatBehavior))] + [DefaultValue(null)] + [SRDescription(nameof(SR.ControlContextMenuDescr))] +#nullable disable + public virtual ContextMenu ContextMenu + { + get => _contextMenu; + set => _contextMenu = value; + } +#nullable enable +#pragma warning restore RS0016 + /// /// The associated with this tree node. This menu /// will be shown when the user right clicks the mouse on the control. @@ -1353,6 +1373,7 @@ public virtual object Clone() node.StateImageIndexer.Index = StateImageIndexer.Index; node.ToolTipText = _toolTipText; + node.ContextMenu = _contextMenu; node.ContextMenuStrip = _contextMenuStrip; // only set the key if it's set to something useful diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/ContextMenu.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/ContextMenu.cs index 8c73772752e..ad39d1d806d 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/ContextMenu.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/ContextMenu.cs @@ -1,60 +1,218 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable IDE1006, CS8625, CS8618, CS8601, RS0036 + using System.ComponentModel; +using System.Runtime.InteropServices; +using SafeNativeMethods = System.Windows.Forms.LegacyMenuSafeNativeMethods; -namespace System.Windows.Forms; - -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[DefaultEvent(nameof(Popup))] -[Obsolete( - Obsoletions.ContextMenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -public class ContextMenu : Menu +namespace System.Windows.Forms { - public ContextMenu() : base(items: default) => throw new PlatformNotSupportedException(); + /// + /// This class is used to put context menus on your form and show them for + /// controls at runtime. It basically acts like a regular Menu control, + /// but can be set for the ContextMenu property that most controls have. + /// + [DefaultEvent(nameof(Popup))] + public class ContextMenu : Menu + { + private EventHandler onPopup; + private EventHandler onCollapse; + internal Control sourceControl; - public ContextMenu(MenuItem[] menuItems) : base(items: menuItems) => throw new PlatformNotSupportedException(); + private RightToLeft rightToLeft = RightToLeft.Inherit; - [Localizable(true)] - [DefaultValue(RightToLeft.No)] - public virtual RightToLeft RightToLeft - { - get => throw null; - set { } - } + /// + /// Creates a new ContextMenu object with no items in it by default. + /// + public ContextMenu() + : base(null) + { + } - [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public Control SourceControl => throw null; + /// + /// Creates a ContextMenu object with the given MenuItems. + /// + public ContextMenu(MenuItem[] menuItems) + : base(menuItems) + { + } - public event EventHandler Popup - { - add { } - remove { } - } + /// + /// The last control that was acted upon that resulted in this context + /// menu being displayed. + /// + [ + Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) + ] + public Control SourceControl + { + get + { + return sourceControl; + } + } - public event EventHandler Collapse - { - add { } - remove { } - } + public event EventHandler Popup + { + add => onPopup += value; + remove => onPopup -= value; + } + + /// + /// Fires when the context menu collapses. + /// + public event EventHandler Collapse + { + add => onCollapse += value; + remove => onCollapse -= value; + } + + /// + /// This is used for international applications where the language + /// is written from RightToLeft. When this property is true, + /// text alignment and reading order will be from right to left. + /// + // Add a DefaultValue attribute so that the Reset context menu becomes + // available in the Property Grid but the default value remains No. + [ + Localizable(true), + DefaultValue(RightToLeft.No) + ] + public virtual RightToLeft RightToLeft + { + get + { + if (rightToLeft == RightToLeft.Inherit) + { + if (sourceControl is not null) + { + return ((Control)sourceControl).RightToLeft; + } + else + { + return RightToLeft.No; + } + } + else + { + return rightToLeft; + } + } + set + { + // valid values are 0x0 to 0x2. + if (value is < RightToLeft.No or > RightToLeft.Inherit) + { + throw new InvalidEnumArgumentException(nameof(RightToLeft), (int)value, typeof(RightToLeft)); + } - protected internal virtual void OnCollapse(EventArgs e) { } + if (RightToLeft != value) + { + rightToLeft = value; + UpdateRtl((value == RightToLeft.Yes)); + } + } + } - protected internal virtual void OnPopup(EventArgs e) { } + internal override bool RenderIsRightToLeft + { + get + { + return (rightToLeft == RightToLeft.Yes); + } + } - protected internal virtual bool ProcessCmdKey(ref Message msg, Keys keyData, Control control) => throw null; + /// + /// Fires the popup event + /// + protected internal virtual void OnPopup(EventArgs e) + { + onPopup?.Invoke(this, e); + } - public void Show(Control control, Point pos) { } + /// + /// Fires the collapse event + /// + protected internal virtual void OnCollapse(EventArgs e) + { + onCollapse?.Invoke(this, e); + } - public void Show(Control control, Point pos, LeftRightAlignment alignment) { } + protected internal virtual bool ProcessCmdKey(ref Message msg, Keys keyData, Control control) + { + sourceControl = control; + return ProcessCmdKey(ref msg, keyData); + } + + private void ResetRightToLeft() + { + RightToLeft = RightToLeft.No; + } + + /// + /// Returns true if the RightToLeft should be persisted in code gen. + /// + internal virtual bool ShouldSerializeRightToLeft() + { + if (rightToLeft == RightToLeft.Inherit) + { + return false; + } + + return true; + } + + /// + /// Displays the context menu at the specified position. This method + /// doesn't return until the menu is dismissed. + /// + public void Show(Control control, Point pos) + { + Show(control, pos, LegacyMenuNativeMethods.TPM_VERTICAL | LegacyMenuNativeMethods.TPM_RIGHTBUTTON); + } + + /// + /// Displays the context menu at the specified position. This method + /// doesn't return until the menu is dismissed. + /// + public void Show(Control control, Point pos, LeftRightAlignment alignment) + { + // This code below looks wrong but it's correct. + // WinForms Left alignment means we want the menu to show up left of the point it is invoked from. + // We specify TPM_RIGHTALIGN which tells win32 to align the right side of this + // menu with the point (which aligns it Left visually) + if (alignment == LeftRightAlignment.Left) + { + Show(control, pos, LegacyMenuNativeMethods.TPM_VERTICAL | LegacyMenuNativeMethods.TPM_RIGHTBUTTON | LegacyMenuNativeMethods.TPM_RIGHTALIGN); + } + else + { + Show(control, pos, LegacyMenuNativeMethods.TPM_VERTICAL | LegacyMenuNativeMethods.TPM_RIGHTBUTTON | LegacyMenuNativeMethods.TPM_LEFTALIGN); + } + } + + private void Show(Control control, Point pos, int flags) + { + ArgumentNullException.ThrowIfNull(control); + + if (!control.IsHandleCreated || !control.Visible) + { + throw new ArgumentException("The control must be visible and have a created handle.", nameof(control)); + } + + sourceControl = control; + + OnPopup(EventArgs.Empty); + pos = control.PointToScreen(pos); + SafeNativeMethods.TrackPopupMenuEx(new HandleRef(this, Handle), + flags, + pos.X, + pos.Y, + new HandleRef(control, control.Handle), + null); + } + } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/LegacyMenuInteropCompat.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/LegacyMenuInteropCompat.cs new file mode 100644 index 00000000000..d4de9250512 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/LegacyMenuInteropCompat.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE1006, SA1518 + +using System.Runtime.InteropServices; + +namespace System.Windows.Forms; + +internal static class LegacyMenuNativeMethods +{ + public const int MNC_EXECUTE = 2; + public const int MNC_SELECT = 3; + public const int MIIM_STATE = 0x00000001; + public const int MIIM_ID = 0x00000002; + public const int MIIM_SUBMENU = 0x00000004; + public const int MIIM_TYPE = 0x00000010; + public const int MIIM_DATA = 0x00000020; + public const int MF_BYPOSITION = 0x00000400; + public const int MFT_MENUBREAK = 0x00000040; + public const int MFT_SEPARATOR = 0x00000800; + public const int MFT_RIGHTORDER = 0x00002000; + public const int MFT_RIGHTJUSTIFY = 0x00004000; + public const int TPM_RIGHTBUTTON = 0x0002; + public const int TPM_LEFTALIGN = 0x0000; + public const int TPM_RIGHTALIGN = 0x0008; + public const int TPM_VERTICAL = 0x0040; + + [StructLayout(LayoutKind.Sequential)] + public class DRAWITEMSTRUCT + { + public int CtlType; + public int CtlID; + public int itemID; + public int itemAction; + public int itemState; + public IntPtr hwndItem; + public IntPtr hDC; + public RECT rcItem; + public IntPtr itemData; + } + + [StructLayout(LayoutKind.Sequential)] + public class MEASUREITEMSTRUCT + { + public int CtlType; + public int CtlID; + public int itemID; + public int itemWidth; + public int itemHeight; + public IntPtr itemData; + } + + [StructLayout(LayoutKind.Sequential)] + public class TPMPARAMS + { + public int cbSize = Marshal.SizeOf(); + public int rcExclude_left; + public int rcExclude_top; + public int rcExclude_right; + public int rcExclude_bottom; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class MENUITEMINFO_T + { + public int cbSize = Marshal.SizeOf(); + public int fMask; + public int fType; + public int fState; + public int wID; + public IntPtr hSubMenu; + public IntPtr hbmpChecked; + public IntPtr hbmpUnchecked; + public IntPtr dwItemData; + public string? dwTypeData; + public int cch; + } + + public static class Util + { + public static int MAKELONG(int low, int high) => (high << 16) | (low & 0xffff); + + public static int LOWORD(nint n) => PARAM.LOWORD(n); + } +} + +internal static class LegacyMenuUnsafeNativeMethods +{ + [DllImport(Libraries.User32, ExactSpelling = true)] + public static extern IntPtr CreatePopupMenu(); + + [DllImport(Libraries.User32, ExactSpelling = true)] + public static extern IntPtr CreateMenu(); + + [DllImport(Libraries.User32, ExactSpelling = true)] + public static extern bool DestroyMenu(HandleRef hMenu); + + [DllImport(Libraries.User32, ExactSpelling = true)] + public static extern int GetMenuItemCount(HandleRef hMenu); + + [DllImport(Libraries.User32, ExactSpelling = true)] + public static extern bool RemoveMenu(HandleRef hMenu, int uPosition, int uFlags); + + [DllImport(Libraries.User32, CharSet = CharSet.Auto)] + private static extern bool GetMenuItemInfo(IntPtr hMenu, int uItem, bool fByPosition, [In, Out] LegacyMenuNativeMethods.MENUITEMINFO_T lpmii); + + [DllImport(Libraries.User32, CharSet = CharSet.Auto)] + public static extern bool InsertMenuItem(HandleRef hMenu, int uItem, bool fByPosition, LegacyMenuNativeMethods.MENUITEMINFO_T lpmii); + + [DllImport(Libraries.User32, CharSet = CharSet.Auto)] + public static extern bool SetMenuItemInfo(HandleRef hMenu, int uItem, bool fByPosition, LegacyMenuNativeMethods.MENUITEMINFO_T lpmii); + + [DllImport(Libraries.User32, ExactSpelling = true)] + public static extern bool SetMenuDefaultItem(HandleRef hMenu, int uItem, bool fByPos); + + public static bool GetMenuItemInfo(HandleRef hMenu, int uItem, bool fByPosition, LegacyMenuNativeMethods.MENUITEMINFO_T lpmii) + { + bool result = GetMenuItemInfo(hMenu.Handle, uItem, fByPosition, lpmii); + GC.KeepAlive(hMenu.Wrapper); + return result; + } +} + +internal static class LegacyMenuSafeNativeMethods +{ + [DllImport(Libraries.User32, ExactSpelling = true, CharSet = CharSet.Auto)] + public static extern bool TrackPopupMenuEx(HandleRef hmenu, int fuFlags, int x, int y, HandleRef hwnd, LegacyMenuNativeMethods.TPMPARAMS? tpm); + + [DllImport(Libraries.User32, ExactSpelling = true, CharSet = CharSet.Auto)] + public static extern bool DrawMenuBar(HandleRef hWnd); + + [DllImport(Libraries.Gdi32, ExactSpelling = true)] + public static extern IntPtr SelectPalette(HandleRef hdc, HandleRef hpal, int bForceBkgd); +} \ No newline at end of file diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.MenuItemCollection.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.MenuItemCollection.cs deleted file mode 100644 index 25c431d90b3..00000000000 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.MenuItemCollection.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; -using System.ComponentModel; - -namespace System.Windows.Forms; - -#nullable disable - -public abstract partial class Menu -{ - /// - /// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.MenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] - [Browsable(false)] - [ListBindable(false)] - public class MenuItemCollection : IList - { - public MenuItemCollection(Menu owner) => throw new PlatformNotSupportedException(); - - public virtual MenuItem this[int index] => throw null; - - object IList.this[int index] - { - get => throw null; - set { } - } - - public virtual MenuItem this[string key] => throw null; - - public int Count => throw null; - - object ICollection.SyncRoot => throw null; - - bool ICollection.IsSynchronized => throw null; - - bool IList.IsFixedSize => throw null; - - public bool IsReadOnly => throw null; - - public virtual MenuItem Add(string caption) => throw null; - - public virtual MenuItem Add(string caption, EventHandler onClick) => throw null; - - public virtual MenuItem Add(string caption, MenuItem[] items) => throw null; - - public virtual int Add(MenuItem item) => throw null; - - public virtual int Add(int index, MenuItem item) => throw null; - - public virtual void AddRange(MenuItem[] items) { } - - int IList.Add(object value) => throw null; - - public bool Contains(MenuItem value) => throw null; - - bool IList.Contains(object value) => throw null; - - public virtual bool ContainsKey(string key) => throw null; - - public virtual void Clear() { } - - public void CopyTo(Array dest, int index) { } - - public MenuItem[] Find(string key, bool searchAllChildren) => throw null; - - public int IndexOf(MenuItem value) => throw null; - - public virtual int IndexOfKey(string key) => throw null; - - int IList.IndexOf(object value) => throw null; - - void IList.Insert(int index, object value) { } - - public IEnumerator GetEnumerator() => throw null; - - public virtual void RemoveAt(int index) { } - - public virtual void RemoveByKey(string key) { } - - public virtual void Remove(MenuItem item) { } - - void IList.Remove(object value) { } - } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.cs index 16a3d6d783e..f46d8bdc5ea 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/Menu.cs @@ -1,80 +1,1233 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable IDE1006, CS8769, CS8618, CS8603, CS8625, CS8600, CS8602, CS8604, RS0036, RS0016, CA2020 + +using System.Collections; using System.ComponentModel; +using System.Globalization; +using System.Runtime.InteropServices; +using UnsafeNativeMethods = System.Windows.Forms.LegacyMenuUnsafeNativeMethods; -namespace System.Windows.Forms; - -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.MenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[ListBindable(false)] -[ToolboxItemFilter("System.Windows.Forms")] -public abstract partial class Menu : Component +namespace System.Windows.Forms { - public const int FindHandle = 0; - public const int FindShortcut = 1; + /// + /// This is the base class for all menu components (MainMenu, MenuItem, and ContextMenu). + /// + [ToolboxItemFilter("System.Windows.Forms")] + [ListBindable(false)] + public abstract class Menu : Component + { + internal const int CHANGE_ITEMS = 0; // item(s) added or removed + internal const int CHANGE_VISIBLE = 1; // item(s) hidden or shown + internal const int CHANGE_MDI = 2; // mdi item changed + internal const int CHANGE_MERGE = 3; // mergeType or mergeOrder changed + internal const int CHANGE_ITEMADDED = 4; // mergeType or mergeOrder changed - protected Menu(MenuItem[] items) => throw new PlatformNotSupportedException(); + /// + /// Used by findMenuItem + /// + public const int FindHandle = 0; + /// + /// Used by findMenuItem + /// + public const int FindShortcut = 1; - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Advanced)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public IntPtr Handle => throw null; + private MenuItemCollection itemsCollection; + internal MenuItem[] items; + private int _itemCount; + internal IntPtr handle; + internal bool created; + private object userData; + private string name; - [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public virtual bool IsParent => throw null; + /// + /// This is an abstract class. Instances cannot be created, so the constructor + /// is only called from derived classes. + /// + protected Menu(MenuItem[] items) + { + if (items is not null) + { + MenuItems.AddRange(items); + } + } - [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public MenuItem MdiListItem => throw null; + /// + /// The HMENU handle corresponding to this menu. + /// + [ + Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) + ] + public IntPtr Handle + { + get + { + if (handle == IntPtr.Zero) + { + handle = CreateMenuHandle(); + } - [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public string Name - { - get => throw null; - set { } - } + CreateMenuItems(); + return handle; + } + } - [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] - [MergableProperty(false)] - public MenuItemCollection MenuItems => throw null; + /// + /// Specifies whether this menu contains any items. + /// + [ + Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) + ] + public virtual bool IsParent + { + get + { + return items is not null && ItemCount > 0; + } + } - [Localizable(false)] - [Bindable(true)] - [DefaultValue(null)] - [TypeConverter(typeof(StringConverter))] - public object Tag - { - get => throw null; - set { } - } + internal int ItemCount + { + get + { + return _itemCount; + } + } + + /// + /// The MenuItem that contains the list of MDI child windows. + /// + [ + Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) + ] + public MenuItem MdiListItem + { + get + { + for (int i = 0; i < ItemCount; i++) + { + MenuItem item = items[i]; + if (item.MdiList) + { + return item; + } + + if (item.IsParent) + { + item = item.MdiListItem; + if (item is not null) + { + return item; + } + } + } + + return null; + } + } + + /// + /// Name of this control. The designer will set this to the same + /// as the programatic Id "(name)" of the control - however this + /// property has no bearing on the runtime aspects of this control. + /// + [ + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), + Browsable(false) + ] + public string Name + { + get + { + return WindowsFormsUtils.GetComponentName(this, name); + } + set + { + if (value is null || value.Length == 0) + { + name = null; + } + else + { + name = value; + } + + Site?.Name = name; + } + } + + [ + Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Content), + MergableProperty(false) + ] + public MenuItemCollection MenuItems + { + get + { + itemsCollection ??= new MenuItemCollection(this); + return itemsCollection; + } + } + + internal virtual bool RenderIsRightToLeft + { + get + { + Debug.Assert(true, "Should never get called"); + return false; + } + } + + [ + SRCategory(nameof(SR.CatData)), + Localizable(false), + Bindable(true), + SRDescription(nameof(SR.ControlTagDescr)), + DefaultValue(null), + TypeConverter(typeof(StringConverter)), + ] + public object Tag + { + get + { + return userData; + } + set + { + userData = value; + } + } + + /// + /// Notifies Menu that someone called Windows.DeleteMenu on its handle. + /// + internal void ClearHandles() + { + if (handle != IntPtr.Zero) + { + UnsafeNativeMethods.DestroyMenu(new HandleRef(this, handle)); + } + + handle = IntPtr.Zero; + if (created) + { + for (int i = 0; i < ItemCount; i++) + { + items[i].ClearHandles(); + } + + created = false; + } + } + + /// + /// Sets this menu to be an identical copy of another menu. + /// + protected internal void CloneMenu(Menu menuSrc) + { + ArgumentNullException.ThrowIfNull(menuSrc); + + MenuItem[] newItems = null; + if (menuSrc.items is not null) + { + int count = menuSrc.MenuItems.Count; + newItems = new MenuItem[count]; + for (int i = 0; i < count; i++) + { + newItems[i] = menuSrc.MenuItems[i].CloneMenu(); + } + } + + MenuItems.Clear(); + if (newItems is not null) + { + MenuItems.AddRange(newItems); + } + } + + protected virtual IntPtr CreateMenuHandle() + { + return UnsafeNativeMethods.CreatePopupMenu(); + } + + internal void CreateMenuItems() + { + if (!created) + { + for (int i = 0; i < ItemCount; i++) + { + items[i].CreateMenuItem(); + } + + created = true; + } + } + + internal void DestroyMenuItems() + { + if (created) + { + for (int i = 0; i < ItemCount; i++) + { + items[i].ClearHandles(); + } + + while (UnsafeNativeMethods.GetMenuItemCount(new HandleRef(this, handle)) > 0) + { + UnsafeNativeMethods.RemoveMenu(new HandleRef(this, handle), 0, LegacyMenuNativeMethods.MF_BYPOSITION); + } + + created = false; + } + } + + /// + /// Disposes of the component. Call dispose when the component is no longer needed. + /// This method removes the component from its container (if the component has a site) + /// and triggers the dispose event. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + while (ItemCount > 0) + { + MenuItem item = items[--_itemCount]; + + // remove the item before we dispose it so it still has valid state + // for undo/redo + // + if (item.Site is not null && item.Site.Container is not null) + { + item.Site.Container.Remove(item); + } + + item.Parent = null; + item.Dispose(); + } + + items = null; + } + + if (handle != IntPtr.Zero) + { + UnsafeNativeMethods.DestroyMenu(new HandleRef(this, handle)); + handle = IntPtr.Zero; + if (disposing) + { + ClearHandles(); + } + } + + base.Dispose(disposing); + } + + public MenuItem FindMenuItem(int type, IntPtr value) + { + for (int i = 0; i < ItemCount; i++) + { + MenuItem item = items[i]; + switch (type) + { + case FindHandle: + if (item.handle == value) + { + return item; + } + + break; + case FindShortcut: + if (item.Shortcut == (Shortcut)(int)value) + { + return item; + } + + break; + } + + item = item.FindMenuItem(type, value); + if (item is not null) + { + return item; + } + } + + return null; + } + + protected int FindMergePosition(int mergeOrder) + { + int iMin, iLim, iT; + + for (iMin = 0, iLim = ItemCount; iMin < iLim;) + { + iT = (iMin + iLim) / 2; + if (items[iT].MergeOrder <= mergeOrder) + { + iMin = iT + 1; + } + else + { + iLim = iT; + } + } + + return iMin; + } + + // A new method for finding the approximate merge position. The original + // method assumed (incorrectly) that the MergeOrder of the target menu would be sequential + // as it's guaranteed to be in the MDI imlementation of merging container and child + // menus. However, user code can call MergeMenu independently on a source and target + // menu whose MergeOrder values are not necessarily pre-sorted. + internal int xFindMergePosition(int mergeOrder) + { + int nPosition = 0; + + // Iterate from beginning to end since we can't assume any sequential ordering to MergeOrder + for (int nLoop = 0; nLoop < ItemCount; nLoop++) + { + if (items[nLoop].MergeOrder > mergeOrder) + { + // We didn't find what we're looking for, but we've found a stopping point. + break; + } + else if (items[nLoop].MergeOrder < mergeOrder) + { + // We might have found what we're looking for, but we'll have to come around again + // to know. + nPosition = nLoop + 1; + } + else if (mergeOrder == items[nLoop].MergeOrder) + { + // We've found what we're looking for, so use this value for the merge order + nPosition = nLoop; + break; + } + } + + return nPosition; + } + + // There's a win32 problem that doesn't allow menus to cascade right to left + // unless we explicitely set the bit on the menu the first time it pops up + internal void UpdateRtl(bool setRightToLeftBit) + { + foreach (MenuItem item in MenuItems) + { + item.UpdateItemRtl(setRightToLeftBit); + item.UpdateRtl(setRightToLeftBit); + } + } + + /// + /// Returns the ContextMenu that contains this menu. The ContextMenu + /// is at the top of this menu's parent chain. + /// Returns null if this menu is not contained in a ContextMenu. + /// This can occur if it's contained in a MainMenu or if it isn't + /// currently contained in any menu at all. + /// + public ContextMenu GetContextMenu() + { + Menu menuT; + for (menuT = this; menuT is not ContextMenu;) + { + if (menuT is not MenuItem) + { + return null; + } + + menuT = ((MenuItem)menuT).Parent; + } + + return (ContextMenu)menuT; + } + + /// + /// Returns the MainMenu item that contains this menu. The MainMenu + /// is at the top of this menu's parent chain. + /// Returns null if this menu is not contained in a MainMenu. + /// This can occur if it's contained in a ContextMenu or if it isn't + /// currently contained in any menu at all. + /// + public MainMenu GetMainMenu() + { + Menu menuT; + for (menuT = this; menuT is not MainMenu;) + { + if (menuT is not MenuItem) + { + return null; + } + + menuT = ((MenuItem)menuT).Parent; + } + + return (MainMenu)menuT; + } + + internal virtual void ItemsChanged(int change) + { + switch (change) + { + case CHANGE_ITEMS: + case CHANGE_VISIBLE: + DestroyMenuItems(); + break; + } + } + + /// + /// Walks the menu item collection, using a caller-supplied delegate to find one + /// with a matching access key. Walk starts at specified item index and performs one + /// full pass of the entire collection, looping back to the top if necessary. + /// + /// Return value is intended for return from WM_MENUCHAR message. It includes both + /// index of matching item, and action for OS to take (execute or select). Zero is + /// used to indicate that no match was found (OS should ignore key and beep). + /// + private IntPtr MatchKeyToMenuItem(int startItem, char key, MenuItemKeyComparer comparer) + { + int firstMatch = -1; + bool multipleMatches = false; + + for (int i = 0; i < items.Length && !multipleMatches; ++i) + { + int itemIndex = (startItem + i) % items.Length; + MenuItem mi = items[itemIndex]; + if (mi is not null && comparer(mi, key)) + { + if (firstMatch < 0) + { + // Using Index doesnt respect hidden items. + firstMatch = mi.MenuIndex; + } + else + { + multipleMatches = true; + } + } + } + + if (firstMatch < 0) + { + return IntPtr.Zero; + } + + int action = multipleMatches ? LegacyMenuNativeMethods.MNC_SELECT : LegacyMenuNativeMethods.MNC_EXECUTE; + + return (IntPtr)LegacyMenuNativeMethods.Util.MAKELONG(firstMatch, action); + } + + /// Delegate type used by MatchKeyToMenuItem + private delegate bool MenuItemKeyComparer(MenuItem mi, char key); + + /// + /// Merges another menu's items with this one's. Menu items are merged according to their + /// mergeType and mergeOrder properties. This function is typically used to + /// merge an MDI container's menu with that of its active MDI child. + /// + public virtual void MergeMenu(Menu menuSrc) + { + ArgumentNullException.ThrowIfNull(menuSrc); + if (menuSrc == this) + { + throw new ArgumentException("A menu cannot be merged with itself.", nameof(menuSrc)); + } + + int i, j; + MenuItem item; + MenuItem itemDst; + + if (menuSrc.items is not null && items is null) + { + MenuItems.Clear(); + } + + for (i = 0; i < menuSrc.ItemCount; i++) + { + item = menuSrc.items[i]; + + switch (item.MergeType) + { + default: + continue; + case MenuMerge.Add: + MenuItems.Add(FindMergePosition(item.MergeOrder), item.MergeMenu()); + continue; + case MenuMerge.Replace: + case MenuMerge.MergeItems: + break; + } + + int mergeOrder = item.MergeOrder; + // Can we find a menu item with a matching merge order? + // Use new method to find the approximate merge position. The original + // method assumed (incorrectly) that the MergeOrder of the target menu would be sequential + // as it's guaranteed to be in the MDI imlementation of merging container and child + // menus. However, user code can call MergeMenu independently on a source and target + // menu whose MergeOrder values are not necessarily pre-sorted. + for (j = xFindMergePosition(mergeOrder); ; j++) + { + if (j >= ItemCount) + { + // A matching merge position could not be found, + // so simply append this menu item to the end. + MenuItems.Add(j, item.MergeMenu()); + break; + } + + itemDst = items[j]; + if (itemDst.MergeOrder != mergeOrder) + { + MenuItems.Add(j, item.MergeMenu()); + break; + } + + if (itemDst.MergeType != MenuMerge.Add) + { + if (item.MergeType != MenuMerge.MergeItems + || itemDst.MergeType != MenuMerge.MergeItems) + { + itemDst.Dispose(); + MenuItems.Add(j, item.MergeMenu()); + } + else + { + itemDst.MergeMenu(item); + } + + break; + } + } + } + } + + internal virtual bool ProcessInitMenuPopup(IntPtr handle) + { + MenuItem item = FindMenuItem(FindHandle, handle); + if (item is not null) + { + item.OnInitMenuPopup(EventArgs.Empty); + item.CreateMenuItems(); + return true; + } + + return false; + } + + protected internal virtual bool ProcessCmdKey(ref Message msg, Keys keyData) + { + MenuItem item = FindMenuItem(FindShortcut, (IntPtr)(int)keyData); + return item is not null && item.ShortcutClick(); + } + + /// + /// Returns index of currently selected menu item in + /// this menu, or -1 if no item is currently selected. + /// + internal int SelectedMenuItemIndex + { + get + { + for (int i = 0; i < items.Length; ++i) + { + MenuItem mi = items[i]; + if (mi is not null && mi.Selected) + { + return i; + } + } + + return -1; + } + } - protected internal void CloneMenu(Menu menuSrc) { } + /// + /// Returns a string representation for this control. + /// + public override string ToString() + { + string s = base.ToString(); + return s + ", Items.Count: " + ItemCount.ToString(CultureInfo.CurrentCulture); + } - protected virtual IntPtr CreateMenuHandle() => throw null; + /// + /// Handles the WM_MENUCHAR message, forwarding it to the intended Menu + /// object. All the real work is done inside WmMenuCharInternal(). + /// + internal void WmMenuChar(ref Message m) + { + Menu menu = (m.LParamInternal == handle) ? this : FindMenuItem(FindHandle, m.LParamInternal); - public MenuItem FindMenuItem(int type, IntPtr value) => throw null; + if (menu is null) + { + return; + } - protected int FindMergePosition(int mergeOrder) => throw null; + char menuKey = char.ToUpper((char)LegacyMenuNativeMethods.Util.LOWORD((nint)m.WParamInternal), CultureInfo.CurrentCulture); - public ContextMenu GetContextMenu() => throw null; + m.ResultInternal = (LRESULT)menu.WmMenuCharInternal(menuKey); + } - public MainMenu GetMainMenu() => throw null; + /// + /// Handles WM_MENUCHAR to provide access key support for owner-draw menu items (which + /// means *all* menu items on a menu when IsImageMarginPresent == true). Attempts to + /// simulate the exact behavior that the OS provides for non owner-draw menu items. + /// + internal IntPtr WmMenuCharInternal(char key) + { + // Start looking just beyond the current selected item (otherwise just start at the top) + int startItem = (SelectedMenuItemIndex + 1) % items.Length; - public virtual void MergeMenu(Menu menuSrc) { } + // First, search for match among owner-draw items with explicitly defined access keys (eg. "S&ave") + IntPtr result = MatchKeyToMenuItem(startItem, key, new MenuItemKeyComparer(CheckOwnerDrawItemWithMnemonic)); - protected internal virtual bool ProcessCmdKey(ref Message msg, Keys keyData) => throw null; + // Next, search for match among owner-draw items with no access keys (looking at first char of item text) + if (result == IntPtr.Zero) + { + result = MatchKeyToMenuItem(startItem, key, new MenuItemKeyComparer(CheckOwnerDrawItemNoMnemonic)); + } + + return result; + } + + /// MenuItemKeyComparer delegate used by WmMenuCharInternal + private bool CheckOwnerDrawItemWithMnemonic(MenuItem mi, char key) + { + return mi.OwnerDraw && + mi.Mnemonic == key; + } + + /// MenuItemKeyComparer delegate used by WmMenuCharInternal + private bool CheckOwnerDrawItemNoMnemonic(MenuItem mi, char key) + { + return mi.OwnerDraw && + mi.Mnemonic == 0 && + mi.Text.Length > 0 && + char.ToUpper(mi.Text[0], CultureInfo.CurrentCulture) == key; + } + + [ListBindable(false)] + public class MenuItemCollection : IList + { + private readonly Menu owner; + + /// A caching mechanism for key accessor + /// We use an index here rather than control so that we don't have lifetime + /// issues by holding on to extra references. + private int lastAccessedIndex = -1; + + public MenuItemCollection(Menu owner) + { + this.owner = owner; + } + + public virtual MenuItem this[int index] + { + get + { + if (index < 0 || index >= owner.ItemCount) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + return owner.items[index]; + } + + // set not supported + } + + object IList.this[int index] + { + get + { + return this[index]; + } + set + { + throw new NotSupportedException(); + } + } + + /// + /// Retrieves the child control with the specified key. + /// + public virtual MenuItem this[string key] + { + get + { + // We do not support null and empty string as valid keys. + if (string.IsNullOrEmpty(key)) + { + return null; + } + + // Search for the key in our collection + int index = IndexOfKey(key); + if (IsValidIndex(index)) + { + return this[index]; + } + else + { + return null; + } + } + } + + public int Count + { + get + { + return owner.ItemCount; + } + } + + object ICollection.SyncRoot + { + get + { + return this; + } + } + + bool ICollection.IsSynchronized + { + get + { + return false; + } + } + + bool IList.IsFixedSize + { + get + { + return false; + } + } + + public bool IsReadOnly + { + get + { + return false; + } + } + + /// + /// Adds a new MenuItem to the end of this menu with the specified caption. + /// + public virtual MenuItem Add(string caption) + { + MenuItem item = new MenuItem(caption); + Add(item); + return item; + } + + /// + /// Adds a new MenuItem to the end of this menu with the specified caption, + /// and click handler. + /// + public virtual MenuItem Add(string caption, EventHandler onClick) + { + MenuItem item = new MenuItem(caption, onClick); + Add(item); + return item; + } + + /// + /// Adds a new MenuItem to the end of this menu with the specified caption, + /// click handler, and items. + /// + public virtual MenuItem Add(string caption, MenuItem[] items) + { + MenuItem item = new MenuItem(caption, items); + Add(item); + return item; + } + + /// + /// Adds a MenuItem to the end of this menu + /// MenuItems can only be contained in one menu at a time, and may not be added + /// more than once to the same menu. + /// + public virtual int Add(MenuItem item) + { + return Add(owner.ItemCount, item); + } + + /// + /// Adds a MenuItem to this menu at the specified index. The item currently at + /// that index, and all items after it, will be moved up one slot. + /// MenuItems can only be contained in one menu at a time, and may not be added + /// more than once to the same menu. + /// + public virtual int Add(int index, MenuItem item) + { + ArgumentNullException.ThrowIfNull(item); + + // MenuItems can only belong to one menu at a time + if (item.Parent is not null) + { + // First check that we're not adding ourself, i.e. walk + // the parent chain for equality + if (owner is MenuItem parent) + { + while (parent is not null) + { + if (parent.Equals(item)) + { + throw new ArgumentException($"The menu item '{item.Text}' already exists in this menu hierarchy.", nameof(item)); + } + + if (parent.Parent is MenuItem) + { + parent = (MenuItem)parent.Parent; + } + else + { + break; + } + } + } + + // if we're re-adding an item back to the same collection + // the target index needs to be decremented since we're + // removing an item from the collection + if (item.Parent.Equals(owner) && index > 0) + { + index--; + } + + item.Parent.MenuItems.Remove(item); + } + + // Validate our index + if (index < 0 || index > owner.ItemCount) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + if (owner.items is null || owner.items.Length == owner.ItemCount) + { + MenuItem[] newItems = new MenuItem[owner.ItemCount < 2 ? 4 : owner.ItemCount * 2]; + if (owner.ItemCount > 0) + { + Array.Copy(owner.items, 0, newItems, 0, owner.ItemCount); + } + + owner.items = newItems; + } + + Array.Copy(owner.items, index, owner.items, index + 1, owner.ItemCount - index); + owner.items[index] = item; + owner._itemCount++; + item.Parent = owner; + owner.ItemsChanged(CHANGE_ITEMS); + if (owner is MenuItem) + { + ((MenuItem)owner).ItemsChanged(CHANGE_ITEMADDED, item); + } + + return index; + } + + public virtual void AddRange(MenuItem[] items) + { + ArgumentNullException.ThrowIfNull(items); + foreach (MenuItem item in items) + { + Add(item); + } + } + + int IList.Add(object value) + { + if (value is MenuItem) + { + return Add((MenuItem)value); + } + else + { + throw new ArgumentException("Value must be a MenuItem.", nameof(value)); + } + } + + public bool Contains(MenuItem value) + { + return IndexOf(value) != -1; + } + + bool IList.Contains(object value) + { + if (value is MenuItem) + { + return Contains((MenuItem)value); + } + else + { + return false; + } + } + + /// + /// Returns true if the collection contains an item with the specified key, false otherwise. + /// + public virtual bool ContainsKey(string key) + { + return IsValidIndex(IndexOfKey(key)); + } + + /// + /// Searches for Controls by their Name property, builds up an array + /// of all the controls that match. + /// + public MenuItem[] Find(string key, bool searchAllChildren) + { + if ((key is null) || (key.Length == 0)) + { + throw new ArgumentNullException(nameof(key), SR.FindKeyMayNotBeEmptyOrNull); + } + + ArrayList foundMenuItems = FindInternal(key, searchAllChildren, this, new ArrayList()); + + // Make this a stongly typed collection. + MenuItem[] stronglyTypedfoundMenuItems = new MenuItem[foundMenuItems.Count]; + foundMenuItems.CopyTo(stronglyTypedfoundMenuItems, 0); + + return stronglyTypedfoundMenuItems; + } + + /// + /// Searches for Controls by their Name property, builds up an array list + /// of all the controls that match. + /// + private static ArrayList FindInternal(string key, bool searchAllChildren, MenuItemCollection menuItemsToLookIn, ArrayList foundMenuItems) + { + if ((menuItemsToLookIn is null) || (foundMenuItems is null)) + { + return null; // + } + + // Perform breadth first search - as it's likely people will want controls belonging + // to the same parent close to each other. + + for (int i = 0; i < menuItemsToLookIn.Count; i++) + { + if (menuItemsToLookIn[i] is null) + { + continue; + } + + if (WindowsFormsUtils.SafeCompareStrings(menuItemsToLookIn[i].Name, key, /* ignoreCase = */ true)) + { + foundMenuItems.Add(menuItemsToLookIn[i]); + } + } + + // Optional recurive search for controls in child collections. + + if (searchAllChildren) + { + for (int i = 0; i < menuItemsToLookIn.Count; i++) + { + if (menuItemsToLookIn[i] is null) + { + continue; + } + + if ((menuItemsToLookIn[i].MenuItems is not null) && menuItemsToLookIn[i].MenuItems.Count > 0) + { + // if it has a valid child collecion, append those results to our collection + foundMenuItems = FindInternal(key, searchAllChildren, menuItemsToLookIn[i].MenuItems, foundMenuItems); + } + } + } + + return foundMenuItems; + } + + public int IndexOf(MenuItem value) + { + for (int index = 0; index < Count; ++index) + { + if (this[index] == value) + { + return index; + } + } + + return -1; + } + + int IList.IndexOf(object value) + { + if (value is MenuItem) + { + return IndexOf((MenuItem)value); + } + else + { + return -1; + } + } + + /// + /// The zero-based index of the first occurrence of value within the entire CollectionBase, if found; otherwise, -1. + /// + public virtual int IndexOfKey(string key) + { + // Step 0 - Arg validation + if (string.IsNullOrEmpty(key)) + { + return -1; // we dont support empty or null keys. + } + + // step 1 - check the last cached item + if (IsValidIndex(lastAccessedIndex)) + { + if (WindowsFormsUtils.SafeCompareStrings(this[lastAccessedIndex].Name, key, /* ignoreCase = */ true)) + { + return lastAccessedIndex; + } + } + + // step 2 - search for the item + for (int i = 0; i < Count; i++) + { + if (WindowsFormsUtils.SafeCompareStrings(this[i].Name, key, /* ignoreCase = */ true)) + { + lastAccessedIndex = i; + return i; + } + } + + // step 3 - we didn't find it. Invalidate the last accessed index and return -1. + lastAccessedIndex = -1; + return -1; + } + + void IList.Insert(int index, object value) + { + if (value is MenuItem) + { + Add(index, (MenuItem)value); + } + else + { + throw new ArgumentException("Value must be a MenuItem.", nameof(value)); + } + } + + /// + /// Determines if the index is valid for the collection. + /// + private bool IsValidIndex(int index) + { + return ((index >= 0) && (index < Count)); + } + + /// + /// Removes all existing MenuItems from this menu + /// + public virtual void Clear() + { + if (owner.ItemCount > 0) + { + for (int i = 0; i < owner.ItemCount; i++) + { + owner.items[i].Parent = null; + } + + owner._itemCount = 0; + owner.items = null; + + owner.ItemsChanged(CHANGE_ITEMS); + + if (owner is MenuItem) + { + ((MenuItem)(owner)).UpdateMenuItem(true); + } + } + } + + public void CopyTo(Array dest, int index) + { + if (owner.ItemCount > 0) + { + Array.Copy(owner.items, 0, dest, index, owner.ItemCount); + } + } + + public IEnumerator GetEnumerator() + { + return new ArraySubsetEnumerator(owner.items, owner.ItemCount); + } + + /// + /// Removes the item at the specified index in this menu. All subsequent + /// items are moved up one slot. + /// + public virtual void RemoveAt(int index) + { + if (index < 0 || index >= owner.ItemCount) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + MenuItem item = owner.items[index]; + item.Parent = null; + owner._itemCount--; + Array.Copy(owner.items, index + 1, owner.items, index, owner.ItemCount - index); + owner.items[owner.ItemCount] = null; + owner.ItemsChanged(CHANGE_ITEMS); + + // if the last item was removed, clear the collection + // + if (owner.ItemCount == 0) + { + Clear(); + } + } + + /// + /// Removes the menu iteml with the specified key. + /// + public virtual void RemoveByKey(string key) + { + int index = IndexOfKey(key); + if (IsValidIndex(index)) + { + RemoveAt(index); + } + } + + /// + /// Removes the specified item from this menu. All subsequent + /// items are moved down one slot. + /// + public virtual void Remove(MenuItem item) + { + if (item.Parent == owner) + { + RemoveAt(item.Index); + } + } + + void IList.Remove(object value) + { + if (value is MenuItem) + { + Remove((MenuItem)value); + } + } + } + } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuItem.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuItem.cs index 796345c289e..7d009374f37 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuItem.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuItem.cs @@ -1,270 +1,1766 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable IDE1006, CS8625, CS8618, CS8601, CS8600, CS8603, CS8602, CS8604, RS0036, RS0016, IDE0052 + +using System.Collections; using System.ComponentModel; +using System.Drawing; +using System.Globalization; +using System.Runtime.InteropServices; +using SafeNativeMethods = System.Windows.Forms.LegacyMenuSafeNativeMethods; +using UnsafeNativeMethods = System.Windows.Forms.LegacyMenuUnsafeNativeMethods; -namespace System.Windows.Forms; - -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.MenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[ToolboxItem(false)] -[DesignTimeVisible(false)] -[DefaultEvent(nameof(Click))] -[DefaultProperty(nameof(Text))] -public class MenuItem : Menu +namespace System.Windows.Forms { - public MenuItem() : this( - mergeType: MenuMerge.Add, - mergeOrder: 0, - shortcut: 0, - text: null, - onClick: null, - onPopup: null, - onSelect: null, - items: null) => throw new PlatformNotSupportedException(); - - public MenuItem(string text) : this( - mergeType: MenuMerge.Add, - mergeOrder: 0, - shortcut: 0, - text: text, - onClick: null, - onPopup: null, - onSelect: null, - items: null) => throw new PlatformNotSupportedException(); - - public MenuItem(string text, EventHandler onClick) : this( - mergeType: MenuMerge.Add, - mergeOrder: 0, - shortcut: 0, - text: text, - onClick: onClick, - onPopup: null, - onSelect: null, - items: null) => throw new PlatformNotSupportedException(); - - public MenuItem(string text, EventHandler onClick, Shortcut shortcut) : this( - mergeType: MenuMerge.Add, - mergeOrder: 0, - shortcut: shortcut, - text: text, - onClick: onClick, - onPopup: null, - onSelect: null, - items: null) => throw new PlatformNotSupportedException(); - - public MenuItem(string text, MenuItem[] items) : this( - mergeType: MenuMerge.Add, - mergeOrder: 0, - shortcut: 0, - text: text, - onClick: null, - onPopup: null, - onSelect: null, - items: items) => throw new PlatformNotSupportedException(); - - public MenuItem( - MenuMerge mergeType, - int mergeOrder, - Shortcut shortcut, - string text, - EventHandler onClick, - EventHandler onPopup, - EventHandler onSelect, - MenuItem[] items) : base(items: items) => throw new PlatformNotSupportedException(); - - [Browsable(false)] - [DefaultValue(false)] - public bool BarBreak + /// + /// Represents an individual item that is displayed within a + /// or . + /// + [ToolboxItem(false)] + [DesignTimeVisible(false)] + [DefaultEvent(nameof(Click))] + [DefaultProperty(nameof(Text))] + public class MenuItem : Menu { - get => throw null; - set { } - } + private const int StateBarBreak = 0x00000020; + private const int StateBreak = 0x00000040; + private const int StateChecked = 0x00000008; + private const int StateDefault = 0x00001000; + private const int StateDisabled = 0x00000003; + private const int StateRadioCheck = 0x00000200; + private const int StateHidden = 0x00010000; + private const int StateMdiList = 0x00020000; + private const int StateCloneMask = 0x0003136B; + private const int StateOwnerDraw = 0x00000100; + private const int StateInMdiPopup = 0x00000200; + private const int StateHiLite = 0x00000080; - [Browsable(false)] - [DefaultValue(false)] - public bool Break - { - get => throw null; - set { } - } + private bool _hasHandle; + private MenuItemData _data; + private int _dataVersion; + private MenuItem _nextLinkedItem; // Next item linked to the same MenuItemData. - [DefaultValue(false)] - public bool Checked - { - get => throw null; - set { } - } + // We need to store a table of all created menuitems, so that other objects + // such as ContainerControl can get a reference to a particular menuitem, + // given a unique ID. + private static readonly Hashtable s_allCreatedMenuItems = new Hashtable(); + private const uint FirstUniqueID = 0xC0000000; + private static long s_nextUniqueID = FirstUniqueID; + private uint _uniqueID; + private IntPtr _msaaMenuInfoPtr = IntPtr.Zero; + private bool _menuItemIsCreated; - [DefaultValue(false)] - public bool DefaultItem - { - get => throw null; - set { } - } +#if DEBUG + private string _debugText; + private readonly int _creationNumber; + private static int CreateCount; +#endif - [DefaultValue(false)] - public bool OwnerDraw - { - get => throw null; - set { } - } + /// + /// Initializes a with a blank caption. + /// + public MenuItem() : this(MenuMerge.Add, 0, 0, null, null, null, null, null) + { + } - [Localizable(true)] - [DefaultValue(true)] - public bool Enabled - { - get => throw null; - set { } - } + /// + /// Initializes a new instance of the class + /// with a specified caption for the menu item. + /// + public MenuItem(string text) : this(MenuMerge.Add, 0, 0, text, null, null, null, null) + { + } - [Browsable(false)] - public int Index - { - get => throw null; - set { } - } + /// + /// Initializes a new instance of the class with a specified caption and event handler + /// for the menu item. + /// + public MenuItem(string text, EventHandler onClick) : this(MenuMerge.Add, 0, 0, text, onClick, null, null, null) + { + } - [Browsable(false)] - public override bool IsParent - { - get => throw null; - } + /// + /// Initializes a new instance of the class with a specified caption, event handler, + /// and associated shorcut key for the menu item. + /// + public MenuItem(string text, EventHandler onClick, Shortcut shortcut) : this(MenuMerge.Add, 0, shortcut, text, onClick, null, null, null) + { + } - [DefaultValue(false)] - public bool MdiList - { - get => throw null; - set { } - } + /// + /// Initializes a new instance of the class with a specified caption and an array of + /// submenu items defined for the menu item. + /// + public MenuItem(string text, MenuItem[] items) : this(MenuMerge.Add, 0, 0, text, null, null, null, items) + { + } - protected int MenuID - { - get => throw null; - } + internal MenuItem(MenuItemData data) : base(null) + { + data.AddItem(this); - [DefaultValue(MenuMerge.Add)] - public MenuMerge MergeType - { - get => throw null; - set { } - } +#if DEBUG + _debugText = data._caption; +#endif + } - [DefaultValue(0)] - public int MergeOrder - { - get => throw null; - set { } - } + /// + /// Initializes a new instance of the class with a specified caption, defined + /// event-handlers for the Click, Select and Popup events, a shortcut key, + /// a merge type, and order specified for the menu item. + /// + public MenuItem(MenuMerge mergeType, int mergeOrder, Shortcut shortcut, + string text, EventHandler onClick, EventHandler onPopup, + EventHandler onSelect, MenuItem[] items) : base(items) + { + new MenuItemData(this, mergeType, mergeOrder, shortcut, true, + text, onClick, onPopup, onSelect, null, null); - [Browsable(false)] - public char Mnemonic => throw null; +#if DEBUG + _debugText = text; + _creationNumber = CreateCount++; +#endif + } - [Browsable(false)] - public Menu Parent - { - get => throw null; - } + /// + /// Gets or sets a value indicating whether the item is placed on a new line (for a + /// menu item added to a object) or in a + /// new column (for a submenu or menu displayed in a ). + /// + [Browsable(false)] + [DefaultValue(false)] + public bool BarBreak + { + get + { + CheckIfDisposed(); + return (_data.State & StateBarBreak) != 0; + } + set + { + CheckIfDisposed(); + _data.SetState(StateBarBreak, value); + } + } - [DefaultValue(false)] - public bool RadioCheck - { - get => throw null; - set { } - } + /// + /// Gets or sets a value indicating whether the item is placed on a new line (for a + /// menu item added to a object) or in a + /// new column (for a submenu or menu displayed in a ). + /// + [Browsable(false)] + [DefaultValue(false)] + public bool Break + { + get + { + CheckIfDisposed(); + return (_data.State & StateBreak) != 0; + } + set + { + CheckIfDisposed(); + _data.SetState(StateBreak, value); + } + } - [Localizable(true)] - public string Text - { - get => throw null; - set { } - } + /// + /// Gets or sets a value indicating whether a checkmark appears beside the text of + /// the menu item. + /// + [DefaultValue(false)] + public bool Checked + { + get + { + CheckIfDisposed(); + return (_data.State & StateChecked) != 0; + } + set + { + CheckIfDisposed(); - [Localizable(true)] - [DefaultValue(Shortcut.None)] - public Shortcut Shortcut - { - get => throw null; - set { } - } + if (value && (ItemCount != 0 || Parent is MainMenu)) + { + throw new ArgumentException("Checked menu items cannot be parent items or top-level main menu items.", nameof(value)); + } - [DefaultValue(true)] - [Localizable(true)] - public bool ShowShortcut - { - get => throw null; - set { } - } + _data.SetState(StateChecked, value); + } + } - [Localizable(true)] - [DefaultValue(true)] - public bool Visible - { - get => throw null; - set { } - } + /// + /// Gets or sets a value indicating whether the menu item is the default. + /// + [DefaultValue(false)] + public bool DefaultItem + { + get + { + CheckIfDisposed(); + return (_data.State & StateDefault) != 0; + } + set + { + CheckIfDisposed(); + if (Parent is not null) + { + if (value) + { + UnsafeNativeMethods.SetMenuDefaultItem(new HandleRef(Parent, Parent.handle), MenuID, false); + } + else if (DefaultItem) + { + UnsafeNativeMethods.SetMenuDefaultItem(new HandleRef(Parent, Parent.handle), -1, false); + } + } - public event EventHandler Click - { - add { } - remove { } - } + _data.SetState(StateDefault, value); + } + } - public event DrawItemEventHandler DrawItem - { - add { } - remove { } - } + /// + /// Gets or sets a value indicating whether code that you provide draws the menu + /// item or Windows draws the menu item. + /// + [SRCategory(nameof(SR.CatBehavior))] + [DefaultValue(false)] + public bool OwnerDraw + { + get + { + CheckIfDisposed(); + return ((_data.State & StateOwnerDraw) != 0); + } + set + { + CheckIfDisposed(); + _data.SetState(StateOwnerDraw, value); + } + } - public event MeasureItemEventHandler MeasureItem - { - add { } - remove { } - } + /// + /// Gets or sets a value indicating whether the menu item is enabled. + /// + [Localizable(true)] + [DefaultValue(true)] + public bool Enabled + { + get + { + CheckIfDisposed(); + return (_data.State & StateDisabled) == 0; + } + set + { + CheckIfDisposed(); + _data.SetState(StateDisabled, !value); + } + } - public event EventHandler Popup - { - add { } - remove { } - } + /// + /// Gets or sets the menu item's position in its parent menu. + /// + [Browsable(false)] + public int Index + { + get + { + if (Parent is not null) + { + for (int i = 0; i < Parent.ItemCount; i++) + { + if (Parent.items[i] == this) + { + return i; + } + } + } - public event EventHandler Select - { - add { } - remove { } - } + return -1; + } + set + { + int oldIndex = Index; + if (oldIndex >= 0) + { + if (value < 0 || value >= Parent.ItemCount) + { + throw new ArgumentOutOfRangeException(nameof(value), string.Format(SR.InvalidArgument, nameof(Index), value)); + } + + if (value != oldIndex) + { + // The menu reverts to null when we're removed, so hold onto it in a + // local variable + Menu parent = Parent; + parent.MenuItems.RemoveAt(oldIndex); + parent.MenuItems.Add(value, this); + } + } + } + } + + /// + /// Gets a value indicating whether the menu item contains child menu items. + /// + [Browsable(false)] + public override bool IsParent + { + get + { + if (_data is not null && MdiList) + { + for (int i = 0; i < ItemCount; i++) + { + if (items[i]._data.UserData is not MdiListUserData) + { + return true; + } + } + + if (FindMdiForms().Length > 0) + { + return true; + } + + if (Parent is not null and not MenuItem) + { + return true; + } + + return false; + } + + return base.IsParent; + } + } + + /// + /// Gets or sets a value indicating whether the menu item will be populated with a + /// list of the MDI child windows that are displayed within the associated form. + /// + [DefaultValue(false)] + public bool MdiList + { + get + { + CheckIfDisposed(); + return (_data.State & StateMdiList) != 0; + } + set + { + CheckIfDisposed(); + _data.MdiList = value; + CleanListItems(this); + } + } + + /// + /// Gets the Windows identifier for this menu item. + /// + protected int MenuID + { + get + { + CheckIfDisposed(); + return _data.GetMenuID(); + } + } + + /// + /// Is this menu item currently selected (highlighted) by the user? + /// + internal bool Selected + { + get + { + if (Parent is null) + { + return false; + } + + LegacyMenuNativeMethods.MENUITEMINFO_T info = new() + { + fMask = LegacyMenuNativeMethods.MIIM_STATE + }; + UnsafeNativeMethods.GetMenuItemInfo(new HandleRef(Parent, Parent.handle), MenuID, false, info); + + return (info.fState & StateHiLite) != 0; + } + } + + /// + /// Gets the zero-based index of this menu item in the parent menu, or -1 if this + /// menu item is not associated with a parent menu. + /// + internal int MenuIndex + { + get + { + if (Parent is null) + { + return -1; + } + + int count = UnsafeNativeMethods.GetMenuItemCount(new HandleRef(Parent, Parent.Handle)); + int id = MenuID; + LegacyMenuNativeMethods.MENUITEMINFO_T info = new() + { + fMask = LegacyMenuNativeMethods.MIIM_ID | LegacyMenuNativeMethods.MIIM_SUBMENU + }; + + for (int i = 0; i < count; i++) + { + UnsafeNativeMethods.GetMenuItemInfo(new HandleRef(Parent, Parent.handle), i, true, info); + + // For sub menus, the handle is always valid. + // For items, however, it is always zero. + if ((info.hSubMenu == IntPtr.Zero || info.hSubMenu == Handle) && info.wID == id) + { + return i; + } + } + + return -1; + } + } + + /// + /// Gets or sets a value that indicates the behavior of this + /// menu item when its menu is merged with another. + /// + [DefaultValue(MenuMerge.Add)] + public MenuMerge MergeType + { + get + { + CheckIfDisposed(); + return _data._mergeType; + } + set + { + CheckIfDisposed(); + if (value is < MenuMerge.Add or > MenuMerge.Remove) + { + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(MenuMerge)); + } + + _data.MergeType = value; + } + } + + /// + /// Gets or sets the relative position the menu item when its + /// menu is merged with another. + /// + [DefaultValue(0)] + public int MergeOrder + { + get + { + CheckIfDisposed(); + return _data._mergeOrder; + } + set + { + CheckIfDisposed(); + _data.MergeOrder = value; + } + } + + /// + /// Retrieves the hotkey mnemonic that is associated with this menu item. + /// The mnemonic is the first character after an ampersand symbol in the menu's text + /// that is not itself an ampersand symbol. If no such mnemonic is defined this + /// will return zero. + /// + [Browsable(false)] + public char Mnemonic + { + get + { + CheckIfDisposed(); + return _data.Mnemonic; + } + } + + /// + /// Gets the menu in which this menu item appears. + /// + [Browsable(false)] + public Menu Parent { get; internal set; } + + /// + /// Gets or sets a value that indicates whether the menu item, if checked, + /// displays a radio-button mark instead of a check mark. + /// + [DefaultValue(false)] + public bool RadioCheck + { + get + { + CheckIfDisposed(); + return (_data.State & StateRadioCheck) != 0; + } + set + { + CheckIfDisposed(); + _data.SetState(StateRadioCheck, value); + } + } + + internal override bool RenderIsRightToLeft => Parent is not null && Parent.RenderIsRightToLeft; + + /// + /// Gets or sets the text of the menu item. + /// + [Localizable(true)] + public string Text + { + get + { + CheckIfDisposed(); + return _data._caption; + } + set + { + CheckIfDisposed(); + _data.SetCaption(value); + } + } + + /// + /// Gets or sets the shortcut key associated with the menu item. + /// + [Localizable(true)] + [DefaultValue(Shortcut.None)] + public Shortcut Shortcut + { + get + { + CheckIfDisposed(); + return _data._shortcut; + } + set + { + CheckIfDisposed(); + if (!Enum.IsDefined(typeof(Shortcut), value)) + { + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(Shortcut)); + } + + _data._shortcut = value; + UpdateMenuItem(force: true); + } + } + + /// + /// Gets or sets a value that indicates whether the shortcut key that is associated + /// with the menu item is displayed next to the menu item caption. + /// + [DefaultValue(true), + Localizable(true)] + public bool ShowShortcut + { + get + { + CheckIfDisposed(); + return _data._showShortcut; + } + set + { + CheckIfDisposed(); + if (value != _data._showShortcut) + { + _data._showShortcut = value; + UpdateMenuItem(force: true); + } + } + } + + /// + /// Gets or sets a value that indicates whether the menu item is visible on its + /// parent menu. + /// + [Localizable(true)] + [DefaultValue(true)] + public bool Visible + { + get + { + CheckIfDisposed(); + return (_data.State & StateHidden) == 0; + } + set + { + CheckIfDisposed(); + _data.Visible = value; + } + } + + /// + /// Occurs when the menu item is clicked or selected using a shortcut key defined + /// for the menu item. + /// + public event EventHandler Click + { + add + { + CheckIfDisposed(); + _data._onClick += value; + } + remove + { + CheckIfDisposed(); + _data._onClick -= value; + } + } + + /// + /// Occurs when when the property of a menu item is set to and + /// a request is made to draw the menu item. + /// + [SRCategory(nameof(SR.CatBehavior)), SRDescription(nameof(SR.drawItemEventDescr))] + public event DrawItemEventHandler DrawItem + { + add + { + CheckIfDisposed(); + _data._onDrawItem += value; + } + remove + { + CheckIfDisposed(); + _data._onDrawItem -= value; + } + } + + /// + /// Occurs when when the menu needs to know the size of a menu item before drawing it. + /// + [SRCategory(nameof(SR.CatBehavior)), SRDescription(nameof(SR.measureItemEventDescr))] + public event MeasureItemEventHandler MeasureItem + { + add + { + CheckIfDisposed(); + _data._onMeasureItem += value; + } + remove + { + CheckIfDisposed(); + _data._onMeasureItem -= value; + } + } + + /// + /// Occurs before a menu item's list of menu items is displayed. + /// + public event EventHandler Popup + { + add + { + CheckIfDisposed(); + _data._onPopup += value; + } + remove + { + CheckIfDisposed(); + _data._onPopup -= value; + } + } + + /// + /// Occurs when the user hovers their mouse over a menu item or selects it with the + /// keyboard but has not activated it. + /// + public event EventHandler Select + { + add + { + CheckIfDisposed(); + _data._onSelect += value; + } + remove + { + CheckIfDisposed(); + _data._onSelect -= value; + } + } + + private static void CleanListItems(MenuItem senderMenu) + { + // Remove dynamic items. + for (int i = senderMenu.MenuItems.Count - 1; i >= 0; i--) + { + MenuItem item = senderMenu.MenuItems[i]; + if (item._data.UserData is MdiListUserData) + { + item.Dispose(); + continue; + } + } + } + + /// + /// Creates and returns an identical copy of this menu item. + /// + public virtual MenuItem CloneMenu() + { + var newItem = new MenuItem(); + newItem.CloneMenu(this); + return newItem; + } + + /// + /// Creates a copy of the specified menu item. + /// + protected void CloneMenu(MenuItem itemSrc) + { + base.CloneMenu(itemSrc); + int state = itemSrc._data.State; + new MenuItemData(this, + itemSrc.MergeType, itemSrc.MergeOrder, itemSrc.Shortcut, itemSrc.ShowShortcut, + itemSrc.Text, itemSrc._data._onClick, itemSrc._data._onPopup, itemSrc._data._onSelect, + itemSrc._data._onDrawItem, itemSrc._data._onMeasureItem); + _data.SetState(state & StateCloneMask, true); + } + + internal virtual void CreateMenuItem() + { + if ((_data.State & StateHidden) == 0) + { + LegacyMenuNativeMethods.MENUITEMINFO_T info = CreateMenuItemInfo(); + UnsafeNativeMethods.InsertMenuItem(new HandleRef(Parent, Parent.handle), -1, true, info); + + _hasHandle = info.hSubMenu != IntPtr.Zero; + _dataVersion = _data._version; + + _menuItemIsCreated = true; + if (RenderIsRightToLeft) + { + Parent.UpdateRtl(true); + } + +#if DEBUG + LegacyMenuNativeMethods.MENUITEMINFO_T infoVerify = new() + { + fMask = LegacyMenuNativeMethods.MIIM_ID | LegacyMenuNativeMethods.MIIM_STATE | + LegacyMenuNativeMethods.MIIM_SUBMENU | LegacyMenuNativeMethods.MIIM_TYPE + }; + UnsafeNativeMethods.GetMenuItemInfo(new HandleRef(Parent, Parent.handle), MenuID, false, infoVerify); +#endif + } + } + + private LegacyMenuNativeMethods.MENUITEMINFO_T CreateMenuItemInfo() + { + LegacyMenuNativeMethods.MENUITEMINFO_T info = new() + { + fMask = LegacyMenuNativeMethods.MIIM_ID | LegacyMenuNativeMethods.MIIM_STATE | + LegacyMenuNativeMethods.MIIM_SUBMENU | LegacyMenuNativeMethods.MIIM_TYPE | LegacyMenuNativeMethods.MIIM_DATA, + fType = _data.State & (StateBarBreak | StateBreak | StateRadioCheck | StateOwnerDraw) + }; + + // Top level menu items shouldn't have barbreak or break bits set on them. + bool isTopLevel = Parent == GetMainMenu(); + + if (_data._caption.Equals("-")) + { + if (isTopLevel) + { + _data._caption = " "; + info.fType |= LegacyMenuNativeMethods.MFT_MENUBREAK; + } + else + { + info.fType |= LegacyMenuNativeMethods.MFT_SEPARATOR; + } + } + + info.fState = _data.State & (StateChecked | StateDefault | StateDisabled); + + info.wID = MenuID; + if (IsParent) + { + info.hSubMenu = Handle; + } + + info.hbmpChecked = IntPtr.Zero; + info.hbmpUnchecked = IntPtr.Zero; + + // Assign a unique ID to this menu item object. + // The ID is stored in the dwItemData of the corresponding Win32 menu item, so + // that when we get Win32 messages about the item later, we can delegate to the + // original object menu item object. A static hash table is used to map IDs to + // menu item objects. + if (_uniqueID == 0) + { + lock (s_allCreatedMenuItems) + { + _uniqueID = (uint)Interlocked.Increment(ref s_nextUniqueID); + Debug.Assert(_uniqueID >= FirstUniqueID); // ...check for ID range exhaustion (unlikely!) + // We add a weak ref wrapping a MenuItem to the static hash table, as + // supposed to adding the item ref itself, to allow the item to be finalized + // in case it is not disposed and no longer referenced anywhere else, hence + // preventing leaks. + s_allCreatedMenuItems.Add(_uniqueID, new WeakReference(this)); + } + } + + if (IntPtr.Size == 4) + { + // Store the unique ID in the dwItemData.. + // For simple menu items, we can just put the unique ID in the dwItemData. + // But for owner-draw items, we need to point the dwItemData at an MSAAMENUINFO + // structure so that MSAA can get the item text. + // To allow us to reliably distinguish between IDs and structure pointers later + // on, we keep IDs in the 0xC0000000-0xFFFFFFFF range. This is the top 1Gb of + // unmananged process memory, where an app's heap allocations should never come + // from. So that we can still get the ID from the dwItemData for an owner-draw + // item later on, a copy of the ID is tacked onto the end of the MSAAMENUINFO + // structure. + if (_data.OwnerDraw) + { + info.dwItemData = AllocMsaaMenuInfo(); + } + else + { + info.dwItemData = unchecked((IntPtr)(int)_uniqueID); + } + } + else + { + // On Win64, there are no reserved address ranges we can use for menu item IDs. So instead we will + // have to allocate an MSAMENUINFO heap structure for all menu items, not just owner-drawn ones. + info.dwItemData = AllocMsaaMenuInfo(); + } + + // We won't render the shortcut if: 1) it's not set, 2) we're a parent, 3) we're toplevel + if (_data._showShortcut && _data._shortcut != 0 && !IsParent && !isTopLevel) + { + info.dwTypeData = _data._caption + "\t" + TypeDescriptor.GetConverter(typeof(Keys)).ConvertToString((Keys)(int)_data._shortcut); + } + else + { + // Windows issue: Items with empty captions sometimes block keyboard + // access to other items in same menu. + info.dwTypeData = (_data._caption.Length == 0 ? " " : _data._caption); + } + + info.cch = 0; + + return info; + } + + /// + /// Disposes the . + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + Parent?.MenuItems.Remove(this); + _data?.RemoveItem(this); + lock (s_allCreatedMenuItems) + { + s_allCreatedMenuItems.Remove(_uniqueID); + } + + _uniqueID = 0; + } + + FreeMsaaMenuInfo(); + base.Dispose(disposing); + } + + /// + /// Given a unique menu item ID, find the corresponding MenuItem + /// object, using the master lookup table of all created MenuItems. + /// + internal static MenuItem GetMenuItemFromUniqueID(uint uniqueID) + { + WeakReference weakRef = (WeakReference)s_allCreatedMenuItems[uniqueID]; + if (weakRef is not null && weakRef.IsAlive) + { + return (MenuItem)weakRef.Target; + } - public virtual MenuItem CloneMenu() => throw null; + Debug.Fail("Weakref for menu item has expired or has been removed! Who is trying to access this ID?"); + return null; + } - protected void CloneMenu(MenuItem itemSrc) { } + /// + /// Given the "item data" value of a Win32 menu item, find the corresponding MenuItem object (using + /// the master lookup table of all created MenuItems). The item data may be either the unique menu + /// item ID, or a pointer to an MSAAMENUINFO structure with a copy of the unique ID tacked to the end. + /// To reliably tell IDs and structure addresses apart, IDs live in the 0xC0000000-0xFFFFFFFF range. + /// This is the top 1Gb of unmananged process memory, where an app's heap allocations should never be. + /// + internal static MenuItem GetMenuItemFromItemData(IntPtr itemData) + { + uint uniqueID = (uint)(ulong)itemData; + if (uniqueID == 0) + { + return null; + } - public virtual MenuItem MergeMenu() => throw null; + if (IntPtr.Size == 4) + { + if (uniqueID < FirstUniqueID) + { + MsaaMenuInfoWithId msaaMenuInfo = Marshal.PtrToStructure(itemData); + uniqueID = msaaMenuInfo._uniqueID; + } + } + else + { + // Its always a pointer on Win64 (see CreateMenuItemInfo) + MsaaMenuInfoWithId msaaMenuInfo = Marshal.PtrToStructure(itemData); + uniqueID = msaaMenuInfo._uniqueID; + } - public void MergeMenu(MenuItem itemSrc) { } + return GetMenuItemFromUniqueID(uniqueID); + } - protected virtual void OnClick(EventArgs e) { } + /// + /// MsaaMenuInfo is a small compatibility structure that carries menu text for accessibility. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct MsaaMenuInfo + { + public readonly uint _signature; + public readonly uint _textLength; - protected virtual void OnDrawItem(DrawItemEventArgs e) { } + [MarshalAs(UnmanagedType.LPWStr)] + public readonly string _text; - protected virtual void OnInitMenuPopup(EventArgs e) { } + public MsaaMenuInfo(string text) + { + _signature = 0; + _text = text; + _textLength = (uint)(text?.Length ?? 0); + } + } - protected virtual void OnMeasureItem(MeasureItemEventArgs e) { } + /// + /// MsaaMenuInfoWithId is an MSAAMENUINFO structure with a menu item ID field tacked onto the + /// end. This allows us to pass the data we need to Win32 / MSAA, and still be able to get the ID + /// out again later on, so we can delegate Win32 menu messages back to the correct MenuItem object. + /// + [StructLayout(LayoutKind.Sequential)] + private struct MsaaMenuInfoWithId + { + public readonly MsaaMenuInfo _msaaMenuInfo; + public readonly uint _uniqueID; - protected virtual void OnPopup(EventArgs e) { } + public MsaaMenuInfoWithId(string text, uint uniqueID) + { + _msaaMenuInfo = new MsaaMenuInfo(text); + _uniqueID = uniqueID; + } + } - protected virtual void OnSelect(EventArgs e) { } + /// + /// Creates an MSAAMENUINFO structure (in the unmanaged heap) based on the current state + /// of this MenuItem object. Address of this structure is cached in the object so we can + /// free it later on using FreeMsaaMenuInfo(). If structure has already been allocated, + /// it is destroyed and a new one created. + /// + private IntPtr AllocMsaaMenuInfo() + { + FreeMsaaMenuInfo(); + _msaaMenuInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf()); - public void PerformClick() { } + if (IntPtr.Size == 4) + { + // We only check this on Win32, irrelevant on Win64 (see CreateMenuItemInfo) + // Check for incursion into menu item ID range (unlikely!) + Debug.Assert(((uint)(ulong)_msaaMenuInfoPtr) < FirstUniqueID); + } - public virtual void PerformSelect() { } + MsaaMenuInfoWithId msaaMenuInfoStruct = new MsaaMenuInfoWithId(_data._caption, _uniqueID); + Marshal.StructureToPtr(msaaMenuInfoStruct, _msaaMenuInfoPtr, false); + Debug.Assert(_msaaMenuInfoPtr != IntPtr.Zero); + return _msaaMenuInfoPtr; + } + + /// + /// Frees the MSAAMENUINFO structure (in the unmanaged heap) for the current MenuObject + /// object, if one has previously been allocated. Takes care to free sub-structures too, + /// to avoid leaks! + /// + private void FreeMsaaMenuInfo() + { + if (_msaaMenuInfoPtr != IntPtr.Zero) + { + Marshal.DestroyStructure(_msaaMenuInfoPtr, typeof(MsaaMenuInfoWithId)); + Marshal.FreeHGlobal(_msaaMenuInfoPtr); + _msaaMenuInfoPtr = IntPtr.Zero; + } + } + + internal override void ItemsChanged(int change) + { + base.ItemsChanged(change); + + if (change == CHANGE_ITEMS) + { + // when the menu collection changes deal with it locally + Debug.Assert(!created, "base.ItemsChanged should have wiped out our handles"); + if (Parent is not null && Parent.created) + { + UpdateMenuItem(force: true); + CreateMenuItems(); + } + } + else + { + if (!_hasHandle && IsParent) + { + UpdateMenuItem(force: true); + } + + MainMenu main = GetMainMenu(); + if (main is not null && ((_data.State & StateInMdiPopup) == 0)) + { + main.ItemsChanged(change, this); + } + } + } + + internal void ItemsChanged(int change, MenuItem item) + { + if (change == CHANGE_ITEMADDED && + _data is not null && + _data.baseItem is not null && + _data.baseItem.MenuItems.Contains(item)) + { + if (Parent is not null && Parent.created) + { + UpdateMenuItem(force: true); + CreateMenuItems(); + } + else if (_data is not null) + { + MenuItem currentMenuItem = _data.firstItem; + while (currentMenuItem is not null) + { + if (currentMenuItem.created) + { + MenuItem newItem = item.CloneMenu(); + item._data.AddItem(newItem); + currentMenuItem.MenuItems.Add(newItem); + break; + } + + currentMenuItem = currentMenuItem._nextLinkedItem; + } + } + } + } + + internal Form[] FindMdiForms() + { + Form[] forms = null; + MainMenu main = GetMainMenu(); + Form menuForm = null; + if (main is not null) + { + menuForm = main.GetFormUnsafe(); + } + + if (menuForm is not null) + { + forms = menuForm.MdiChildren; + } + + forms ??= []; + + return forms; + } + + /// + /// See the similar code in MdiWindowListStrip.PopulateItems, which is + /// unfortunately just different enough in its working environment that we + /// can't readily combine the two. But if you're fixing something here, chances + /// are that the same issue will need scrutiny over there. + /// +// "-" is OK + private void PopulateMdiList() + { + MenuItem senderMenu = this; + _data.SetState(StateInMdiPopup, true); + try + { + CleanListItems(this); + + // Add new items + Form[] forms = FindMdiForms(); + if (forms is not null && forms.Length > 0) + { + Form activeMdiChild = GetMainMenu().GetFormUnsafe().ActiveMdiChild; + + if (senderMenu.MenuItems.Count > 0) + { + MenuItem sep = (MenuItem)Activator.CreateInstance(GetType()); + sep._data.UserData = new MdiListUserData(); + sep.Text = "-"; + senderMenu.MenuItems.Add(sep); + } + + // Build a list of child windows to be displayed in + // the MDIList menu item... + // Show the first maxMenuForms visible elements of forms[] as Window menu items, except: + // Always show the active form, even if it's not in the first maxMenuForms visible elements of forms[]. + // If the active form isn't in the first maxMenuForms forms, then show the first maxMenuForms-1 elements + // in forms[], and make the active form the last one on the menu. + // Don't count nonvisible forms against the limit on Window menu items. + + const int MaxMenuForms = 9; // Max number of Window menu items for forms + int visibleChildren = 0; // the number of visible child forms (so we know to show More Windows...) + int accel = 1; // prefix the form name with this digit, underlined, as an accelerator + int formsAddedToMenu = 0; + bool activeFormAdded = false; + for (int i = 0; i < forms.Length; i++) + { + if (forms[i].Visible) + { + visibleChildren++; + if ((activeFormAdded && (formsAddedToMenu < MaxMenuForms)) || // don't exceed max + ((!activeFormAdded && (formsAddedToMenu < (MaxMenuForms - 1))) || // save room for active if it's not in yet + (forms[i].Equals(activeMdiChild)))) + { + // there's always room for activeMdiChild + MenuItem windowItem = (MenuItem)Activator.CreateInstance(GetType()); + windowItem._data.UserData = new MdiListFormData(this, i); + + if (forms[i].Equals(activeMdiChild)) + { + windowItem.Checked = true; + activeFormAdded = true; + } + + windowItem.Text = string.Format(CultureInfo.CurrentUICulture, "&{0} {1}", accel, forms[i].Text); + accel++; + formsAddedToMenu++; + senderMenu.MenuItems.Add(windowItem); + } + } + } + + // Display the More Windows menu option when there are more than 9 MDI + // Child menu items to be displayed. This is necessary because we're managing our own + // MDI lists, rather than letting Windows do this for us. + if (visibleChildren > MaxMenuForms) + { + MenuItem moreWindows = (MenuItem)Activator.CreateInstance(GetType()); + moreWindows._data.UserData = new MdiListMoreWindowsData(this); + moreWindows.Text = SR.MDIMenuMoreWindows; + senderMenu.MenuItems.Add(moreWindows); + } + } + } + finally + { + _data.SetState(StateInMdiPopup, false); + } + } + + /// + /// Merges this menu item with another menu item and returns the resulting merged + /// . + /// + public virtual MenuItem MergeMenu() + { + CheckIfDisposed(); + + MenuItem newItem = (MenuItem)Activator.CreateInstance(GetType()); + _data.AddItem(newItem); + newItem.MergeMenu(this); + return newItem; + } + + /// + /// Merges another menu item with this menu item. + /// + public void MergeMenu(MenuItem itemSrc) + { + base.MergeMenu(itemSrc); + itemSrc._data.AddItem(this); + } + + /// + /// Raises the event. + /// + protected virtual void OnClick(EventArgs e) + { + CheckIfDisposed(); + + if (_data.UserData is MdiListUserData) + { + ((MdiListUserData)_data.UserData).OnClick(e); + } + else if (_data.baseItem != this) + { + _data.baseItem.OnClick(e); + } + else + { + _data._onClick?.Invoke(this, e); + } + } + + /// + /// Raises the event. + /// + protected virtual void OnDrawItem(DrawItemEventArgs e) + { + CheckIfDisposed(); + + if (_data.baseItem != this) + { + _data.baseItem.OnDrawItem(e); + } + else + { + _data._onDrawItem?.Invoke(this, e); + } + } + + /// + /// Raises the event. + /// + protected virtual void OnMeasureItem(MeasureItemEventArgs e) + { + CheckIfDisposed(); + + if (_data.baseItem != this) + { + _data.baseItem.OnMeasureItem(e); + } + else + { + _data._onMeasureItem?.Invoke(this, e); + } + } + + /// + /// Raises the event. + /// + protected virtual void OnPopup(EventArgs e) + { + CheckIfDisposed(); + + bool recreate = false; + for (int i = 0; i < ItemCount; i++) + { + if (items[i].MdiList) + { + recreate = true; + items[i].UpdateMenuItem(force: true); + } + } + + if (recreate || (_hasHandle && !IsParent)) + { + UpdateMenuItem(force: true); + } + + if (_data.baseItem != this) + { + _data.baseItem.OnPopup(e); + } + else + { + _data._onPopup?.Invoke(this, e); + } + + // Update any subitem states that got changed in the event + for (int i = 0; i < ItemCount; i++) + { + MenuItem item = items[i]; + if (item._dataVersion != item._data._version) + { + item.UpdateMenuItem(force: true); + } + } + + if (MdiList) + { + PopulateMdiList(); + } + } + + /// + /// Raises the event. + /// + protected virtual void OnSelect(EventArgs e) + { + CheckIfDisposed(); + + if (_data.baseItem != this) + { + _data.baseItem.OnSelect(e); + } + else + { + _data._onSelect?.Invoke(this, e); + } + } + + protected internal virtual void OnInitMenuPopup(EventArgs e) => OnPopup(e); + + /// + /// Generates a event for the MenuItem, + /// simulating a click by a user. + /// + public void PerformClick() => OnClick(EventArgs.Empty); + + /// + /// Raises the event for this menu item. + /// + public virtual void PerformSelect() => OnSelect(EventArgs.Empty); + + internal virtual bool ShortcutClick() + { + if (Parent is MenuItem parent) + { + if (!parent.ShortcutClick() || Parent != parent) + { + return false; + } + } + + if ((_data.State & StateDisabled) != 0) + { + return false; + } + + if (ItemCount > 0) + { + OnPopup(EventArgs.Empty); + } + else + { + OnClick(EventArgs.Empty); + } + + return true; + } + + public override string ToString() + { + string s = base.ToString(); + string menuItemText = _data?._caption ?? string.Empty; + ; + return s + ", Text: " + menuItemText; + } + + internal void UpdateItemRtl(bool setRightToLeftBit) + { + if (!_menuItemIsCreated) + { + return; + } + + LegacyMenuNativeMethods.MENUITEMINFO_T info = new() + { + fMask = LegacyMenuNativeMethods.MIIM_TYPE | LegacyMenuNativeMethods.MIIM_STATE | LegacyMenuNativeMethods.MIIM_SUBMENU, + dwTypeData = new string('\0', Text.Length + 2) + }; + info.cch = info.dwTypeData.Length - 1; + UnsafeNativeMethods.GetMenuItemInfo(new HandleRef(Parent, Parent.handle), MenuID, false, info); + if (setRightToLeftBit) + { + info.fType |= LegacyMenuNativeMethods.MFT_RIGHTJUSTIFY | LegacyMenuNativeMethods.MFT_RIGHTORDER; + } + else + { + info.fType &= ~(LegacyMenuNativeMethods.MFT_RIGHTJUSTIFY | LegacyMenuNativeMethods.MFT_RIGHTORDER); + } + + UnsafeNativeMethods.SetMenuItemInfo(new HandleRef(Parent, Parent.handle), MenuID, false, info); + } + + internal void UpdateMenuItem(bool force) + { + if (Parent is null || !Parent.created) + { + return; + } + + if (force || Parent is MainMenu || Parent is ContextMenu) + { + LegacyMenuNativeMethods.MENUITEMINFO_T info = CreateMenuItemInfo(); + UnsafeNativeMethods.SetMenuItemInfo(new HandleRef(Parent, Parent.handle), MenuID, false, info); +#if DEBUG + LegacyMenuNativeMethods.MENUITEMINFO_T infoVerify = new() + { + fMask = LegacyMenuNativeMethods.MIIM_ID | LegacyMenuNativeMethods.MIIM_STATE | + LegacyMenuNativeMethods.MIIM_SUBMENU | LegacyMenuNativeMethods.MIIM_TYPE + }; + UnsafeNativeMethods.GetMenuItemInfo(new HandleRef(Parent, Parent.handle), MenuID, false, infoVerify); +#endif + + if (_hasHandle && info.hSubMenu == IntPtr.Zero) + { + ClearHandles(); + } + + _hasHandle = info.hSubMenu != IntPtr.Zero; + _dataVersion = _data._version; + if (Parent is MainMenu mainMenu) + { + Form f = mainMenu.GetFormUnsafe(); + if (f is not null) + { + SafeNativeMethods.DrawMenuBar(new HandleRef(f, f.Handle)); + } + } + } + } + + internal void WmDrawItem(ref Message m) + { + // Handles the OnDrawItem message sent from ContainerControl + LegacyMenuNativeMethods.DRAWITEMSTRUCT dis = (LegacyMenuNativeMethods.DRAWITEMSTRUCT)m.GetLParam(typeof(LegacyMenuNativeMethods.DRAWITEMSTRUCT)); + using Graphics g = Graphics.FromHdcInternal(dis.hDC); + + OnDrawItem(new DrawItemEventArgs(g, SystemInformation.MenuFont, Rectangle.FromLTRB(dis.rcItem.left, dis.rcItem.top, dis.rcItem.right, dis.rcItem.bottom), Index, (DrawItemState)dis.itemState)); + + m.ResultInternal = (LRESULT)1; + } + + internal void WmMeasureItem(ref Message m) + { + // Handles the OnMeasureItem message sent from ContainerControl + + // Obtain the measure item struct + LegacyMenuNativeMethods.MEASUREITEMSTRUCT mis = (LegacyMenuNativeMethods.MEASUREITEMSTRUCT)m.GetLParam(typeof(LegacyMenuNativeMethods.MEASUREITEMSTRUCT)); + + // The OnMeasureItem handler now determines the height and width of the item + using var screen = GdiCache.GetScreenDCGraphics(); + MeasureItemEventArgs mie = new MeasureItemEventArgs(screen.Graphics, Index); + + OnMeasureItem(mie); + + // Update the measure item struct with the new width and height + mis.itemHeight = mie.ItemHeight; + mis.itemWidth = mie.ItemWidth; + Marshal.StructureToPtr(mis, m.LParamInternal, false); + + m.ResultInternal = (LRESULT)1; + } + + private void CheckIfDisposed() + { + ObjectDisposedException.ThrowIf(_data is null, this); + } + + internal class MenuItemData : ICommandExecutor + { + internal MenuItem baseItem; + internal MenuItem firstItem; + + private int _state; + internal int _version; + internal MenuMerge _mergeType; + internal int _mergeOrder; + internal string _caption; + internal short _mnemonic; + internal Shortcut _shortcut; + internal bool _showShortcut; + internal EventHandler _onClick; + internal EventHandler _onPopup; + internal EventHandler _onSelect; + internal DrawItemEventHandler _onDrawItem; + internal MeasureItemEventHandler _onMeasureItem; + + private Command _cmd; + + internal MenuItemData(MenuItem baseItem, MenuMerge mergeType, int mergeOrder, Shortcut shortcut, bool showShortcut, + string caption, EventHandler onClick, EventHandler onPopup, EventHandler onSelect, + DrawItemEventHandler onDrawItem, MeasureItemEventHandler onMeasureItem) + { + AddItem(baseItem); + _mergeType = mergeType; + _mergeOrder = mergeOrder; + _shortcut = shortcut; + _showShortcut = showShortcut; + _caption = caption ?? string.Empty; + _onClick = onClick; + _onPopup = onPopup; + _onSelect = onSelect; + _onDrawItem = onDrawItem; + _onMeasureItem = onMeasureItem; + _version = 1; + _mnemonic = -1; + } + + internal bool OwnerDraw + { + get => ((State & StateOwnerDraw) != 0); + set => SetState(StateOwnerDraw, value); + } + + internal bool MdiList + { + get => ((State & StateMdiList) == StateMdiList); + set + { + if (((_state & StateMdiList) != 0) != value) + { + SetState(StateMdiList, value); + for (MenuItem item = firstItem; item is not null; item = item._nextLinkedItem) + { + item.ItemsChanged(CHANGE_MDI); + } + } + } + } + + internal MenuMerge MergeType + { + get => _mergeType; + set + { + if (_mergeType != value) + { + _mergeType = value; + ItemsChanged(CHANGE_MERGE); + } + } + } + + internal int MergeOrder + { + get => _mergeOrder; + set + { + if (_mergeOrder != value) + { + _mergeOrder = value; + ItemsChanged(CHANGE_MERGE); + } + } + } + + internal char Mnemonic + { + get + { + if (_mnemonic == -1) + { + _mnemonic = (short)WindowsFormsUtils.GetMnemonic(_caption, true); + } + + return (char)_mnemonic; + } + } + + internal int State => _state; + + internal bool Visible + { + get => (_state & StateHidden) == 0; + set + { + if (((_state & StateHidden) == 0) != value) + { + _state = value ? _state & ~StateHidden : _state | StateHidden; + ItemsChanged(CHANGE_VISIBLE); + } + } + } + + internal object UserData { get; set; } + + internal void AddItem(MenuItem item) + { + if (item._data != this) + { + item._data?.RemoveItem(item); + + item._nextLinkedItem = firstItem; + firstItem = item; + baseItem ??= item; + + item._data = this; + item._dataVersion = 0; + item.UpdateMenuItem(false); + } + } + + public void Execute() + { + baseItem?.OnClick(EventArgs.Empty); + } + + internal int GetMenuID() + { + _cmd ??= new Command(this); + + return _cmd.ID; + } + + internal void ItemsChanged(int change) + { + for (MenuItem item = firstItem; item is not null; item = item._nextLinkedItem) + { + item.Parent?.ItemsChanged(change); + } + } + + internal void RemoveItem(MenuItem item) + { + Debug.Assert(item._data == this, "bad item passed to MenuItemData.removeItem"); + + if (item == firstItem) + { + firstItem = item._nextLinkedItem; + } + else + { + MenuItem itemT; + for (itemT = firstItem; item != itemT._nextLinkedItem;) + { + itemT = itemT._nextLinkedItem; + } + + itemT._nextLinkedItem = item._nextLinkedItem; + } + + item._nextLinkedItem = null; + item._data = null; + item._dataVersion = 0; + + if (item == baseItem) + { + baseItem = firstItem; + } + + if (firstItem is null) + { + // No longer needed. Toss all references and the Command object. + Debug.Assert(baseItem is null, "why isn't baseItem null?"); + _onClick = null; + _onPopup = null; + _onSelect = null; + _onDrawItem = null; + _onMeasureItem = null; + _cmd?.Dispose(); + _cmd = null; + } + } + + internal void SetCaption(string value) + { + value ??= string.Empty; + + if (!_caption.Equals(value)) + { + _caption = value; + UpdateMenuItems(); + } + +#if DEBUG + if (value.Length > 0) + { + baseItem._debugText = value; + } +#endif + } + + internal void SetState(int flag, bool value) + { + if (((_state & flag) != 0) != value) + { + _state = value ? _state | flag : _state & ~flag; + UpdateMenuItems(); + } + } + + internal void UpdateMenuItems() + { + _version++; + for (MenuItem item = firstItem; item is not null; item = item._nextLinkedItem) + { + item.UpdateMenuItem(force: true); + } + } + } + + private class MdiListUserData + { + public virtual void OnClick(EventArgs e) + { + } + } + + private class MdiListFormData : MdiListUserData + { + private readonly MenuItem _parent; + private readonly int _boundIndex; + + public MdiListFormData(MenuItem parentItem, int boundFormIndex) + { + _boundIndex = boundFormIndex; + _parent = parentItem; + } + + public override void OnClick(EventArgs e) + { + if (_boundIndex != -1) + { + Form[] forms = _parent.FindMdiForms(); + Debug.Assert(forms is not null, "Didn't get a list of the MDI Forms."); + + if (forms is not null && forms.Length > _boundIndex) + { + Form boundForm = forms[_boundIndex]; + boundForm.Activate(); + if (boundForm.ActiveControl is not null && !boundForm.ActiveControl.Focused) + { + boundForm.ActiveControl.Focus(); + } + } + } + } + } + + private class MdiListMoreWindowsData : MdiListUserData + { + private readonly MenuItem _parent; + + public MdiListMoreWindowsData(MenuItem parent) + { + _parent = parent; + } + + public override void OnClick(EventArgs e) + { + Form[] forms = _parent.FindMdiForms(); + Debug.Assert(forms is not null, "Didn't get a list of the MDI Forms."); + Form active = _parent.GetMainMenu().GetFormUnsafe().ActiveMdiChild; + Debug.Assert(active is not null, "Didn't get the active MDI child"); + if (forms is not null && forms.Length > 0 && active is not null) + { + using var dialog = new MdiWindowDialog(); + dialog.SetItems(active, forms); + DialogResult result = dialog.ShowDialog(); + if (result == DialogResult.OK) + { + dialog.ActiveChildForm.Activate(); + if (dialog.ActiveChildForm.ActiveControl is not null && !dialog.ActiveChildForm.ActiveControl.Focused) + { + dialog.ActiveChildForm.ActiveControl.Focus(); + } + } + } + } + } + } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuMerge.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuMerge.cs index 213491c8c79..f98608e335b 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuMerge.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ContextMenu/MenuMerge.cs @@ -1,20 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; - namespace System.Windows.Forms; -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.MenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] public enum MenuMerge { Add = 0, diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/CompModSwitches.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/CompModSwitches.cs new file mode 100644 index 00000000000..6fe49e985f7 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/CompModSwitches.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE1006, SA1518 + +namespace System.Windows.Forms; + +internal static class CompModSwitches +{ + public static TraceSwitch DataGridCursor { get; } = new("DataGridCursor", "DataGrid cursor tracing"); + public static TraceSwitch DataGridEditing { get; } = new("DataGridEditing", "DataGrid editing tracing"); + public static TraceSwitch DataGridKeys { get; } = new("DataGridKeys", "DataGrid keyboard tracing"); + public static TraceSwitch DataGridLayout { get; } = new("DataGridLayout", "DataGrid layout tracing"); + public static TraceSwitch DataGridPainting { get; } = new("DataGridPainting", "DataGrid painting tracing"); + public static TraceSwitch DataGridParents { get; } = new("DataGridParents", "DataGrid parent rows tracing"); + public static TraceSwitch DataGridScrolling { get; } = new("DataGridScrolling", "DataGrid scrolling tracing"); + public static TraceSwitch DataGridSelection { get; } = new("DataGridSelection", "DataGrid selection tracing"); + public static TraceSwitch DGCaptionPaint { get; } = new("DGCaptionPaint", "DataGrid caption painting tracing"); + public static TraceSwitch DGEditColumnEditing { get; } = new("DGEditColumnEditing", "DataGrid edit column tracing"); + public static TraceSwitch DGRelationShpRowLayout { get; } = new("DGRelationShpRowLayout", "DataGrid relationship row layout tracing"); + public static TraceSwitch DGRelationShpRowPaint { get; } = new("DGRelationShpRowPaint", "DataGrid relationship row painting tracing"); + public static TraceSwitch DGRowPaint { get; } = new("DGRowPaint", "DataGrid row painting tracing"); +} \ No newline at end of file diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.HitTestInfo.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.HitTestInfo.cs deleted file mode 100644 index 83dc85c3308..00000000000 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.HitTestInfo.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; - -namespace System.Windows.Forms; - -#nullable disable - -public partial class DataGrid -{ - /// - /// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] - [Browsable(false)] - public sealed class HitTestInfo - { - internal HitTestInfo() => throw new PlatformNotSupportedException(); - - public static readonly HitTestInfo Nowhere; - - public int Column => throw null; - - public int Row => throw null; - - public HitTestType Type => throw null; - } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.HitTestType.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.HitTestType.cs deleted file mode 100644 index 6ef140092a7..00000000000 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.HitTestType.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; - -namespace System.Windows.Forms; - -public partial class DataGrid -{ - /// - /// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] - [Browsable(false)] - [Flags] - public enum HitTestType - { - None = 0x00000000, - Cell = 0x00000001, - ColumnHeader = 0x00000002, - RowHeader = 0x00000004, - ColumnResize = 0x00000008, - RowResize = 0x00000010, - Caption = 0x00000020, - ParentRows = 0x00000040 - } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.cs index e3f18cd7017..b8449129eaf 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGrid.cs @@ -1,599 +1,10706 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, WFDEV006, RS0016, IDE0301, IDE0300, IDE0036, SA1507, IDE0078, IDE0075, IDE0059, IDE0051, IDE0031, SA1129, IDE0063, IDE0060, CA1823, CA1822 + +#nullable disable + +using System.Collections; using System.ComponentModel; +using System.ComponentModel.Design; using System.Drawing; -using System.Drawing.Design; +using System.Globalization; using System.Runtime.InteropServices; +using System.Text; +using static System.Windows.Forms.LegacyDataGridInteropCompat; +using static System.Windows.Forms.Triangle; +using DSR = System.Windows.Forms.DataGridStrings; namespace System.Windows.Forms; - -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[ComVisible(true)] -[ClassInterface(ClassInterfaceType.AutoDispatch)] -[Designer($"System.Windows.Forms.Design.DataGridDesigner, {Assemblies.SystemDesign}")] -[DefaultProperty(nameof(DataSource))] -[DefaultEvent(nameof(Navigate))] -[ComplexBindingProperties(nameof(DataSource), nameof(DataMember))] -public partial class DataGrid : Control, ISupportInitialize, IDataGridEditingService -{ - // Implement the default constructor explicitly to ensure that class can't be constructed. - public DataGrid() => throw new PlatformNotSupportedException(); - - [DefaultValue(true)] - public bool AllowNavigation - { - get => throw null; - set { } - } - - [DefaultValue(true)] - public bool AllowSorting - { - get => throw null; - set { } - } - - public Color AlternatingBackColor - { - get => throw null; - set { } - } - - public Color BackgroundColor - { - get => throw null; - set { } - } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override Image BackgroundImage - { - get => throw null; - set { } - } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override ImageLayout BackgroundImageLayout - { - get => throw null; - set { } - } - - [DefaultValue(BorderStyle.Fixed3D)] - [DispId(-504)] - public BorderStyle BorderStyle - { - get => throw null; - set { } - } - - public event EventHandler BorderStyleChanged - { - add { } - remove { } - } - - public Color CaptionBackColor - { - get => throw null; - set { } - } - - public Color CaptionForeColor - { - get => throw null; - set { } - } - - [Localizable(true)] - [AmbientValue(null)] - public Font CaptionFont - { - get => throw null; - set { } - } - - [DefaultValue("")] - [Localizable(true)] - public string CaptionText - { - get => throw null; - set { } - } - - [DefaultValue(true)] - public bool CaptionVisible - { - get => throw null; - set { } - } - - public event EventHandler CaptionVisibleChanged - { - add { } - remove { } - } - - [DefaultValue(true)] - public bool ColumnHeadersVisible - { - get => throw null; - set { } - } - - [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public DataGridCell CurrentCell - { - get => throw null; - set { } - } - - [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public int CurrentRowIndex - { - get => throw null; - set { } - } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override Cursor Cursor - { - get => throw null; - set { } - } - - [DefaultValue(null)] - [RefreshProperties(RefreshProperties.Repaint)] - [AttributeProvider(typeof(IListSource))] - public object DataSource - { - get => throw null; - set { } - } - - public event EventHandler DataSourceChanged - { - add { } - remove { } - } - - [DefaultValue(null)] - [Editor($"System.Windows.Forms.Design.DataMemberListEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))] - public string DataMember - { - get => throw null; - set { } - } - - public event EventHandler CurrentCellChanged - { - add { } - remove { } - } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler CursorChanged - { - add { } - remove { } - } - - public Color SelectionBackColor - { - get => throw null; - set { } - } - - public Color SelectionForeColor - { - get => throw null; - set { } - } - - [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] - [Localizable(true)] - public GridTableStylesCollection TableStyles => throw null; - - public Color GridLineColor - { - get => throw null; - set { } - } - - [DefaultValue(DataGridLineStyle.Solid)] - public DataGridLineStyle GridLineStyle - { - get => throw null; - set { } - } - - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [DefaultValue(DataGridParentRowsLabelStyle.Both)] - public DataGridParentRowsLabelStyle ParentRowsLabelStyle - { - get => throw null; - set { } - } - - public event EventHandler ParentRowsLabelStyleChanged - { - add { } - remove { } - } - - [Browsable(false)] - public int FirstVisibleColumn => throw null; - - [DefaultValue(false)] - public bool FlatMode - { - get => throw null; - set { } - } - - public event EventHandler FlatModeChanged - { - add { } - remove { } - } - - public Color HeaderBackColor - { - get => throw null; - set { } - } - - public Font HeaderFont - { - get => throw null; - set { } - } - - public Color HeaderForeColor - { - get => throw null; - set { } - } - - public event EventHandler BackgroundColorChanged - { - add { } - remove { } - } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler BackgroundImageChanged - { - add { } - remove { } - } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler BackgroundImageLayoutChanged - { - add { } - remove { } - } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Advanced)] - protected internal CurrencyManager ListManager + /// + /// Displays ADO.NET data in a scrollable grid. + /// + [ + ComVisible(true), + ClassInterface(ClassInterfaceType.AutoDispatch), + Designer("System.Windows.Forms.Design.DataGridDesigner, " + Assemblies.SystemDesign), + DefaultProperty(nameof(DataSource)), + DefaultEvent(nameof(Navigate)), + ComplexBindingProperties(nameof(DataSource), nameof(DataMember)), + ] + public class DataGrid : Control, ISupportInitialize, IDataGridEditingService { - get => throw null; - set { } - } - - public void ResetAlternatingBackColor() { } - - public void ResetGridLineColor() { } - - public void ResetHeaderBackColor() { } - - public void ResetHeaderFont() { } - - public void ResetSelectionBackColor() { } - - public void ResetSelectionForeColor() { } - - protected bool ShouldSerializeSelectionBackColor() => throw null; - - protected virtual bool ShouldSerializeSelectionForeColor() => throw null; - - public void SetDataBinding(object dataSource, string dataMember) { } - - protected virtual bool ShouldSerializeGridLineColor() => throw null; +#if DEBUG + internal TraceSwitch DataGridAcc = new TraceSwitch("DataGridAcc", "Trace Windows Forms DataGrid Accessibility"); +#else + internal TraceSwitch DataGridAcc = null; +#endif - protected virtual bool ShouldSerializeCaptionBackColor() => throw null; + private const int GRIDSTATE_allowSorting = 0x00000001; + private const int GRIDSTATE_columnHeadersVisible = 0x00000002; + private const int GRIDSTATE_rowHeadersVisible = 0x00000004; + private const int GRIDSTATE_trackColResize = 0x00000008; + private const int GRIDSTATE_trackRowResize = 0x00000010; + private const int GRIDSTATE_isLedgerStyle = 0x00000020; + private const int GRIDSTATE_isFlatMode = 0x00000040; + private const int GRIDSTATE_listHasErrors = 0x00000080; + private const int GRIDSTATE_dragging = 0x00000100; + private const int GRIDSTATE_inListAddNew = 0x00000200; + private const int GRIDSTATE_inDeleteRow = 0x00000400; + private const int GRIDSTATE_canFocus = 0x00000800; + private const int GRIDSTATE_readOnlyMode = 0x00001000; + private const int GRIDSTATE_allowNavigation = 0x00002000; + private const int GRIDSTATE_isNavigating = 0x00004000; + private const int GRIDSTATE_isEditing = 0x00008000; + private const int GRIDSTATE_editControlChanging = 0x00010000; + private const int GRIDSTATE_isScrolling = 0x00020000; + private const int GRIDSTATE_overCaption = 0x00040000; + private const int GRIDSTATE_childLinkFocused = 0x00080000; + private const int GRIDSTATE_inAddNewRow = 0x00100000; + private const int GRIDSTATE_inSetListManager = 0x00200000; + private const int GRIDSTATE_metaDataChanged = 0x00400000; + private const int GRIDSTATE_exceptionInPaint = 0x00800000; + private const int GRIDSTATE_layoutSuspended = 0x01000000; - protected virtual bool ShouldSerializeAlternatingBackColor() => throw null; + // PERF: take all the bools and put them into a state variable + private Collections.Specialized.BitVector32 gridState; // see GRIDSTATE_ consts above - protected virtual bool ShouldSerializeCaptionForeColor() => throw null; + // for column widths + private const int NumRowsForAutoResize = 10; - protected virtual bool ShouldSerializeHeaderBackColor() => throw null; + private const int errorRowBitmapWidth = 15; + + private const DataGridParentRowsLabelStyle defaultParentRowsLabelStyle = DataGridParentRowsLabelStyle.Both; - protected virtual bool ShouldSerializeBackgroundColor() => throw null; + private const BorderStyle defaultBorderStyle = BorderStyle.Fixed3D; - protected bool ShouldSerializeHeaderFont() => throw null; + private const bool defaultCaptionVisible = true; - protected virtual bool ShouldSerializeHeaderForeColor() => throw null; + private const bool defaultParentRowsVisible = true; - public void ResetHeaderForeColor() { } + private readonly DataGridTableStyle defaultTableStyle = new DataGridTableStyle(true); - protected ScrollBar HorizScrollBar => throw null; - - public Color LinkColor - { - get => throw null; - set { } - } - - public void ResetLinkColor() { } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public Color LinkHoverColor - { - get => throw null; - set { } - } - - protected virtual bool ShouldSerializeLinkHoverColor() => throw null; - - public void ResetLinkHoverColor() { } - - public event EventHandler AllowNavigationChanged - { - add { } - remove { } - } - - public Color ParentRowsBackColor - { - get => throw null; - set { } - } + // private bool allowSorting = true; - protected virtual bool ShouldSerializeParentRowsBackColor() => throw null; + private SolidBrush alternatingBackBrush = DefaultAlternatingBackBrush; - public Color ParentRowsForeColor - { - get => throw null; - set { } - } + // private bool columnHeadersVisible = true; - protected virtual bool ShouldSerializeParentRowsForeColor() => throw null; + private SolidBrush gridLineBrush = DefaultGridLineBrush; - [DefaultValue(75)] - [TypeConverter(typeof(DataGridPreferredColumnWidthTypeConverter))] - public int PreferredColumnWidth - { - get => throw null; - set { } - } + private const DataGridLineStyle defaultGridLineStyle = DataGridLineStyle.Solid; + private DataGridLineStyle gridLineStyle = defaultGridLineStyle; - public int PreferredRowHeight - { - get => throw null; - set { } - } + private SolidBrush headerBackBrush = DefaultHeaderBackBrush; - protected bool ShouldSerializePreferredRowHeight() => throw null; + private Font headerFont; // this is ambient property to Font value - [DefaultValue(false)] - public bool ReadOnly - { - get => throw null; - set { } - } + private SolidBrush headerForeBrush = DefaultHeaderForeBrush; + private Pen headerForePen = DefaultHeaderForePen; - public event EventHandler ReadOnlyChanged - { - add { } - remove { } - } + private SolidBrush linkBrush = DefaultLinkBrush; - [DefaultValue(true)] - public bool ParentRowsVisible - { - get => throw null; - set { } - } + private const int defaultPreferredColumnWidth = 75; + private int preferredColumnWidth = defaultPreferredColumnWidth; - public event EventHandler ParentRowsVisibleChanged - { - add { } - remove { } - } + private static readonly int defaultFontHeight = Control.DefaultFont.Height; + private int preferredRowHeight = defaultFontHeight + 3; + + // private bool rowHeadersVisible = true; + private const int defaultRowHeaderWidth = 35; + private int rowHeaderWidth = defaultRowHeaderWidth; + private int minRowHeaderWidth; - [DefaultValue(true)] - public bool RowHeadersVisible - { - get => throw null; - set { } - } + private SolidBrush selectionBackBrush = DefaultSelectionBackBrush; + private SolidBrush selectionForeBrush = DefaultSelectionForeBrush; + + // parent rows + // + private readonly DataGridParentRows parentRows; + // Set_ListManager uses the originalState to determine + // if the grid should disconnect from all the MetaDataChangedEvents + // keep "originalState is not null" when navigating back and forth in the grid + // and use Add/RemoveMetaDataChanged methods. + private DataGridState originalState; + + // ui state + // + // Don't use dataGridRows, use the accessor!!! + private DataGridRow[] dataGridRows = Array.Empty(); + private int dataGridRowsLength; + + // for toolTip + private int toolTipId; + private DataGridToolTip toolTipProvider; + + private DataGridAddNewRow addNewRow; + private LayoutData layout = new LayoutData(); + private RECT[] cachedScrollableRegion; + + // header namespace goo + // + + // these are actually get/set by ColumnBehavior + internal bool allowColumnResize = true; + + internal bool allowRowResize = true; - [DefaultValue(35)] - public int RowHeaderWidth - { - get => throw null; - set { } - } + internal DataGridParentRowsLabelStyle parentRowsLabels = defaultParentRowsLabelStyle; - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [Bindable(false)] - public override string Text - { - get => throw null; - set { } - } + // information for col/row resizing + // private bool trackColResize = false; + private int trackColAnchor; + private int trackColumn; + // private bool trackRowResize = false; + private int trackRowAnchor; + private int trackRow; + private PropertyDescriptor trackColumnHeader; + private MouseEventArgs lastSplitBar; - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Advanced)] - protected ScrollBar VertScrollBar => throw null; + // private bool isLedgerStyle = true; + // private bool isFlatMode = false; + private Font linkFont; - [Browsable(false)] - public int VisibleColumnCount => throw null; + private SolidBrush backBrush = DefaultBackBrush; + private SolidBrush foreBrush = DefaultForeBrush; + private SolidBrush backgroundBrush = DefaultBackgroundBrush; - [Browsable(false)] - public int VisibleRowCount => throw null; + // font cacheing info + private int fontHeight = -1; + private int linkFontHeight = -1; + private int captionFontHeight = -1; + private int headerFontHeight = -1; - public object this[int rowIndex, int columnIndex] - { - get => throw null; - set { } - } + // the preffered height of the row. - public object this[DataGridCell cell] - { - get => throw null; - set { } + // if the list has items with errors + + // private bool listHasErrors = false; + + // caption + private readonly DataGridCaption caption; + + // Border + // + private BorderStyle borderStyle; + + // data binding + // + private object dataSource; + private string dataMember = string.Empty; + private CurrencyManager listManager; + + // currently focused control + // we want to unparent it either when rebinding the grid or when the grid is disposed + Control toBeDisposedEditingControl; + + // persistent data state + // + internal GridTableStylesCollection dataGridTables; + // SET myGridTable in SetDataGridTable ONLY + internal DataGridTableStyle myGridTable; + internal bool checkHierarchy = true; + internal bool inInit; + + // Selection + internal int currentRow; + internal int currentCol; + private int numSelectedRows; + private int lastRowSelected = -1; + + // dragging: + // private bool dragging = false; + + // addNewRow + // private bool inAddNewRow = false; + // delete Row + // private bool inDeleteRow = false; + + // when we leave, we call CommitEdit + // if we leave, then do not focus the dataGrid. + // so we can't focus the grid at the following moments: + // 1. while processing the OnLayout event + // 2. while processing the OnLeave event + // private bool canFocus = true; + + // for CurrentCell +#if DEBUG + private bool inDataSource_PositionChanged; +#endif // DEBUG + + // Policy + // private bool readOnlyMode = false; + private readonly Policy policy = new Policy(); + // private bool allowNavigation = true; + + // editing + // private bool isNavigating = false; + // private bool isEditing = false; + // private bool editControlChanging = false; + private DataGridColumnStyle editColumn; + private DataGridRow editRow; + + // scrolling + // + private readonly HScrollBar horizScrollBar = new(); + private readonly VScrollBar vertScrollBar = new(); + + // the sum of the widths of the columns preceding the firstVisibleColumn + // + private int horizontalOffset; + + // the number of pixels of the firstVisibleColumn which are not visible + // + private int negOffset; + + private int wheelDelta; + // private bool isScrolling = false; + + // Visibility + // + internal int firstVisibleRow; + internal int firstVisibleCol; + private int numVisibleRows; + // the number of columns which are visible + private int numVisibleCols; + private int numTotallyVisibleRows; + // lastTotallyVisibleCol == -1 means that the data grid does not show any column in its entirety + private int lastTotallyVisibleCol; + + // mouse move hot-tracking + // + private int oldRow = -1; + // private bool overCaption = true; + + // child relationships focused + // + // private bool childLinkFocused = false; + + // private static readonly object EVENT_COLUMNHEADERCLICK = new object(); + private static readonly object EVENT_CURRENTCELLCHANGED = new object(); + // private static readonly object EVENT_COLUMNRESIZE = new object(); + // private static readonly object EVENT_LINKCLICKED = new object(); + private static readonly object EVENT_NODECLICKED = new object(); + // private static readonly object EVENT_ROWRESIZE = new object(); + private static readonly object EVENT_SCROLL = new object(); + private static readonly object EVENT_BACKBUTTONCLICK = new object(); + private static readonly object EVENT_DOWNBUTTONCLICK = new object(); + + // event handlers + // + private readonly ItemChangedEventHandler itemChangedHandler; + private readonly EventHandler positionChangedHandler; + private readonly EventHandler currentChangedHandler; + private readonly EventHandler metaDataChangedHandler; + + // we have to know when the collection of dataGridTableStyles changes + private readonly CollectionChangeEventHandler dataGridTableStylesCollectionChanged; + + private readonly EventHandler backButtonHandler; + private readonly EventHandler downButtonHandler; + + private NavigateEventHandler onNavigate; + + private EventHandler onRowHeaderClick; + + // forDebug + // + // private int forDebug = 0; + + // =----------------------------------------------------------------- + + /// + /// Initializes a new instance of the + /// class. + /// + public DataGrid() : base() + { + SetStyle(ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Opaque, false); + SetStyle(ControlStyles.SupportsTransparentBackColor, false); + SetStyle(ControlStyles.UserMouse, true); + gridState = new Collections.Specialized.BitVector32(0x00042827); + + dataGridTables = new GridTableStylesCollection(this); + layout = CreateInitialLayoutState(); + parentRows = new DataGridParentRows(this); + + horizScrollBar.Top = ClientRectangle.Height - horizScrollBar.Height; + horizScrollBar.Left = 0; + horizScrollBar.Visible = false; + horizScrollBar.Scroll += new ScrollEventHandler(GridHScrolled); + Controls.Add(horizScrollBar); + + vertScrollBar.Top = 0; + vertScrollBar.Left = ClientRectangle.Width - vertScrollBar.Width; + vertScrollBar.Visible = false; + vertScrollBar.Scroll += new ScrollEventHandler(GridVScrolled); + Controls.Add(vertScrollBar); + + BackColor = DefaultBackBrush.Color; + ForeColor = DefaultForeBrush.Color; + borderStyle = defaultBorderStyle; + + // create the event handlers + // + currentChangedHandler = new EventHandler(DataSource_RowChanged); + positionChangedHandler = new EventHandler(DataSource_PositionChanged); + itemChangedHandler = new ItemChangedEventHandler(DataSource_ItemChanged); + metaDataChangedHandler = new EventHandler(DataSource_MetaDataChanged); + dataGridTableStylesCollectionChanged = new CollectionChangeEventHandler(TableStylesCollectionChanged); + dataGridTables.CollectionChanged += dataGridTableStylesCollectionChanged; + + SetDataGridTable(defaultTableStyle, true); + + backButtonHandler = new EventHandler(OnBackButtonClicked); + downButtonHandler = new EventHandler(OnShowParentDetailsButtonClicked); + + caption = new DataGridCaption(this); + caption.BackwardClicked += backButtonHandler; + caption.DownClicked += downButtonHandler; + + RecalculateFonts(); + Size = new Size(130, 80); + Invalidate(); + PerformLayout(); + } + + // =------------------------------------------------------------------ + // = Properties + // =------------------------------------------------------------------ + + /// + /// Gets or sets a value indicating whether the grid can be resorted by clicking on + /// a column header. + /// + [ + SRCategory(nameof(SR.CatBehavior)), + DefaultValue(true), + ] + public bool AllowSorting + { + get + { + return gridState[GRIDSTATE_allowSorting]; + } + set + { + if (AllowSorting != value) + { + gridState[GRIDSTATE_allowSorting] = value; + if (!value && listManager is not null) + { + IList list = listManager.List; + if (list is IBindingList) + { + ((IBindingList)list).RemoveSort(); + } + } + } + } + } + + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color AlternatingBackColor + { + get + { + return alternatingBackBrush.Color; + } + set + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, + "AlternatingBackColor")); + } + + if (IsTransparentColor(value)) + { + throw new ArgumentException(DSR.DataGridTransparentAlternatingBackColorNotAllowed); + } + + if (!alternatingBackBrush.Color.Equals(value)) + { + alternatingBackBrush = new SolidBrush(value); + InvalidateInside(); + } + } + } + + public void ResetAlternatingBackColor() + { + if (ShouldSerializeAlternatingBackColor()) + { + AlternatingBackColor = DefaultAlternatingBackBrush.Color; + InvalidateInside(); + } + } + + protected virtual bool ShouldSerializeAlternatingBackColor() + { + return !AlternatingBackBrush.Equals(DefaultAlternatingBackBrush); + } + + internal Brush AlternatingBackBrush + { + get + { + return alternatingBackBrush; + } + } + + // overrode those properties just to move the BackColor and the ForeColor + // from the Appearance group onto the Color Group + /// + /// Gets or sets the background color of the grid. + /// + [ + SRCategory(nameof(SR.CatColors)), + SRDescription(nameof(SR.ControlBackColorDescr)) + ] + public override Color BackColor + { + // overrode those properties just to move the BackColor and the ForeColor + // from the Appearance group onto the Color Group + get + { + return base.BackColor; + } + set + { + if (IsTransparentColor(value)) + { + throw new ArgumentException(DSR.DataGridTransparentBackColorNotAllowed); + } + + base.BackColor = value; + } + } + + public override void ResetBackColor() + { + if (!BackColor.Equals(DefaultBackBrush.Color)) + { + BackColor = DefaultBackBrush.Color; + } + } + + [ + SRCategory(nameof(SR.CatColors)), + SRDescription(nameof(SR.ControlForeColorDescr)) + ] + public override Color ForeColor + { + get + { + return base.ForeColor; + } + set + { + base.ForeColor = value; + } + } + + public override void ResetForeColor() + { + if (!ForeColor.Equals(DefaultForeBrush.Color)) + { + ForeColor = DefaultForeBrush.Color; + } + } + + /// + /// Gets a value + /// indicating whether the property should be + /// persisted. + /// + internal SolidBrush BackBrush + { + get + { + return backBrush; + } + } + + internal SolidBrush ForeBrush + { + get + { + return foreBrush; + } + } + + /// + /// Gets or sets the border style. + /// + [SRCategory(nameof(SR.CatAppearance))] + [DefaultValue(defaultBorderStyle)] + [DispId(-504)] + public BorderStyle BorderStyle + { + get => borderStyle; + set + { + SourceGenerated.EnumValidator.Validate(value); + + if (borderStyle != value) + { + borderStyle = value; + PerformLayout(); + Invalidate(); + OnBorderStyleChanged(EventArgs.Empty); + } + } + } + + private static readonly object EVENT_BORDERSTYLECHANGED = new object(); + + [SRCategory(nameof(SR.CatPropertyChanged))] + public event EventHandler BorderStyleChanged + { + add => Events.AddHandler(EVENT_BORDERSTYLECHANGED, value); + remove => Events.RemoveHandler(EVENT_BORDERSTYLECHANGED, value); + } + + private int BorderWidth + { + get + { + if (BorderStyle == BorderStyle.Fixed3D) + { + return SystemInformation.Border3DSize.Width; + } + else if (BorderStyle == BorderStyle.FixedSingle) + { + return 2; + } + else + { + return 0; + } + } + } + + protected override Size DefaultSize + { + get + { + return new Size(130, 80); + } + } + + private static SolidBrush DefaultSelectionBackBrush + { + get + { + return (SolidBrush)SystemBrushes.ActiveCaption; + } + } + + private static SolidBrush DefaultSelectionForeBrush + { + get + { + return (SolidBrush)SystemBrushes.ActiveCaptionText; + } + } + + internal static SolidBrush DefaultBackBrush + { + get + { + return (SolidBrush)SystemBrushes.Window; + } + } + + internal static SolidBrush DefaultForeBrush + { + get + { + return (SolidBrush)SystemBrushes.WindowText; + } + } + + private static SolidBrush DefaultBackgroundBrush + { + get + { + return (SolidBrush)SystemBrushes.AppWorkspace; + } + } + + internal static SolidBrush DefaultParentRowsForeBrush + { + get + { + return (SolidBrush)SystemBrushes.WindowText; + } + } + + internal static SolidBrush DefaultParentRowsBackBrush + { + get + { + return (SolidBrush)SystemBrushes.Control; + } + } + + internal static SolidBrush DefaultAlternatingBackBrush + { + get + { + return (SolidBrush)SystemBrushes.Window; + } + } + + private static SolidBrush DefaultGridLineBrush + { + get + { + return (SolidBrush)SystemBrushes.Control; + } + } + + private static SolidBrush DefaultHeaderBackBrush + { + get + { + return (SolidBrush)SystemBrushes.Control; + } + } + + private static SolidBrush DefaultHeaderForeBrush + { + get + { + return (SolidBrush)SystemBrushes.ControlText; + } + } + + private static Pen DefaultHeaderForePen + { + get + { + return new Pen(SystemColors.ControlText); + } + } + + private static SolidBrush DefaultLinkBrush + { + get + { + return (SolidBrush)SystemBrushes.HotTrack; + } + } + + private bool ListHasErrors + { + get + { + return gridState[GRIDSTATE_listHasErrors]; + } + set + { + if (ListHasErrors != value) + { + gridState[GRIDSTATE_listHasErrors] = value; + ComputeMinimumRowHeaderWidth(); + if (!layout.RowHeadersVisible) + { + return; + } + + if (value) + { + if (myGridTable.IsDefault) + { + RowHeaderWidth += errorRowBitmapWidth; + } + else + { + myGridTable.RowHeaderWidth += errorRowBitmapWidth; + } + } + else + { + if (myGridTable.IsDefault) + { + RowHeaderWidth -= errorRowBitmapWidth; + } + else + { + myGridTable.RowHeaderWidth -= errorRowBitmapWidth; + } + } + } + } + } + + private bool Bound + { + get + { + return !(listManager is null || myGridTable is null); + } + } + + internal DataGridCaption Caption + { + get + { + return caption; + } + } + + /// + /// Gets or sets the background color of the caption area. + /// + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color CaptionBackColor + { + get + { + return Caption.BackColor; + } + set + { + if (IsTransparentColor(value)) + { + throw new ArgumentException(DSR.DataGridTransparentCaptionBackColorNotAllowed); + } + + Caption.BackColor = value; + } + } + + private void ResetCaptionBackColor() + { + Caption.ResetBackColor(); + } + + /// + /// Gets a value + /// indicating whether the property should be + /// persisted. + /// + protected virtual bool ShouldSerializeCaptionBackColor() + { + return Caption.ShouldSerializeBackColor(); + } + + /// + /// Gets + /// or sets the foreground color + /// of the caption area. + /// + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color CaptionForeColor + { + get + { + return Caption.ForeColor; + } + set + { + Caption.ForeColor = value; + } + } + + private void ResetCaptionForeColor() + { + Caption.ResetForeColor(); + } + + /// + /// Gets a value + /// indicating whether the property should be + /// persisted. + /// + protected virtual bool ShouldSerializeCaptionForeColor() + { + return Caption.ShouldSerializeForeColor(); + } + + /// + /// Gets or sets the font of the grid's caption. + /// + [ + SRCategory(nameof(SR.CatAppearance)), + Localizable(true), + AmbientValue(null), + ] + public Font CaptionFont + { + get + { + return Caption.Font; + } + set + { + Caption.Font = value; + } + } + + /// + /// Gets a value indicating whether the + /// caption's font is persisted. + /// + private bool ShouldSerializeCaptionFont() + { + return Caption.ShouldSerializeFont(); + } + + private void ResetCaptionFont() + { + Caption.ResetFont(); + } + + /// + /// Gets or sets the text of the grid's caption. + /// + [ + SRCategory(nameof(SR.CatAppearance)), + DefaultValue(""), + Localizable(true), + ] + public string CaptionText + { + get + { + return Caption.Text; + } + set + { + Caption.Text = value; + } + } + + /// + /// Gets or sets a value that indicates + /// whether the grid's caption is visible. + /// + [ + DefaultValue(true), + SRCategory(nameof(SR.CatDisplay)), + ] + public bool CaptionVisible + { + get + { + return layout.CaptionVisible; + } + set + { + if (layout.CaptionVisible != value) + { + layout.CaptionVisible = value; + PerformLayout(); + Invalidate(); + OnCaptionVisibleChanged(EventArgs.Empty); + } + } + } + + private static readonly object EVENT_CAPTIONVISIBLECHANGED = new object(); + + [SRCategory(nameof(SR.CatPropertyChanged))] + public event EventHandler CaptionVisibleChanged + { + add => Events.AddHandler(EVENT_CAPTIONVISIBLECHANGED, value); + remove => Events.RemoveHandler(EVENT_CAPTIONVISIBLECHANGED, value); + } + + /// + /// Gets or sets which cell has the focus. Not available at design time. + /// + [ + Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), + ] + public DataGridCell CurrentCell + { + get + { + return new DataGridCell(currentRow, currentCol); + } + set + { + // if the OnLayout event was not set in the grid, then we can't + // reliably set the currentCell on the grid. + if (layout.dirty) + { + throw new ArgumentException(DSR.DataGridSettingCurrentCellNotGood); + } + + if (value.RowNumber == currentRow && value.ColumnNumber == currentCol) + { + return; + } + + // should we throw an exception, maybe? + if (DataGridRowsLength == 0 || myGridTable.GridColumnStyles is null || myGridTable.GridColumnStyles.Count == 0) + { + return; + } + + EnsureBound(); + + int currentRowSaved = currentRow; + int currentColSaved = currentCol; + bool wasEditing = gridState[GRIDSTATE_isEditing]; + bool cellChanged = false; + + // if the position in the listManager changed under the DataGrid, + // then do not edit after setting the current cell + bool doNotEdit = false; + + int newCol = value.ColumnNumber; + int newRow = value.RowNumber; + + string errorMessage = null; + + try + { + int columnCount = myGridTable.GridColumnStyles.Count; + if (newCol < 0) + { + newCol = 0; + } + + if (newCol >= columnCount) + { + newCol = columnCount - 1; + } + + int localGridRowsLength = DataGridRowsLength; + DataGridRow[] localGridRows = DataGridRows; + + if (newRow < 0) + { + newRow = 0; + } + + if (newRow >= localGridRowsLength) + { + newRow = localGridRowsLength - 1; + } + + // Current Column changing + // + if (currentCol != newCol) + { + cellChanged = true; + int currentListManagerPosition = ListManager.Position; + int currentListManagerCount = ListManager.List.Count; + + EndEdit(); + + if (ListManager.Position != currentListManagerPosition || + currentListManagerCount != ListManager.List.Count) + { + // EndEdit changed the list. + // Reset the data grid rows and the current row inside the datagrid. + // And then exit the method. + RecreateDataGridRows(); + if (ListManager.List.Count > 0) + { + currentRow = ListManager.Position; + Edit(); + } + else + { + currentRow = -1; + } + + return; + } + + currentCol = newCol; + InvalidateRow(currentRow); + } + + // Current Row changing + // + if (currentRow != newRow) + { + cellChanged = true; + int currentListManagerPosition = ListManager.Position; + int currentListManagerCount = ListManager.List.Count; + + EndEdit(); + + if (ListManager.Position != currentListManagerPosition || + currentListManagerCount != ListManager.List.Count) + { + // EndEdit changed the list. + // Reset the data grid rows and the current row inside the datagrid. + // And then exit the method. + RecreateDataGridRows(); + if (ListManager.List.Count > 0) + { + currentRow = ListManager.Position; + Edit(); + } + else + { + currentRow = -1; + } + + return; + } + + if (currentRow < localGridRowsLength) + { + localGridRows[currentRow].OnRowLeave(); + } + + localGridRows[newRow].OnRowEnter(); + currentRow = newRow; + if (currentRowSaved < localGridRowsLength) + { + InvalidateRow(currentRowSaved); + } + + InvalidateRow(currentRow); + + if (currentRowSaved != listManager.Position) + { + // not in sync +#if DEBUG + Debug.Assert(inDataSource_PositionChanged, "currentRow and listManager.Position can be out of sync only when the listManager changes its position under the DataGrid or when navigating back"); + Debug.Assert(ListManager.Position == currentRow || listManager.Position == -1, "DataSource_PositionChanged changes the position in the grid to the position in the listManager"); +#endif //DEBUG + doNotEdit = true; + if (gridState[GRIDSTATE_isEditing]) + { + AbortEdit(); + } + } + else if (gridState[GRIDSTATE_inAddNewRow]) + { +#if DEBUG + int currentRowCount = DataGridRowsLength; +#endif // debug + // cancelCurrentEdit will change the position in the list + // to the last element in the list. and the grid will get an on position changed + // event, and will set the current cell to the last element in the dataSource. + // so unhook the PositionChanged event from the listManager; + ListManager.PositionChanged -= positionChangedHandler; + ListManager.CancelCurrentEdit(); + ListManager.Position = currentRow; + ListManager.PositionChanged += positionChangedHandler; +#if DEBUG + + Debug.Assert(currentRowSaved > currentRow, "we can only go up when we are inAddNewRow"); + Debug.Assert(currentRowCount == DataGridRowsLength, "the number of rows in the dataGrid should not change"); + Debug.Assert(currentRowCount == ListManager.Count + 1, "the listManager should have one less record"); +#endif // debug + localGridRows[DataGridRowsLength - 1] = new DataGridAddNewRow(this, myGridTable, DataGridRowsLength - 1); + SetDataGridRows(localGridRows, DataGridRowsLength); + gridState[GRIDSTATE_inAddNewRow] = false; + } + else + { + ListManager.EndCurrentEdit(); + // some special care must be given when setting the + // position in the listManager. + // if EndCurrentEdit() deleted the current row + // ( because of some filtering problem, say ) + // then we cannot go over the last row + // + if (localGridRowsLength != DataGridRowsLength) + { + Debug.Assert(localGridRowsLength == DataGridRowsLength + 1, "this is the only change that could have happened"); + currentRow = (currentRow == localGridRowsLength - 1) ? DataGridRowsLength - 1 : currentRow; + } + + if (currentRow == dataGridRowsLength - 1 && policy.AllowAdd) + { + // it may be case ( see previous comment ) + // that listManager.EndCurrentEdit changed the number of rows + // in the grid. in this case, we should not be using the old + // localGridRows in our assertion, cause they are outdated now + // + Debug.Assert(DataGridRows[currentRow] is DataGridAddNewRow, "the last row is the DataGridAddNewRow"); + AddNewRow(); + Debug.Assert(ListManager.Position == currentRow || listManager.Position == -1, "the listManager should be positioned at the last row"); + } + else + { + ListManager.Position = currentRow; + } + } + } + } + catch (Exception e) + { + errorMessage = e.Message; + } + + if (errorMessage is not null) + { + DialogResult result = RTLAwareMessageBox.Show(null, + string.Format(DSR.DataGridPushedIncorrectValueIntoColumn, errorMessage), + DSR.DataGridErrorMessageBoxCaption, MessageBoxButtons.YesNo, + MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, 0); + + if (result == DialogResult.Yes) + { + currentRow = currentRowSaved; + currentCol = currentColSaved; + Debug.Assert(currentRow == ListManager.Position || listManager.Position == -1, "the position in the list manager (" + ListManager.Position + ") is out of sync with the currentRow (" + currentRow + ")" + " and the exception is '" + errorMessage + "'"); + // this will make sure the newRow will not paint the + // row selector. + InvalidateRowHeader(newRow); + // also, make sure that we get the row selector on the currentrow, too + InvalidateRowHeader(currentRow); + if (wasEditing) + { + Edit(); + } + } + else + { + // if the user committed a row that used to be addNewRow and the backEnd rejects it, + // and then it tries to navigate down then we should stay in the addNewRow + // in this particular scenario, CancelCurrentEdit will cause the last row to be deleted, + // and this will ultimately call InvalidateRow w/ a row number larger than the number of rows + // so set the currentRow here: + if (currentRow == DataGridRowsLength - 1 && currentRowSaved == DataGridRowsLength - 2 && DataGridRows[currentRow] is DataGridAddNewRow) + { + newRow = currentRowSaved; + } + + currentRow = newRow; + Debug.Assert(result == DialogResult.No, "we only put cancel and ok on the error message box"); + listManager.PositionChanged -= positionChangedHandler; + listManager.CancelCurrentEdit(); + listManager.Position = newRow; + listManager.PositionChanged += positionChangedHandler; + currentRow = newRow; + currentCol = newCol; + if (wasEditing) + { + Edit(); + } + } + } + + if (cellChanged) + { + EnsureVisible(currentRow, currentCol); + OnCurrentCellChanged(EventArgs.Empty); + + // if the user changed the current cell using the UI, edit the new cell + // but if the user changed the current cell by changing the position in the + // listManager, then do not continue the edit + // + if (!doNotEdit) + { +#if DEBUG + Debug.Assert(!inDataSource_PositionChanged, "if the user changed the current cell using the UI, then do not edit"); +#endif // debug + Edit(); + } + + AccessibilityNotifyClients(AccessibleEvents.Focus, CurrentCellAccIndex); + AccessibilityNotifyClients(AccessibleEvents.Selection, CurrentCellAccIndex); + } + + Debug.Assert(currentRow == ListManager.Position || listManager.Position == -1, "the position in the list manager is out of sync with the currentRow"); + } + } + + internal int CurrentCellAccIndex + { + get + { + int currentCellAccIndex = 0; + currentCellAccIndex++; // ParentRowsAccessibleObject + currentCellAccIndex += myGridTable.GridColumnStyles.Count; // ColumnHeaderAccessibleObject + currentCellAccIndex += DataGridRows.Length; // DataGridRowAccessibleObject + if (horizScrollBar.Visible) // Horizontal Scroll Bar Accessible Object + { + currentCellAccIndex++; + } + + if (vertScrollBar.Visible) // Vertical Scroll Bar Accessible Object + { + currentCellAccIndex++; + } + + currentCellAccIndex += (currentRow * myGridTable.GridColumnStyles.Count) + currentCol; + return currentCellAccIndex; + } + } + + [SRCategory(nameof(SR.CatPropertyChanged))] + public event EventHandler CurrentCellChanged + { + add => Events.AddHandler(EVENT_CURRENTCELLCHANGED, value); + remove => Events.RemoveHandler(EVENT_CURRENTCELLCHANGED, value); + } + + private int CurrentColumn + { + get + { + return CurrentCell.ColumnNumber; + } + set + { + CurrentCell = new DataGridCell(currentRow, value); + } + } + + private int CurrentRow + { + get + { + return CurrentCell.RowNumber; + } + set + { + CurrentCell = new DataGridCell(value, currentCol); + } + } + + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color SelectionBackColor + { + get + { + return selectionBackBrush.Color; + } + set + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "SelectionBackColor")); + } + + if (IsTransparentColor(value)) + { + throw new ArgumentException(DSR.DataGridTransparentSelectionBackColorNotAllowed); + } + + if (!value.Equals(selectionBackBrush.Color)) + { + selectionBackBrush = new SolidBrush(value); + + InvalidateInside(); + } + } + } + + internal SolidBrush SelectionBackBrush + { + get + { + return selectionBackBrush; + } + } + + internal SolidBrush SelectionForeBrush + { + get + { + return selectionForeBrush; + } + } + + protected bool ShouldSerializeSelectionBackColor() + { + return !DefaultSelectionBackBrush.Equals(selectionBackBrush); + } + + public void ResetSelectionBackColor() + { + if (ShouldSerializeSelectionBackColor()) + { + SelectionBackColor = DefaultSelectionBackBrush.Color; + } + } + + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color SelectionForeColor + { + get + { + return selectionForeBrush.Color; + } + set + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "SelectionForeColor")); + } + + if (!value.Equals(selectionForeBrush.Color)) + { + selectionForeBrush = new SolidBrush(value); + + InvalidateInside(); + } + } + } + + protected virtual bool ShouldSerializeSelectionForeColor() + { + return !SelectionForeBrush.Equals(DefaultSelectionForeBrush); + } + + public void ResetSelectionForeColor() + { + if (ShouldSerializeSelectionForeColor()) + { + SelectionForeColor = DefaultSelectionForeBrush.Color; + } + } + + internal override bool ShouldSerializeForeColor() + { + return !DefaultForeBrush.Color.Equals(ForeColor); + } + + /// + /// Indicates whether the property should be + /// persisted. + /// + internal override bool ShouldSerializeBackColor() + { + return !DefaultBackBrush.Color.Equals(BackColor); + } + + // Don't use dataGridRows, use the accessor!!! + internal DataGridRow[] DataGridRows + { + get + { + if (dataGridRows is null) + { + CreateDataGridRows(); + } + + return dataGridRows; + } + } + + // ToolTipping + internal DataGridToolTip ToolTipProvider + { + get + { + return toolTipProvider; + } + } + + internal int ToolTipId + { + get + { + return toolTipId; + } + set + { + toolTipId = value; + } + } + + private void ResetToolTip() + { + // remove all the tool tips which are stored + for (int i = 0; i < ToolTipId; i++) + { + ToolTipProvider.RemoveToolTip(new IntPtr(i)); + } + + // add toolTips for the backButton and + // details button on the caption. + if (!parentRows.IsEmpty()) + { + bool alignRight = isRightToLeft(); + int detailsButtonWidth = Caption.GetDetailsButtonWidth(); + Rectangle backButton = Caption.GetBackButtonRect(layout.Caption, alignRight, detailsButtonWidth); + Rectangle detailsButton = Caption.GetDetailsButtonRect(layout.Caption, alignRight); + + // mirror the buttons wrt RTL property + backButton.X = MirrorRectangle(backButton, layout.Inside, isRightToLeft()); + detailsButton.X = MirrorRectangle(detailsButton, layout.Inside, isRightToLeft()); + + ToolTipProvider.AddToolTip(DSR.DataGridCaptionBackButtonToolTip, new IntPtr(0), backButton); + ToolTipProvider.AddToolTip(DSR.DataGridCaptionDetailsButtonToolTip, new IntPtr(1), detailsButton); + ToolTipId = 2; + } + else + { + ToolTipId = 0; + } + } + + /// + /// Given a cursor, this will Create the right DataGridRows + /// + private void CreateDataGridRows() + { + CurrencyManager listManager = ListManager; + DataGridTableStyle dgt = myGridTable; + InitializeColumnWidths(); + + if (listManager is null) + { + SetDataGridRows(Array.Empty(), 0); + return; + } + + int nDataGridRows = listManager.Count; + if (policy.AllowAdd) + { + nDataGridRows++; + } + + DataGridRow[] rows = new DataGridRow[nDataGridRows]; + for (int r = 0; r < listManager.Count; r++) + { + rows[r] = new DataGridRelationshipRow(this, dgt, r); + } + + if (policy.AllowAdd) + { + addNewRow = new DataGridAddNewRow(this, dgt, nDataGridRows - 1); + rows[nDataGridRows - 1] = addNewRow; + } + else + { + addNewRow = null; + } + + // SetDataGridRows(rows, rows.Length); + SetDataGridRows(rows, nDataGridRows); + } + + private void RecreateDataGridRows() + { + int nDataGridRows = 0; + CurrencyManager listManager = ListManager; + + if (listManager is not null) + { + nDataGridRows = listManager.Count; + if (policy.AllowAdd) + { + nDataGridRows++; + } + } + + SetDataGridRows(null, nDataGridRows); + } + + /// + /// Sets the array of DataGridRow objects used for + /// all row-related logic in the DataGrid. + /// + internal void SetDataGridRows(DataGridRow[] newRows, int newRowsLength) + { + dataGridRows = newRows; + dataGridRowsLength = newRowsLength; + + // update the vertical scroll bar + vertScrollBar.Maximum = Math.Max(0, DataGridRowsLength - 1); + if (firstVisibleRow > newRowsLength) + { + vertScrollBar.Value = 0; + firstVisibleRow = 0; + } + + ResetUIState(); +#if DEBUG + // sanity check: all the rows should have the same + // dataGridTable + if (newRows is not null && newRowsLength > 0) + { + DataGridTableStyle dgTable = newRows[0].DataGridTableStyle; + for (int i = 0; i < newRowsLength; i++) + { + Debug.Assert(dgTable == newRows[i].DataGridTableStyle, "how can two rows have different tableStyles?"); + } + } +#endif // DEBUG + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: There are now " + DataGridRowsLength.ToString(CultureInfo.InvariantCulture) + " rows."); + } + + internal int DataGridRowsLength + { + get + { + return dataGridRowsLength; + } + } + + /// + /// Gets or sets the data source that the grid is displaying data for. + /// + [ + DefaultValue(null), + SRCategory(nameof(SR.CatData)), + RefreshProperties(RefreshProperties.Repaint), + AttributeProvider(typeof(IListSource)), + ] + public object DataSource + { + get + { + return dataSource; + } + + set + { + if (value is not null && !(value is IList || value is IListSource)) + { + throw new ArgumentException(SR.BadDataSourceForComplexBinding); + } + + if (dataSource is not null && dataSource.Equals(value)) + { + return; + } + + // when the designer resets the dataSource to null, set the dataMember to null, too + if ((value is null || value == Convert.DBNull) && DataMember is not null && DataMember.Length != 0) + { + dataSource = null; + DataMember = string.Empty; + return; + } + + // if we are setting the dataSource and the dataMember is not a part + // of the properties in the dataSource, then set the dataMember to "" + // + if (value is not null) + { + EnforceValidDataMember(value); + } + + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: DataSource being set to " + ((value is null) ? "null" : value.ToString())); + + // when we change the dataSource, we need to clear the parent rows. + // the same goes for all the caption UI: reset it when the datasource changes. + // + ResetParentRows(); + Set_ListManager(value, DataMember, false); + } + } + + private static readonly object EVENT_DATASOURCECHANGED = new object(); + + [SRCategory(nameof(SR.CatPropertyChanged))] + public event EventHandler DataSourceChanged + { + add => Events.AddHandler(EVENT_DATASOURCECHANGED, value); + remove => Events.RemoveHandler(EVENT_DATASOURCECHANGED, value); + } + + /// + /// Gets or sets the specific table in a DataSource for the control. + /// + [ + DefaultValue(null), + SRCategory(nameof(SR.CatData)), + Editor("System.Windows.Forms.Design.DataMemberListEditor, " + Assemblies.SystemDesign, typeof(Drawing.Design.UITypeEditor)), + ] + public string DataMember + { + get + { + return dataMember; + } + set + { + if (dataMember is not null && dataMember.Equals(value)) + { + return; + } + + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: DataSource being set to " + ((value is null) ? "null" : value.ToString())); + // when we change the dataMember, we need to clear the parent rows. + // the same goes for all the caption UI: reset it when the datamember changes. + // + ResetParentRows(); + Set_ListManager(DataSource, value, false); + } + } + + public void SetDataBinding(object dataSource, string dataMember) + { + parentRows.Clear(); + originalState = null; + caption.BackButtonActive = caption.DownButtonActive = caption.BackButtonVisible = false; + caption.SetDownButtonDirection(!layout.ParentRowsVisible); + + Set_ListManager(dataSource, dataMember, false); + } + + [ + Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced), + ] + internal protected CurrencyManager ListManager + { + get + { + //try to return something useful: + if (listManager is null && BindingContext is not null && DataSource is not null) + { + return (CurrencyManager)BindingContext[DataSource, DataMember]; + } + else + { + return listManager; + } + } + set + { + throw new NotSupportedException(DSR.DataGridSetListManager); + } + } + + internal void Set_ListManager(object newDataSource, string newDataMember, bool force) + { + Set_ListManager(newDataSource, newDataMember, force, true); // true for forcing column creation + } + + // + // prerequisite: the dataMember and the dataSource should be set to the new values + // + // will do the following: + // call EndEdit on the current listManager, will unWire the listManager events, will set the listManager to the new + // reality, will wire the new listManager, will update the policy, will set the dataGridTable, will reset the ui state. + // + internal void Set_ListManager(object newDataSource, string newDataMember, bool force, bool forceColumnCreation) + { + bool dataSourceChanged = DataSource != newDataSource; + bool dataMemberChanged = DataMember != newDataMember; + + // if nothing happened, then why do any work? + if (!force && !dataSourceChanged && !dataMemberChanged && gridState[GRIDSTATE_inSetListManager]) + { + return; + } + + gridState[GRIDSTATE_inSetListManager] = true; + if (toBeDisposedEditingControl is not null) + { + Debug.Assert(Controls.Contains(toBeDisposedEditingControl)); + Controls.Remove(toBeDisposedEditingControl); + toBeDisposedEditingControl = null; + } + + bool beginUpdateInternal = true; + try + { + // will endEdit on the current listManager + UpdateListManager(); + + // unwire the events: + if (listManager is not null) + { + UnWireDataSource(); + } + + CurrencyManager oldListManager = listManager; + bool listManagerChanged = false; + // set up the new listManager + // CAUTION: we need to set up the listManager in the grid before setting the dataSource/dataMember props + // in the grid. the reason is that if the BindingContext was not yet requested, and it is created in the BindingContext prop + // then the grid will call Set_ListManager again, and eventually that means that the dataGrid::listManager will + // be hooked up twice to all the events (PositionChanged, ItemChanged, CurrentChanged) + if (newDataSource is not null && BindingContext is not null && !(newDataSource == Convert.DBNull)) + { + listManager = (CurrencyManager)BindingContext[newDataSource, newDataMember]; + } + else + { + listManager = null; + } + + // update the dataSource and the dateMember + dataSource = newDataSource; + dataMember = newDataMember ?? ""; + + listManagerChanged = (listManager != oldListManager); + + // wire the events + if (listManager is not null) + { + WireDataSource(); + // update the policy + policy.UpdatePolicy(listManager, ReadOnly); + } + + if (!Initializing) + { + if (listManager is null) + { + if (ContainsFocus && ParentInternal is null) + { + Debug.Assert(toBeDisposedEditingControl is null, "we should have removed the toBeDisposedEditingControl already"); + // if we unparent the active control then the form won't close + for (int i = 0; i < Controls.Count; i++) + { + if (Controls[i].Focused) + { + toBeDisposedEditingControl = Controls[i]; + break; + } + } + + if (toBeDisposedEditingControl == horizScrollBar || toBeDisposedEditingControl == vertScrollBar) + { + toBeDisposedEditingControl = null; + } + +#if DEBUG + else + { + Debug.Assert(toBeDisposedEditingControl is not null, "if the grid contains the focus, then the active control should be in the children of data grid control"); + Debug.Assert(editColumn is not null, "if we have an editing control should be a control in the data grid column"); + if (editColumn is DataGridTextBoxColumn) + { + Debug.Assert(((DataGridTextBoxColumn)editColumn).TextBox == toBeDisposedEditingControl, "if we have an editing control should be a control in the data grid column"); + } + } +#endif // debug; + + } + + SetDataGridRows(null, 0); + defaultTableStyle.GridColumnStyles.Clear(); + SetDataGridTable(defaultTableStyle, forceColumnCreation); + + if (toBeDisposedEditingControl is not null) + { + Controls.Add(toBeDisposedEditingControl); + } + } + } + + // PERF: if the listManager did not change, then do not: + // 1. create new rows + // 2. create new columns + // 3. compute the errors in the list + // + // when the metaDataChanges, we need to recreate + // the rows and the columns + // + if (listManagerChanged || gridState[GRIDSTATE_metaDataChanged]) + { + if (Visible) + { + BeginUpdateInternal(); + } + + if (listManager is not null) + { + // get rid of the old gridColumns + // we need to clear the old column collection even when navigating to + // a list that has a table style associated w/ it. Why? because the + // old column collection will be used by the parent rows to paint + defaultTableStyle.GridColumnStyles.ResetDefaultColumnCollection(); + + DataGridTableStyle newGridTable = dataGridTables[listManager.GetListName()]; + if (newGridTable is null) + { + SetDataGridTable(defaultTableStyle, forceColumnCreation); + } + else + { + SetDataGridTable(newGridTable, forceColumnCreation); + } + + // set the currentRow in ssync w/ the position in the listManager + currentRow = listManager.Position == -1 ? 0 : listManager.Position; + } + + // when we create the rows we need to use the current dataGridTable + // + RecreateDataGridRows(); + if (Visible) + { + EndUpdateInternal(); + } + + beginUpdateInternal = false; + + ComputeMinimumRowHeaderWidth(); + if (myGridTable.IsDefault) + { + RowHeaderWidth = Math.Max(minRowHeaderWidth, RowHeaderWidth); + } + else + { + myGridTable.RowHeaderWidth = Math.Max(minRowHeaderWidth, RowHeaderWidth); + } + + ListHasErrors = DataGridSourceHasErrors(); + + // build the list of columns and relationships + // wipe out the now invalid states + //ResetMouseState(); + + ResetUIState(); + + //layout.CaptionVisible = dataCursor is null ? false : true; + + OnDataSourceChanged(EventArgs.Empty); + } + } + finally + { + gridState[GRIDSTATE_inSetListManager] = false; + // start painting again + if (beginUpdateInternal && Visible) + { + EndUpdateInternal(); + } + } + } + + /// + /// Gets or sets index of the selected row. + /// + // will set the position in the ListManager + // + [ + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), + Browsable(false), + ] + public int CurrentRowIndex + { + get + { + if (originalState is null) + { + return listManager is null ? -1 : listManager.Position; + } + else + { + if (BindingContext is null) + { + return -1; + } + + CurrencyManager originalListManager = (CurrencyManager)BindingContext[originalState.DataSource, originalState.DataMember]; + return originalListManager.Position; + } + } + set + { + if (listManager is null) + { + throw new InvalidOperationException(DSR.DataGridSetSelectIndex); + } + + if (originalState is null) + { + listManager.Position = value; + currentRow = value; + return; + } + + // if we have a this.ListManager, then this.BindingManager cannot be null + // + CurrencyManager originalListManager = (CurrencyManager)BindingContext[originalState.DataSource, originalState.DataMember]; + originalListManager.Position = value; + + // this is for parent rows + originalState.LinkingRow = originalState.DataGridRows[value]; + + // Invalidate everything + Invalidate(); + } + } + + /// + /// Gets the collection of tables for the grid. + /// + [ + SRCategory(nameof(SR.CatData)), + DesignerSerializationVisibility(DesignerSerializationVisibility.Content), + Localizable(true), + ] + public GridTableStylesCollection TableStyles + { + get + { + return dataGridTables; + } + } + + internal new int FontHeight + { + get + { + return fontHeight; + } + } + + internal AccessibleObject ParentRowsAccessibleObject + { + get + { + return parentRows.AccessibleObject; + } + } + + internal Rectangle ParentRowsBounds + { + get + { + return layout.ParentRows; + } + } + + /// + /// Gets or sets the color of the grid lines. + /// + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color GridLineColor + { + get + { + return gridLineBrush.Color; + } + set + { + if (gridLineBrush.Color != value) + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "GridLineColor")); + } + + gridLineBrush = new SolidBrush(value); + + Invalidate(layout.Data); + } + } + } + + protected virtual bool ShouldSerializeGridLineColor() + { + return !GridLineBrush.Equals(DefaultGridLineBrush); + } + + public void ResetGridLineColor() + { + if (ShouldSerializeGridLineColor()) + { + GridLineColor = DefaultGridLineBrush.Color; + } + } + + internal SolidBrush GridLineBrush + { + get + { + return gridLineBrush; + } + } + + /// + /// Gets or sets the line style of the grid. + /// + [ + SRCategory(nameof(SR.CatAppearance)), + DefaultValue(defaultGridLineStyle), + ] + public DataGridLineStyle GridLineStyle + { + get + { + return gridLineStyle; + } + set + { + if (value is < DataGridLineStyle.None or > DataGridLineStyle.Solid) + { + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(DataGridLineStyle)); + } + + if (gridLineStyle != value) + { + gridLineStyle = value; + myGridTable.ResetRelationsUI(); + Invalidate(layout.Data); + } + } + } + + internal int GridLineWidth + { + get + { + Debug.Assert(GridLineStyle == DataGridLineStyle.Solid || GridLineStyle == DataGridLineStyle.None, "are there any other styles?"); + return GridLineStyle == DataGridLineStyle.Solid ? 1 : 0; + } + } + + /// + /// Gets or + /// sets the + /// way parent row labels are displayed. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), + DefaultValue(defaultParentRowsLabelStyle), + SRCategory(nameof(SR.CatDisplay)), + ] + public DataGridParentRowsLabelStyle ParentRowsLabelStyle + { + get + { + return parentRowsLabels; + } + + set + { + if (value is < DataGridParentRowsLabelStyle.None or > DataGridParentRowsLabelStyle.Both) + { + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(DataGridParentRowsLabelStyle)); + } + + if (parentRowsLabels != value) + { + parentRowsLabels = value; + Invalidate(layout.ParentRows); + OnParentRowsLabelStyleChanged(EventArgs.Empty); + } + } + } + + private static readonly object EVENT_PARENTROWSLABELSTYLECHANGED = new object(); + + [SRCategory(nameof(SR.CatPropertyChanged))] + public event EventHandler ParentRowsLabelStyleChanged + { + add => Events.AddHandler(EVENT_PARENTROWSLABELSTYLECHANGED, value); + remove => Events.RemoveHandler(EVENT_PARENTROWSLABELSTYLECHANGED, value); + } + + internal bool Initializing + { + get + { + return inInit; + } + } + + /// + /// Gets the index of the first visible column in a grid. + /// + [ + Browsable(false), + ] + public int FirstVisibleColumn + { + get + { + return firstVisibleCol; + } + } + + /// + /// Gets or sets a value indicating whether the grid displays in flat mode. + /// + [ + DefaultValue(false), + SRCategory(nameof(SR.CatAppearance)), + ] + public bool FlatMode + { + get + { + return gridState[GRIDSTATE_isFlatMode]; + } + set + { + if (value != FlatMode) + { + gridState[GRIDSTATE_isFlatMode] = value; + Invalidate(layout.Inside); + OnFlatModeChanged(EventArgs.Empty); + } + } + } + + private static readonly object EVENT_FLATMODECHANGED = new object(); + + [SRCategory(nameof(SR.CatPropertyChanged))] + public event EventHandler FlatModeChanged + { + add => Events.AddHandler(EVENT_FLATMODECHANGED, value); + remove => Events.RemoveHandler(EVENT_FLATMODECHANGED, value); + } + + /// + /// Gets or + /// sets the background color of all row and column headers. + /// + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color HeaderBackColor + { + get + { + return headerBackBrush.Color; + } + set + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "HeaderBackColor")); + } + + if (IsTransparentColor(value)) + { + throw new ArgumentException(DSR.DataGridTransparentHeaderBackColorNotAllowed); + } + + if (!value.Equals(headerBackBrush.Color)) + { + headerBackBrush = new SolidBrush(value); + + if (layout.RowHeadersVisible) + { + Invalidate(layout.RowHeaders); + } + + if (layout.ColumnHeadersVisible) + { + Invalidate(layout.ColumnHeaders); + } + + Invalidate(layout.TopLeftHeader); + } + } + } + + internal SolidBrush HeaderBackBrush + { + get + { + return headerBackBrush; + } + } + + protected virtual bool ShouldSerializeHeaderBackColor() + { + return !HeaderBackBrush.Equals(DefaultHeaderBackBrush); + } + + public void ResetHeaderBackColor() + { + if (ShouldSerializeHeaderBackColor()) + { + HeaderBackColor = DefaultHeaderBackBrush.Color; + } + } + + internal SolidBrush BackgroundBrush + { + get + { + return backgroundBrush; + } + } + + private void ResetBackgroundColor() + { + if (backgroundBrush is not null && BackgroundBrush != DefaultBackgroundBrush) + { + backgroundBrush.Dispose(); + backgroundBrush = null; + } + + backgroundBrush = DefaultBackgroundBrush; + } + + protected virtual bool ShouldSerializeBackgroundColor() + { + return !BackgroundBrush.Equals(DefaultBackgroundBrush); + } + + // using this property, the user can set the backGround color + /// + /// Gets or sets the background color of the grid. + /// + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color BackgroundColor + { + get + { + return backgroundBrush.Color; + } + set + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "BackgroundColor")); + } + + if (!value.Equals(backgroundBrush.Color)) + { + if (backgroundBrush is not null && BackgroundBrush != DefaultBackgroundBrush) + { + backgroundBrush.Dispose(); + backgroundBrush = null; + } + + backgroundBrush = new SolidBrush(value); + + Invalidate(layout.Inside); + OnBackgroundColorChanged(EventArgs.Empty); + } + } + } + + private static readonly object EVENT_BACKGROUNDCOLORCHANGED = new object(); + + [SRCategory(nameof(SR.CatPropertyChanged))] + public event EventHandler BackgroundColorChanged + { + add => Events.AddHandler(EVENT_BACKGROUNDCOLORCHANGED, value); + remove => Events.RemoveHandler(EVENT_BACKGROUNDCOLORCHANGED, value); + } + + /// + /// Indicates whether the property should be persisted. + /// + [ + SRCategory(nameof(SR.CatAppearance)), + ] + public Font HeaderFont + { + get + { + return (headerFont ?? Font); + } + set + { + ArgumentNullException.ThrowIfNull(value); + + if (!value.Equals(headerFont)) + { + headerFont = value; + RecalculateFonts(); + PerformLayout(); + Invalidate(layout.Inside); + } + } + } + + protected bool ShouldSerializeHeaderFont() + { + return (headerFont is not null); + } + + public void ResetHeaderFont() + { + if (headerFont is not null) + { + headerFont = null; + RecalculateFonts(); + PerformLayout(); + Invalidate(layout.Inside); + } + } + + /// + /// Resets the property to its default value. + /// + /// + /// Gets or sets the foreground color of the grid's headers. + /// + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color HeaderForeColor + { + get + { + return headerForePen.Color; + } + set + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "HeaderForeColor")); + } + + if (!value.Equals(headerForePen.Color)) + { + headerForePen = new Pen(value); + headerForeBrush = new SolidBrush(value); + + if (layout.RowHeadersVisible) + { + Invalidate(layout.RowHeaders); + } + + if (layout.ColumnHeadersVisible) + { + Invalidate(layout.ColumnHeaders); + } + + Invalidate(layout.TopLeftHeader); + } + } + } + + protected virtual bool ShouldSerializeHeaderForeColor() + { + return !HeaderForePen.Equals(DefaultHeaderForePen); + } + + public void ResetHeaderForeColor() + { + if (ShouldSerializeHeaderForeColor()) + { + HeaderForeColor = DefaultHeaderForeBrush.Color; + } + } + + internal SolidBrush HeaderForeBrush + { + get + { + return headerForeBrush; + } + } + + internal Pen HeaderForePen + { + get + { + return headerForePen; + } + } + + private void ResetHorizontalOffset() + { + horizontalOffset = 0; + negOffset = 0; + firstVisibleCol = 0; + numVisibleCols = 0; + lastTotallyVisibleCol = -1; + } + + internal int HorizontalOffset + { + get + { + return horizontalOffset; + } + set + { + //if (CompModSwitches.DataGridScrolling.TraceVerbose) Debug.WriteLine("DataGridScrolling: Set_HorizontalOffset, value = " + value.ToString()); + if (value < 0) + { + value = 0; + } + + // + // if the dataGrid is not bound ( listManager is null || gridTable is null) + // then use ResetHorizontalOffset(); + // + + int totalWidth = GetColumnWidthSum(); + int widthNotVisible = totalWidth - layout.Data.Width; + if (value > widthNotVisible && widthNotVisible > 0) + { + value = widthNotVisible; + } + + if (value == horizontalOffset) + { + return; + } + + int change = horizontalOffset - value; + horizScrollBar.Value = value; + Rectangle scroll = layout.Data; + if (layout.ColumnHeadersVisible) + { + scroll = Rectangle.Union(scroll, layout.ColumnHeaders); + } + + horizontalOffset = value; + + firstVisibleCol = ComputeFirstVisibleColumn(); + // update the lastTotallyVisibleCol + ComputeVisibleColumns(); + + if (gridState[GRIDSTATE_isScrolling]) + { + // if the user did not click on the grid yet, then do not put the edit + // control when scrolling + if (currentCol >= firstVisibleCol && currentCol < firstVisibleCol + numVisibleCols - 1 && (gridState[GRIDSTATE_isEditing] || gridState[GRIDSTATE_isNavigating])) + { + Edit(); + } + else + { + EndEdit(); + } + + // isScrolling is set to TRUE when the user scrolls. + // once we move the edit box, we finished processing the scroll event, so set isScrolling to FALSE + // to set isScrolling to TRUE, we need another scroll event. + gridState[GRIDSTATE_isScrolling] = false; + } + else + { + EndEdit(); + } + + RECT[] rects = CreateScrollableRegion(scroll); + ScrollRectangles(rects, change); + OnScroll(EventArgs.Empty); + } + } + + private void ScrollRectangles(RECT[] rects, int change) + { + if (rects is not null) + { + RECT scroll; + if (isRightToLeft()) + { + change = -change; + } + + for (int r = 0; r < rects.Length; r++) + { + scroll = rects[r]; + User32.ScrollWindow(this, + change, + 0, + ref scroll, + ref scroll); + } + } + } + + protected ScrollBar HorizScrollBar + { + get + { + return horizScrollBar; + } + } + + /// + /// Retrieves a value indicating whether odd and even + /// rows are painted using a different background color. + /// + // Cleanup eventually to be static. + internal bool LedgerStyle + { + get + { + return gridState[GRIDSTATE_isLedgerStyle]; + } + + /* + set { + if (isLedgerStyle != value) { + isLedgerStyle = value; + InvalidateInside(); + } + } + */ + } + + /// + /// Indicates whether the property should be persisted. + /// + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color LinkColor + { + get + { + return linkBrush.Color; + } + set + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "LinkColor")); + } + + if (!linkBrush.Color.Equals(value)) + { + linkBrush = new SolidBrush(value); + Invalidate(layout.Data); + } + } + } + + internal virtual bool ShouldSerializeLinkColor() + { + return !LinkBrush.Equals(DefaultLinkBrush); + } + + public void ResetLinkColor() + { + if (ShouldSerializeLinkColor()) + { + LinkColor = DefaultLinkBrush.Color; + } + } + + internal Brush LinkBrush + { + get + { + return linkBrush; + } + } + + /// + /// Gets + /// or sets the color a link changes to when + /// the mouse pointer moves over it. + /// + [ + SRCategory(nameof(SR.CatColors)), + Browsable(false), + EditorBrowsable(EditorBrowsableState.Never) + + ] + public Color LinkHoverColor + { + get + { + return LinkColor; + } + set + { + } + } + + protected virtual bool ShouldSerializeLinkHoverColor() + { + return false; + // return !LinkHoverBrush.Equals(defaultLinkHoverBrush); + } + + public void ResetLinkHoverColor() + { + /* + if (ShouldSerializeLinkHoverColor()) + LinkHoverColor = defaultLinkHoverBrush.Color;*/ + } + + /// + /// Indicates whether the property should be + /// persisted. + /// + internal Font LinkFont + { + get + { + return linkFont; + } + } + + internal int LinkFontHeight + { + get + { + return linkFontHeight; + } + } + + /// + /// Gets or sets a value + /// that specifies which links are shown and in what context. + /// + [ + DefaultValue(true), + SRCategory(nameof(SR.CatBehavior)) + ] + public bool AllowNavigation + { + get + { + return gridState[GRIDSTATE_allowNavigation]; + } + set + { + if (AllowNavigation != value) + { + gridState[GRIDSTATE_allowNavigation] = value; + // let the Caption know about this: + Caption.BackButtonActive = !parentRows.IsEmpty() && (value); + Caption.BackButtonVisible = Caption.BackButtonActive; + RecreateDataGridRows(); + + OnAllowNavigationChanged(EventArgs.Empty); + } + } + } + + private static readonly object EVENT_ALLOWNAVIGATIONCHANGED = new object(); + + [SRCategory(nameof(SR.CatPropertyChanged))] + public event EventHandler AllowNavigationChanged + { + add => Events.AddHandler(EVENT_ALLOWNAVIGATIONCHANGED, value); + remove => Events.RemoveHandler(EVENT_ALLOWNAVIGATIONCHANGED, value); + } + + [ + Browsable(false), EditorBrowsable(EditorBrowsableState.Never) + ] + public override Cursor Cursor + { + // get the cursor out of the propertyGrid. + get + { + return base.Cursor; + } + + set + { + base.Cursor = value; + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + new public event EventHandler CursorChanged + { + add => base.CursorChanged += value; + remove => base.CursorChanged -= value; + } + + [ + Browsable(false), EditorBrowsable(EditorBrowsableState.Never) + ] + public override Image BackgroundImage + { + // get the BackgroundImage out of the propertyGrid. + get + { + return base.BackgroundImage; + } + + set + { + base.BackgroundImage = value; + } + } + + [ + Browsable(false), EditorBrowsable(EditorBrowsableState.Never) + ] + public override ImageLayout BackgroundImageLayout + { + // get the BackgroundImage out of the propertyGrid. + get + { + return base.BackgroundImageLayout; + } + + set + { + base.BackgroundImageLayout = value; + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + new public event EventHandler BackgroundImageChanged + { + add => base.BackgroundImageChanged += value; + remove => base.BackgroundImageChanged -= value; + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + new public event EventHandler BackgroundImageLayoutChanged + { + add => base.BackgroundImageLayoutChanged += value; + remove => base.BackgroundImageLayoutChanged -= value; + } + + /// + /// Gets or sets the background color of parent rows. + /// + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color ParentRowsBackColor + { + get + { + return parentRows.BackColor; + } + set + { + if (IsTransparentColor(value)) + { + throw new ArgumentException(DSR.DataGridTransparentParentRowsBackColorNotAllowed); + } + + parentRows.BackColor = value; + } + } + + internal SolidBrush ParentRowsBackBrush + { + get + { + return parentRows.BackBrush; + } + } + + /// + /// Indicates whether the property should be + /// persisted. + /// + protected virtual bool ShouldSerializeParentRowsBackColor() + { + return !ParentRowsBackBrush.Equals(DefaultParentRowsBackBrush); + } + + private void ResetParentRowsBackColor() + { + if (ShouldSerializeParentRowsBackColor()) + { + parentRows.BackBrush = DefaultParentRowsBackBrush; + } + } + + /// + /// Gets or sets the foreground color of parent rows. + /// + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color ParentRowsForeColor + { + get + { + return parentRows.ForeColor; + } + set + { + parentRows.ForeColor = value; + } + } + + internal SolidBrush ParentRowsForeBrush + { + get + { + return parentRows.ForeBrush; + } + } + + /// + /// Indicates whether the property should be + /// persisted. + /// + protected virtual bool ShouldSerializeParentRowsForeColor() + { + return !ParentRowsForeBrush.Equals(DefaultParentRowsForeBrush); + } + + private void ResetParentRowsForeColor() + { + if (ShouldSerializeParentRowsForeColor()) + { + parentRows.ForeBrush = DefaultParentRowsForeBrush; + } + } + + /// + /// Gets + /// or sets the default width of the grid columns in + /// pixels. + /// + [ + DefaultValue(defaultPreferredColumnWidth), + SRCategory(nameof(SR.CatLayout)), + TypeConverter(typeof(DataGridPreferredColumnWidthTypeConverter)) + ] + public int PreferredColumnWidth + { + get + { + return preferredColumnWidth; + } + set + { + if (value < 0) + { + throw new ArgumentException(DSR.DataGridColumnWidth, nameof(PreferredColumnWidth)); + } + + if (preferredColumnWidth != value) + { + preferredColumnWidth = value; + } + } + } + + /// + /// Gets or sets the preferred row height for the control. + /// + [ + SRCategory(nameof(SR.CatLayout)), + ] + public int PreferredRowHeight + { + get + { + return preferredRowHeight; + } + set + { + if (value < 0) + { + throw new ArgumentException(DSR.DataGridRowRowHeight); + } + + preferredRowHeight = value; + } + } + + private void ResetPreferredRowHeight() + { + preferredRowHeight = defaultFontHeight + 3; + } + + protected bool ShouldSerializePreferredRowHeight() + { + return preferredRowHeight != defaultFontHeight + 3; + } + + /// + /// Gets or sets a value indicating whether the grid + /// is in read-only mode. + /// + [ + DefaultValue(false), + SRCategory(nameof(SR.CatBehavior)), + ] + public bool ReadOnly + { + get + { + return gridState[GRIDSTATE_readOnlyMode]; + } + set + { + if (ReadOnly != value) + { + bool recreateRows = false; + if (value) + { + // AllowAdd happens to have the same boolean value as whether we need to recreate rows. + recreateRows = policy.AllowAdd; + + policy.AllowRemove = false; + policy.AllowEdit = false; + policy.AllowAdd = false; + } + else + { + recreateRows |= policy.UpdatePolicy(listManager, value); + } + + gridState[GRIDSTATE_readOnlyMode] = value; + DataGridRow[] dataGridRows = DataGridRows; + if (recreateRows) + { + RecreateDataGridRows(); + + // keep the selected rows + DataGridRow[] currentDataGridRows = DataGridRows; + int rowCount = Math.Min(currentDataGridRows.Length, dataGridRows.Length); + for (int i = 0; i < rowCount; i++) + { + if (dataGridRows[i].Selected) + { + currentDataGridRows[i].Selected = true; + } + } + } + + // the addnew row needs to be updated. + PerformLayout(); + InvalidateInside(); + OnReadOnlyChanged(EventArgs.Empty); + } + } + } + + private static readonly object EVENT_READONLYCHANGED = new object(); + + [SRCategory(nameof(SR.CatPropertyChanged))] + public event EventHandler ReadOnlyChanged + { + add => Events.AddHandler(EVENT_READONLYCHANGED, value); + remove => Events.RemoveHandler(EVENT_READONLYCHANGED, value); + } + + /// + /// Gets + /// or sets a value indicating if the grid's column headers are visible. + /// + [ + SRCategory(nameof(SR.CatDisplay)), + DefaultValue(true), + ] + public bool ColumnHeadersVisible + { + get + { + return gridState[GRIDSTATE_columnHeadersVisible]; + } + set + { + if (ColumnHeadersVisible != value) + { + gridState[GRIDSTATE_columnHeadersVisible] = value; + layout.ColumnHeadersVisible = value; + PerformLayout(); + InvalidateInside(); + } + } + } + + /// + /// Gets or sets a value indicating whether the parent rows of a table are + /// visible. + /// + [ + SRCategory(nameof(SR.CatDisplay)), + DefaultValue(true), + ] + public bool ParentRowsVisible + { + get + { + return layout.ParentRowsVisible; + } + set + { + if (layout.ParentRowsVisible != value) + { + SetParentRowsVisibility(value); + + // update the caption: parentDownVisible == false corresponds to DownButtonDown == true; + // + caption.SetDownButtonDirection(!value); + + OnParentRowsVisibleChanged(EventArgs.Empty); + } + } + } + + private static readonly object EVENT_PARENTROWSVISIBLECHANGED = new object(); + + [SRCategory(nameof(SR.CatPropertyChanged))] + public event EventHandler ParentRowsVisibleChanged + { + add => Events.AddHandler(EVENT_PARENTROWSVISIBLECHANGED, value); + remove => Events.RemoveHandler(EVENT_PARENTROWSVISIBLECHANGED, value); + } + + internal bool ParentRowsIsEmpty() + { + return parentRows.IsEmpty(); + } + + /// + /// Gets or sets a value indicating whether the data grid's row headers are + /// visible. + /// + [ + SRCategory(nameof(SR.CatDisplay)), + DefaultValue(true), + ] + public bool RowHeadersVisible + { + get + { + return gridState[GRIDSTATE_rowHeadersVisible]; + } + set + { + if (RowHeadersVisible != value) + { + gridState[GRIDSTATE_rowHeadersVisible] = value; + PerformLayout(); + InvalidateInside(); + } + } + } + + [ + SRCategory(nameof(SR.CatLayout)), + DefaultValue(defaultRowHeaderWidth), + ] + public int RowHeaderWidth + { + get + { + return rowHeaderWidth; + } + set + { + value = Math.Max(minRowHeaderWidth, value); + if (rowHeaderWidth != value) + { + rowHeaderWidth = value; + if (layout.RowHeadersVisible) + { + PerformLayout(); + InvalidateInside(); + } + } + } + } + + /// + /// Gets or sets the width of headers. + /// + [ + Browsable(false), EditorBrowsable(EditorBrowsableState.Never), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), + Bindable(false) + ] + public override string Text + { + get + { + return base.Text; + } + set + { + base.Text = value; + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + new public event EventHandler TextChanged + { + add => base.TextChanged += value; + remove => base.TextChanged -= value; + } + + /// + /// Gets the vertical scroll bar of the control. + /// + [ + Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced), + ] + protected ScrollBar VertScrollBar + { + get + { + return vertScrollBar; + } + } + + /// + /// Gets the number of visible columns. + /// + [ + Browsable(false), + ] + public int VisibleColumnCount + { + get + { + return Math.Min(numVisibleCols, myGridTable is null ? 0 : myGridTable.GridColumnStyles.Count); + } + } + + /// + /// Gets the number of rows visible. + /// + [ + Browsable(false), + ] + public int VisibleRowCount + { + get + { + return numVisibleRows; + } + } + + /// + /// Gets or sets the value of the cell at + /// the specified the row and column. + /// + public object this[int rowIndex, int columnIndex] + { + get + { + EnsureBound(); + if (rowIndex < 0 || rowIndex >= DataGridRowsLength) + { + throw new ArgumentOutOfRangeException(nameof(rowIndex)); + } + + if (columnIndex < 0 || columnIndex >= myGridTable.GridColumnStyles.Count) + { + throw new ArgumentOutOfRangeException(nameof(columnIndex)); + } + + CurrencyManager listManager = this.listManager; + DataGridColumnStyle column = myGridTable.GridColumnStyles[columnIndex]; + return column.GetColumnValueAtRow(listManager, rowIndex); + } + set + { + EnsureBound(); + if (rowIndex < 0 || rowIndex >= DataGridRowsLength) + { + throw new ArgumentOutOfRangeException(nameof(rowIndex)); + } + + if (columnIndex < 0 || columnIndex >= myGridTable.GridColumnStyles.Count) + { + throw new ArgumentOutOfRangeException(nameof(columnIndex)); + } + + CurrencyManager listManager = this.listManager; + if (listManager.Position != rowIndex) + { + listManager.Position = rowIndex; + } + + DataGridColumnStyle column = myGridTable.GridColumnStyles[columnIndex]; + column.SetColumnValueAtRow(listManager, rowIndex, value); + + // invalidate the bounds of the cell only if the cell is visible + if (columnIndex >= firstVisibleCol && columnIndex <= firstVisibleCol + numVisibleCols - 1 && + rowIndex >= firstVisibleRow && rowIndex <= firstVisibleRow + numVisibleRows) + { + Rectangle bounds = GetCellBounds(rowIndex, columnIndex); + Invalidate(bounds); + } + } + } + + /// + /// Gets or sets the value of a specified . + /// + public object this[DataGridCell cell] + { + get + { + return this[cell.RowNumber, cell.ColumnNumber]; + } + set + { + this[cell.RowNumber, cell.ColumnNumber] = value; + } + } + + private void WireTableStylePropChanged(DataGridTableStyle gridTable) + { + gridTable.GridLineColorChanged += new EventHandler(GridLineColorChanged); + gridTable.GridLineStyleChanged += new EventHandler(GridLineStyleChanged); + gridTable.HeaderBackColorChanged += new EventHandler(HeaderBackColorChanged); + gridTable.HeaderFontChanged += new EventHandler(HeaderFontChanged); + gridTable.HeaderForeColorChanged += new EventHandler(HeaderForeColorChanged); + gridTable.LinkColorChanged += new EventHandler(LinkColorChanged); + gridTable.LinkHoverColorChanged += new EventHandler(LinkHoverColorChanged); + gridTable.PreferredColumnWidthChanged += new EventHandler(PreferredColumnWidthChanged); + gridTable.RowHeadersVisibleChanged += new EventHandler(RowHeadersVisibleChanged); + gridTable.ColumnHeadersVisibleChanged += new EventHandler(ColumnHeadersVisibleChanged); + gridTable.RowHeaderWidthChanged += new EventHandler(RowHeaderWidthChanged); + gridTable.AllowSortingChanged += new EventHandler(AllowSortingChanged); + } + + private void UnWireTableStylePropChanged(DataGridTableStyle gridTable) + { + gridTable.GridLineColorChanged -= new EventHandler(GridLineColorChanged); + gridTable.GridLineStyleChanged -= new EventHandler(GridLineStyleChanged); + gridTable.HeaderBackColorChanged -= new EventHandler(HeaderBackColorChanged); + gridTable.HeaderFontChanged -= new EventHandler(HeaderFontChanged); + gridTable.HeaderForeColorChanged -= new EventHandler(HeaderForeColorChanged); + gridTable.LinkColorChanged -= new EventHandler(LinkColorChanged); + gridTable.LinkHoverColorChanged -= new EventHandler(LinkHoverColorChanged); + gridTable.PreferredColumnWidthChanged -= new EventHandler(PreferredColumnWidthChanged); + gridTable.RowHeadersVisibleChanged -= new EventHandler(RowHeadersVisibleChanged); + gridTable.ColumnHeadersVisibleChanged -= new EventHandler(ColumnHeadersVisibleChanged); + gridTable.RowHeaderWidthChanged -= new EventHandler(RowHeaderWidthChanged); + gridTable.AllowSortingChanged -= new EventHandler(AllowSortingChanged); + } + + /// + /// DataSource events are handled + /// + private void WireDataSource() + { + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: WireDataSource"); + Debug.Assert(listManager is not null, "Can't wire up to a null DataSource"); + listManager.CurrentChanged += currentChangedHandler; + listManager.PositionChanged += positionChangedHandler; + listManager.ItemChanged += itemChangedHandler; + listManager.MetaDataChanged += metaDataChangedHandler; + } + + private void UnWireDataSource() + { + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: UnWireDataSource"); + Debug.Assert(listManager is not null, "Can't un wire from a null DataSource"); + listManager.CurrentChanged -= currentChangedHandler; + listManager.PositionChanged -= positionChangedHandler; + listManager.ItemChanged -= itemChangedHandler; + listManager.MetaDataChanged -= metaDataChangedHandler; + } + + // This is called after a row has been added. And I think whenever + // a row gets deleted, etc. + // We recreate our datagrid rows at this point. + private void DataSource_Changed(object sender, EventArgs ea) + { + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: DataSource_Changed"); + + // the grid will receive the dataSource_Changed event when + // allowAdd changes on the dataView. + policy.UpdatePolicy(ListManager, ReadOnly); + if (gridState[GRIDSTATE_inListAddNew]) + { + // we are adding a new row + // keep the old rows, w/ their height, expanded/collapsed information + // + Debug.Assert(policy.AllowAdd, "how can we add a new row if the policy does not allow this?"); + Debug.Assert(DataGridRowsLength == DataGridRows.Length, "how can this fail?"); + + DataGridRow[] gridRows = DataGridRows; + int currentRowCount = DataGridRowsLength; + // put the added row: + // + gridRows[currentRowCount - 1] = new DataGridRelationshipRow(this, myGridTable, currentRowCount - 1); + SetDataGridRows(gridRows, currentRowCount); + } + else if (gridState[GRIDSTATE_inAddNewRow] && !gridState[GRIDSTATE_inDeleteRow]) + { + // when the backEnd adds a row and we are still inAddNewRow + listManager.CancelCurrentEdit(); + gridState[GRIDSTATE_inAddNewRow] = false; + RecreateDataGridRows(); + } + else if (!gridState[GRIDSTATE_inDeleteRow]) + { + RecreateDataGridRows(); + currentRow = Math.Min(currentRow, listManager.Count); + } + + bool oldListHasErrors = ListHasErrors; + ListHasErrors = DataGridSourceHasErrors(); + // if we changed the ListHasErrors, then the grid is already invalidated + if (oldListHasErrors == ListHasErrors) + { + InvalidateInside(); + } + } + + private void GridLineColorChanged(object sender, EventArgs e) + { + Invalidate(layout.Data); + } + + private void GridLineStyleChanged(object sender, EventArgs e) + { + myGridTable.ResetRelationsUI(); + Invalidate(layout.Data); + } + + private void HeaderBackColorChanged(object sender, EventArgs e) + { + if (layout.RowHeadersVisible) + { + Invalidate(layout.RowHeaders); + } + + if (layout.ColumnHeadersVisible) + { + Invalidate(layout.ColumnHeaders); + } + + Invalidate(layout.TopLeftHeader); + } + + private void HeaderFontChanged(object sender, EventArgs e) + { + RecalculateFonts(); + PerformLayout(); + Invalidate(layout.Inside); + } + + private void HeaderForeColorChanged(object sender, EventArgs e) + { + if (layout.RowHeadersVisible) + { + Invalidate(layout.RowHeaders); + } + + if (layout.ColumnHeadersVisible) + { + Invalidate(layout.ColumnHeaders); + } + + Invalidate(layout.TopLeftHeader); + } + + private void LinkColorChanged(object sender, EventArgs e) + { + Invalidate(layout.Data); + } + + private void LinkHoverColorChanged(object sender, EventArgs e) + { + Invalidate(layout.Data); + } + + private void PreferredColumnWidthChanged(object sender, EventArgs e) + { + // reset the dataGridRows + SetDataGridRows(null, DataGridRowsLength); + // layout the horizontal scroll bar + PerformLayout(); + // invalidate everything + Invalidate(); + } + + private void RowHeadersVisibleChanged(object sender, EventArgs e) + { + layout.RowHeadersVisible = myGridTable is null ? false : myGridTable.RowHeadersVisible; + PerformLayout(); + InvalidateInside(); + } + + private void ColumnHeadersVisibleChanged(object sender, EventArgs e) + { + layout.ColumnHeadersVisible = myGridTable is null ? false : myGridTable.ColumnHeadersVisible; + PerformLayout(); + InvalidateInside(); + } + + private void RowHeaderWidthChanged(object sender, EventArgs e) + { + if (layout.RowHeadersVisible) + { + PerformLayout(); + InvalidateInside(); + } + } + + private void AllowSortingChanged(object sender, EventArgs e) + { + if (!myGridTable.AllowSorting && listManager is not null) + { + IList list = listManager.List; + if (list is IBindingList) + { + ((IBindingList)list).RemoveSort(); + } + } + } + + private void DataSource_RowChanged(object sender, EventArgs ea) + { + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: DataSource_RowChanged"); + // it may be the case that our cache was not updated + // to the latest changes in the list : CurrentChanged is fired before + // ListChanged. + // So invalidate the row if there is something to invalidate + DataGridRow[] rows = DataGridRows; + if (currentRow < DataGridRowsLength) + { + InvalidateRow(currentRow); + } + } + + /// + /// Fired by the DataSource when row position moves. + /// + private void DataSource_PositionChanged(object sender, EventArgs ea) + { +#if DEBUG + inDataSource_PositionChanged = true; +#endif + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: DataSource_PositionChanged to " + listManager.Position.ToString(CultureInfo.InvariantCulture)); + // the grid will get the PositionChanged event + // before the OnItemChanged event when a row will be deleted in the backEnd; + // we still want to keep the old rows when the user deletes the rows using the grid + // and we do not want to do the same work twice when the user adds a row via the grid + if (DataGridRowsLength > listManager.Count + (policy.AllowAdd ? 1 : 0) && !gridState[GRIDSTATE_inDeleteRow]) + { + Debug.Assert(!gridState[GRIDSTATE_inAddNewRow] && !gridState[GRIDSTATE_inListAddNew], "how can the list decrease when we are adding a row?"); + RecreateDataGridRows(); + } + + if (ListManager.Position != currentRow) + { + CurrentCell = new DataGridCell(listManager.Position, currentCol); + } +#if DEBUG + inDataSource_PositionChanged = false; +#endif + } + + internal void DataSource_MetaDataChanged(object sender, EventArgs e) + { + MetaDataChanged(); + } + + private bool DataGridSourceHasErrors() + { + if (listManager is null) + { + return false; + } + + for (int i = 0; i < listManager.Count; i++) + { + object errObj = listManager[i]; + if (errObj is IDataErrorInfo) + { + string errString = ((IDataErrorInfo)errObj).Error; + if (errString is not null && errString.Length != 0) + { + return true; + } + } + } + + return false; + } + + private void TableStylesCollectionChanged(object sender, CollectionChangeEventArgs ccea) + { + // if the users changed the collection of tableStyles + if (sender != dataGridTables) + { + return; + } + + if (listManager is null) + { + return; + } + + if (ccea.Action == CollectionChangeAction.Add) + { + DataGridTableStyle tableStyle = (DataGridTableStyle)ccea.Element; + if (listManager.GetListName().Equals(tableStyle.MappingName)) + { + Debug.Assert(myGridTable.IsDefault, "if the table is not default, then it had a name. how can one add another table to the collection w/ the same name and not throw an exception"); + SetDataGridTable(tableStyle, true); // true for forcing column creation + SetDataGridRows(null, 0); + } + } + else if (ccea.Action == CollectionChangeAction.Remove) + { + DataGridTableStyle tableStyle = (DataGridTableStyle)ccea.Element; + if (myGridTable.MappingName.Equals(tableStyle.MappingName)) + { + Debug.Assert(myGridTable.IsDefault, "if the table is not default, then it had a name. how can one add another table to the collection w/ the same name and not throw an exception"); + defaultTableStyle.GridColumnStyles.ResetDefaultColumnCollection(); + SetDataGridTable(defaultTableStyle, true); // true for forcing column creation + SetDataGridRows(null, 0); + } + } + else + { + Debug.Assert(ccea.Action == CollectionChangeAction.Refresh, "what else is possible?"); + // we have to search to see if the collection of table styles contains one + // w/ the same name as the list in the dataGrid + + DataGridTableStyle newGridTable = dataGridTables[listManager.GetListName()]; + if (newGridTable is null) + { + if (!myGridTable.IsDefault) + { + // get rid of the old gridColumns + defaultTableStyle.GridColumnStyles.ResetDefaultColumnCollection(); + SetDataGridTable(defaultTableStyle, true); // true for forcing column creation + SetDataGridRows(null, 0); + } + } + else + { + SetDataGridTable(newGridTable, true); // true for forcing column creation + SetDataGridRows(null, 0); + } + } + } + + private void DataSource_ItemChanged(object sender, ItemChangedEventArgs ea) + { + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: DataSource_ItemChanged at index " + ea.Index.ToString(CultureInfo.InvariantCulture)); + + // if ea.Index == -1, then we invalidate all rows. + if (ea.Index == -1) + { + DataSource_Changed(sender, EventArgs.Empty); + /* + // if there are rows which are invisible, it is more efficient to invalidata layout.Data + if (numVisibleRows <= dataGridRowsLength) + Invalidate(layout.Data); + else + { + Debug.Assert(firstVisibleRow == 0, "if all rows are visible, then how come that first row is not visible?"); + for (int i = 0; i < numVisibleRows; i++) + InvalidateRow(firstVisibleRow + numVisibleRows); + } + */ + } + else + { + // let's see how we are doing w/ the errors + object errObj = listManager[ea.Index]; + bool oldListHasErrors = ListHasErrors; + if (errObj is IDataErrorInfo) + { + if (((IDataErrorInfo)errObj).Error.Length != 0) + { + ListHasErrors = true; + } + else if (ListHasErrors) + { + // maybe there was an error that now is fixed + ListHasErrors = DataGridSourceHasErrors(); + } + } + + // Invalidate the row only if we did not change the ListHasErrors + if (oldListHasErrors == ListHasErrors) + { + InvalidateRow(ea.Index); + } + + // we need to update the edit box: + // we update the text in the edit box only when the currentRow + // equals the ea.Index + if (editColumn is not null && ea.Index == currentRow) + { + editColumn.UpdateUI(ListManager, ea.Index, null); + } + } + } + + protected virtual void OnBorderStyleChanged(EventArgs e) + { + if (Events[EVENT_BORDERSTYLECHANGED] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnCaptionVisibleChanged(EventArgs e) + { + if (Events[EVENT_CAPTIONVISIBLECHANGED] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnCurrentCellChanged(EventArgs e) + { + if (Events[EVENT_CURRENTCELLCHANGED] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnFlatModeChanged(EventArgs e) + { + if (Events[EVENT_FLATMODECHANGED] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnBackgroundColorChanged(EventArgs e) + { + if (Events[EVENT_BACKGROUNDCOLORCHANGED] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnAllowNavigationChanged(EventArgs e) + { + if (Events[EVENT_ALLOWNAVIGATIONCHANGED] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnParentRowsVisibleChanged(EventArgs e) + { + if (Events[EVENT_PARENTROWSVISIBLECHANGED] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnParentRowsLabelStyleChanged(EventArgs e) + { + if (Events[EVENT_PARENTROWSLABELSTYLECHANGED] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnReadOnlyChanged(EventArgs e) + { + if (Events[EVENT_READONLYCHANGED] is EventHandler eh) + { + eh(this, e); + } + } + + /// + /// Raises the + /// event. + /// + protected void OnNavigate(NavigateEventArgs e) + { + onNavigate?.Invoke(this, e); + } + + /* + /// + /// Raises the event. + /// + protected void OnColumnResize(EventArgs e) { + RaiseEvent(EVENT_COLUMNRESIZE, e); + } + + internal void OnLinkClick(EventArgs e) { + RaiseEvent(EVENT_LINKCLICKED, e); + } + */ + + internal void OnNodeClick(EventArgs e) + { + // if we expanded/collapsed the RelationshipRow + // then we need to layout the vertical scroll bar + // + PerformLayout(); + + // also, we need to let the hosted edit control that its + // boundaries possibly changed. do this with a call to Edit() + // do this only if the firstVisibleColumn is the editColumn + // + GridColumnStylesCollection columns = myGridTable.GridColumnStyles; + if (firstVisibleCol > -1 && firstVisibleCol < columns.Count && columns[firstVisibleCol] == editColumn) + { + Edit(); + } + + // Raise the event for the event listeners + ((EventHandler)Events[EVENT_NODECLICKED])?.Invoke(this, e); + } + + /// + /// Raises the event. + /// + protected void OnRowHeaderClick(EventArgs e) + { + onRowHeaderClick?.Invoke(this, e); + } + + /// + /// Raises the event. + /// + protected void OnScroll(EventArgs e) + { + // reset the toolTip information + if (ToolTipProvider is not null) + { + ResetToolTip(); + } + +((EventHandler)Events[EVENT_SCROLL])?.Invoke(this, e); + } + + /// + /// Listens + /// for the horizontal scrollbar's scroll + /// event. + /// + protected virtual void GridHScrolled(object sender, ScrollEventArgs se) + { + if (!Enabled) + { + return; + } + + if (DataSource is null) + { + Debug.Fail("Horizontal Scrollbar should be disabled without a DataSource."); + return; + } + + gridState[GRIDSTATE_isScrolling] = true; + +#if DEBUG + + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "DataGridScrolling: in GridHScrolled: the scroll event type:"); + switch (se.Type) + { + case ScrollEventType.SmallIncrement: + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "small increment"); + break; + case ScrollEventType.SmallDecrement: + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "small decrement"); + break; + case ScrollEventType.LargeIncrement: + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "Large decrement"); + break; + case ScrollEventType.LargeDecrement: + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "small decrement"); + break; + case ScrollEventType.ThumbPosition: + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "Thumb Position"); + break; + case ScrollEventType.ThumbTrack: + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "Thumb Track"); + break; + case ScrollEventType.First: + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "First"); + break; + case ScrollEventType.Last: + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "Last"); + break; + case ScrollEventType.EndScroll: + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "EndScroll"); + break; + } + +#endif // DEBUG + + if (se.Type == ScrollEventType.SmallIncrement || + se.Type == ScrollEventType.SmallDecrement) + { + int dCols = (se.Type == ScrollEventType.SmallIncrement) ? 1 : -1; + if (se.Type == ScrollEventType.SmallDecrement && negOffset == 0) + { + GridColumnStylesCollection cols = myGridTable.GridColumnStyles; + // if the column before the first visible column has width == 0 then skip it + for (int i = firstVisibleCol - 1; i >= 0 && cols[i].Width == 0; i--) + { + dCols--; + } + } + + if (se.Type == ScrollEventType.SmallIncrement && negOffset == 0) + { + GridColumnStylesCollection cols = myGridTable.GridColumnStyles; + for (int i = firstVisibleCol; i > -1 && i < cols.Count && cols[i].Width == 0; i++) + { + dCols++; + } + } + + ScrollRight(dCols); + se.NewValue = HorizontalOffset; + } + else if (se.Type != ScrollEventType.EndScroll) + { + HorizontalOffset = se.NewValue; + } + + gridState[GRIDSTATE_isScrolling] = false; + } + + /// + /// Listens + /// for the vertical scrollbar's scroll event. + /// + protected virtual void GridVScrolled(object sender, ScrollEventArgs se) + { + if (!Enabled) + { + return; + } + + if (DataSource is null) + { + Debug.Fail("Vertical Scrollbar should be disabled without a DataSource."); + return; + } + + gridState[GRIDSTATE_isScrolling] = true; + + try + { + se.NewValue = Math.Min(se.NewValue, DataGridRowsLength - numTotallyVisibleRows); + int dRows = se.NewValue - firstVisibleRow; + ScrollDown(dRows); + } + finally + { + gridState[GRIDSTATE_isScrolling] = false; + } + } + + private void HandleEndCurrentEdit() + { + int currentRowSaved = currentRow; + int currentColSaved = currentCol; + + string errorMessage = null; + + try + { + listManager.EndCurrentEdit(); + } + catch (Exception e) + { + errorMessage = e.Message; + } + + if (errorMessage is not null) + { + DialogResult result = RTLAwareMessageBox.Show(null, string.Format(DSR.DataGridPushedIncorrectValueIntoColumn, + errorMessage), DSR.DataGridErrorMessageBoxCaption, MessageBoxButtons.YesNo, + MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, 0); + + if (result == DialogResult.Yes) + { + currentRow = currentRowSaved; + currentCol = currentColSaved; + Debug.Assert(currentRow == ListManager.Position || listManager.Position == -1, "the position in the list manager (" + ListManager.Position + ") is out of sync with the currentRow (" + currentRow + ")" + " and the exception is '" + errorMessage + "'"); + // also, make sure that we get the row selector on the currentrow, too + InvalidateRowHeader(currentRow); + Edit(); + } + else + { + // if the user committed a row that used to be addNewRow and the backEnd rejects it, + // and then it tries to navigate down then we should stay in the addNewRow + // in this particular scenario, CancelCurrentEdit will cause the last row to be deleted, + // and this will ultimately call InvalidateRow w/ a row number larger than the number of rows + // so set the currentRow here: + Debug.Assert(result == DialogResult.No, "we only put cancel and ok on the error message box"); + listManager.PositionChanged -= positionChangedHandler; + listManager.CancelCurrentEdit(); + listManager.Position = currentRow; + listManager.PositionChanged += positionChangedHandler; + } + } + } + + /// + /// Listens + /// for the caption's back button clicked event. + /// + protected void OnBackButtonClicked(object sender, EventArgs e) + { + NavigateBack(); + + ((EventHandler)Events[EVENT_BACKBUTTONCLICK])?.Invoke(this, e); + } + + protected override void OnBackColorChanged(EventArgs e) + { + backBrush = new SolidBrush(BackColor); + Invalidate(); + + base.OnBackColorChanged(e); + } + + protected override void OnBindingContextChanged(EventArgs e) + { + if (DataSource is not null && !gridState[GRIDSTATE_inSetListManager]) + { + try + { + Set_ListManager(DataSource, DataMember, true, false); // we do not want to create columns + // if the columns are already created + // the grid should not rely on OnBindingContextChanged + // to create columns. + } + catch + { + // at runtime we will rethrow the exception + if (Site is null || !Site.DesignMode) + { + throw; + } + + RTLAwareMessageBox.Show(null, DSR.DataGridExceptionInPaint, null, + MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, 0); + + if (Visible) + { + BeginUpdateInternal(); + } + + ResetParentRows(); + + Set_ListManager(null, string.Empty, true); + if (Visible) + { + EndUpdateInternal(); + } + } + } + + base.OnBindingContextChanged(e); + } + + protected virtual void OnDataSourceChanged(EventArgs e) + { + if (Events[EVENT_DATASOURCECHANGED] is EventHandler eh) + { + eh(this, e); + } + } + + /// + /// Listens for + /// the caption's down button clicked event. + /// + protected void OnShowParentDetailsButtonClicked(object sender, EventArgs e) + { + // we need to fire the ParentRowsVisibleChanged event + // and the ParentRowsVisible property just calls SetParentRowsVisibility and + // then fires the event. + ParentRowsVisible = !caption.ToggleDownButtonDirection(); + + ((EventHandler)Events[EVENT_DOWNBUTTONCLICK])?.Invoke(this, e); + } + + protected override void OnForeColorChanged(EventArgs e) + { + foreBrush = new SolidBrush(ForeColor); + Invalidate(); + + base.OnForeColorChanged(e); + } + + protected override void OnFontChanged(EventArgs e) + { + // let the caption know about the event changed + // + Caption.OnGridFontChanged(); + RecalculateFonts(); + RecreateDataGridRows(); + // get all the rows in the parentRows stack, and modify their height + if (originalState is not null) + { + Debug.Assert(!parentRows.IsEmpty(), "if the originalState is not null, then parentRows contains at least one row"); + Stack parentStack = new Stack(); + // this is a huge performance hit: + // everytime we get/put something from/to + // the parentRows, the buttons in the dataGridCaption + // are invalidated + while (!parentRows.IsEmpty()) + { + DataGridState dgs = parentRows.PopTop(); + int rowCount = dgs.DataGridRowsLength; + + for (int i = 0; i < rowCount; i++) + { + // performance hit: this will cause to invalidate a bunch of + // stuff + + dgs.DataGridRows[i].Height = dgs.DataGridRows[i].MinimumRowHeight(dgs.GridColumnStyles); + } + + parentStack.Push(dgs); + } + + while (parentStack.Count != 0) + { + parentRows.AddParent((DataGridState)parentStack.Pop()); + } + } + + base.OnFontChanged(e); + } + +#pragma warning disable CS0419 // Ambiguous reference in cref attribute + /// + /// Raises the + /// event. + /// + protected override void OnPaintBackground(PaintEventArgs pevent) +#pragma warning restore CS0419 // Ambiguous reference in cref attribute + { + // null body + } + + /// + /// Raises the event which + /// repositions controls + /// and updates scroll bars. + /// + protected override void OnLayout(LayoutEventArgs levent) + { + // if we get a OnLayout event while the editControl changes, then just + // ignore it + // + if (gridState[GRIDSTATE_editControlChanging]) + { + return; + } + + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: OnLayout"); + base.OnLayout(levent); + + gridState[GRIDSTATE_canFocus] = false; + try + { + if (IsHandleCreated) + { + if (layout.ParentRowsVisible) + { + parentRows.OnLayout(); + } + + // reset the toolTip information + if (ToolTipProvider is not null) + { + ResetToolTip(); + } + + ComputeLayout(); + } + } + finally + { + gridState[GRIDSTATE_canFocus] = true; + } + } + + /// + /// Raises the + /// event. + /// + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + + // toolTipping + toolTipProvider = new DataGridToolTip(this); + toolTipProvider.CreateToolTipHandle(); + toolTipId = 0; + + PerformLayout(); + } + + /// + /// Raises the + /// event. + /// + protected override void OnHandleDestroyed(EventArgs e) + { + base.OnHandleDestroyed(e); + + // toolTipping + if (toolTipProvider is not null) + { + toolTipProvider.Destroy(); + toolTipProvider = null; + } + + toolTipId = 0; + } + + /// + /// Raises the + /// event. + /// + protected internal override void OnEnter(EventArgs e) + { + if (gridState[GRIDSTATE_canFocus] && !gridState[GRIDSTATE_editControlChanging]) + { + if (Bound) + { + Edit(); + } + + base.OnEnter(e); + } + } + + /// + /// Raises the + /// event. + /// + protected internal override void OnLeave(EventArgs e) + { + OnLeave_Grid(); + base.OnLeave(e); + } + + private void OnLeave_Grid() + { + gridState[GRIDSTATE_canFocus] = false; + try + { + EndEdit(); + if (listManager is not null && !gridState[GRIDSTATE_editControlChanging]) + { + if (gridState[GRIDSTATE_inAddNewRow]) + { + // if the user did not type anything + // in the addNewRow, then cancel the currentedit + listManager.CancelCurrentEdit(); + // set the addNewRow back + DataGridRow[] localGridRows = DataGridRows; + localGridRows[DataGridRowsLength - 1] = new DataGridAddNewRow(this, myGridTable, DataGridRowsLength - 1); + SetDataGridRows(localGridRows, DataGridRowsLength); + } + else + { + // this.listManager.EndCurrentEdit(); + HandleEndCurrentEdit(); + } + } + } + finally + { + gridState[GRIDSTATE_canFocus] = true; + // inAddNewRow should be set to false if the control was + // not changing + if (!gridState[GRIDSTATE_editControlChanging]) + { + gridState[GRIDSTATE_inAddNewRow] = false; + } + } + } + + /// + /// Raises the + /// event. + /// + protected override void OnKeyDown(KeyEventArgs e) + { + Debug.WriteLineIf(CompModSwitches.DataGridKeys.TraceVerbose, "DataGridKeys: OnKeyDown "); + base.OnKeyDown(e); + ProcessGridKey(e); + } + + /// + /// Raises the event. + /// + protected override void OnKeyPress(KeyPressEventArgs e) + { + Debug.WriteLineIf(CompModSwitches.DataGridKeys.TraceVerbose, "DataGridKeys: OnKeyPress " + TypeDescriptor.GetConverter(typeof(Keys)).ConvertToString(e.KeyChar)); + + base.OnKeyPress(e); + GridColumnStylesCollection coll = myGridTable.GridColumnStyles; + if (coll is not null && currentCol > 0 && currentCol < coll.Count) + { + if (!coll[currentCol].ReadOnly) + { + if (e.KeyChar > 32) + { + Edit(new string(new char[] { e.KeyChar })); + } + } + } + } + + /// + /// Raises the event. + /// + protected override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + + gridState[GRIDSTATE_childLinkFocused] = false; + gridState[GRIDSTATE_dragging] = false; + if (listManager is null) + { + return; + } + + HitTestInfo location = HitTest(e.X, e.Y); + Keys nModifier = ModifierKeys; + bool isControlDown = (nModifier & Keys.Control) == Keys.Control && (nModifier & Keys.Alt) == 0; + bool isShiftDown = (nModifier & Keys.Shift) == Keys.Shift; + + // Only left clicks for now + if (e.Button != MouseButtons.Left) + { + return; + } + + // Check column resize + if (location.type == HitTestType.ColumnResize) + { + if (e.Clicks > 1) + { + ColAutoResize(location.col); + } + else + { + ColResizeBegin(e, location.col); + } + + return; + } + + // Check row resize + if (location.type == HitTestType.RowResize) + { + if (e.Clicks > 1) + { + RowAutoResize(location.row); + } + else + { + RowResizeBegin(e, location.row); + } + + return; + } + + // Check column headers + if (location.type == HitTestType.ColumnHeader) + { + trackColumnHeader = myGridTable.GridColumnStyles[location.col].PropertyDescriptor; + return; + } + + if (location.type == HitTestType.Caption) + { + Rectangle captionRect = layout.Caption; + caption.MouseDown(e.X - captionRect.X, e.Y - captionRect.Y); + return; + } + + if (layout.Data.Contains(e.X, e.Y) || layout.RowHeaders.Contains(e.X, e.Y)) + { + // Check with the row underneath the mouse + int row = GetRowFromY(e.Y); + if (row > -1) + { + Point p = NormalizeToRow(e.X, e.Y, row); + DataGridRow[] localGridRows = DataGridRows; + if (localGridRows[row].OnMouseDown(p.X, p.Y, + layout.RowHeaders, + isRightToLeft())) + { + CommitEdit(); + + // possibly this was the last row, so then the link may not + // be visible. make it visible, by making the row visible. + // how can we be sure that the user did not click + // on a relationship and navigated to the child rows? + // check if the row is expanded: if the row is expanded, then the user clicked + // on the node. when we navigate to child rows the rows are recreated + // and are initially collapsed + localGridRows = DataGridRows; + if (row < DataGridRowsLength && (localGridRows[row] is DataGridRelationshipRow) && ((DataGridRelationshipRow)localGridRows[row]).Expanded) + { + EnsureVisible(row, 0); + } + + // show the edit box + // + Edit(); + return; + } + } + } + + // Check row headers + // + if (location.type == HitTestType.RowHeader) + { + EndEdit(); + if (!(DataGridRows[location.row] is DataGridAddNewRow)) + { + int savedCurrentRow = currentRow; + CurrentCell = new DataGridCell(location.row, currentCol); + if (location.row != savedCurrentRow && + currentRow != location.row && + currentRow == savedCurrentRow) + { + // The data grid was not able to move away from its previous current row. + // Be defensive and don't select the row. + return; + } + } + + if (isControlDown) + { + if (IsSelected(location.row)) + { + UnSelect(location.row); + } + else + { + Select(location.row); + } + } + else + { + if (lastRowSelected == -1 || !isShiftDown) + { + ResetSelection(); + Select(location.row); + } + else + { + int lowerRow = Math.Min(lastRowSelected, location.row); + int upperRow = Math.Max(lastRowSelected, location.row); + + // we need to reset the old SelectedRows. + // ResetSelection() will also reset lastRowSelected, so we + // need to save it + int saveLastRowSelected = lastRowSelected; + ResetSelection(); + lastRowSelected = saveLastRowSelected; + + DataGridRow[] rows = DataGridRows; + for (int i = lowerRow; i <= upperRow; i++) + { + rows[i].Selected = true; + numSelectedRows++; + } + + // hide the edit box: + // + EndEdit(); + return; + } + } + + lastRowSelected = location.row; + // OnRowHeaderClick(EventArgs.Empty); + return; + } + + // Check parentRows + // + if (location.type == HitTestType.ParentRows) + { + EndEdit(); + parentRows.OnMouseDown(e.X, e.Y, isRightToLeft()); + } + + // Check cell clicks + // + if (location.type == HitTestType.Cell) + { + if (myGridTable.GridColumnStyles[location.col].MouseDown(location.row, e.X, e.Y)) + { + return; + } + + DataGridCell target = new DataGridCell(location.row, location.col); + if (policy.AllowEdit && CurrentCell.Equals(target)) + { + ResetSelection(); + // + // what if only a part of the current cell is visible? + // + EnsureVisible(currentRow, currentCol); + Edit(); + } + else + { + ResetSelection(); + CurrentCell = target; + } + } + } + + /// + /// Creates the + /// event. + /// + protected override void OnMouseLeave(EventArgs e) + { + base.OnMouseLeave(e); + if (oldRow != -1) + { + DataGridRow[] localGridRows = DataGridRows; + localGridRows[oldRow].OnMouseLeft(layout.RowHeaders, isRightToLeft()); + } + + if (gridState[GRIDSTATE_overCaption]) + { + caption.MouseLeft(); + } + + // when the mouse leaves the grid control, reset the cursor to arrow + Cursor = null; + } + + internal void TextBoxOnMouseWheel(MouseEventArgs e) + { + OnMouseWheel(e); + } + + /// + /// Raises the + /// event. + /// + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + if (listManager is null) + { + return; + } + + HitTestInfo location = HitTest(e.X, e.Y); + + bool alignToRight = isRightToLeft(); + + // We need to give UI feedback when the user is resizing a column + if (gridState[GRIDSTATE_trackColResize]) + { + ColResizeMove(e); + } + + if (gridState[GRIDSTATE_trackRowResize]) + { + RowResizeMove(e); + } + + if (gridState[GRIDSTATE_trackColResize] || location.type == HitTestType.ColumnResize) + { + Cursor = Cursors.SizeWE; + return; + } + else if (gridState[GRIDSTATE_trackRowResize] || location.type == HitTestType.RowResize) + { + Cursor = Cursors.SizeNS; + return; + } + else + { + Cursor = null; + } + + if ((layout.Data.Contains(e.X, e.Y) + || (layout.RowHeadersVisible && layout.RowHeaders.Contains(e.X, e.Y)))) + { + // && (isNavigating || isEditing)) { + DataGridRow[] localGridRows = DataGridRows; + // If we are over a row, let it know about the mouse move. + int rowOver = GetRowFromY(e.Y); + // set the dragging bit: + if (lastRowSelected != -1 && !gridState[GRIDSTATE_dragging]) + { + int topRow = GetRowTop(lastRowSelected); + int bottomRow = topRow + localGridRows[lastRowSelected].Height; + int dragHeight = SystemInformation.DragSize.Height; + gridState[GRIDSTATE_dragging] = ((e.Y - topRow < dragHeight && topRow - e.Y < dragHeight) || (e.Y - bottomRow < dragHeight && bottomRow - e.Y < dragHeight)); + } + + if (rowOver > -1) + { + Point p = NormalizeToRow(e.X, e.Y, rowOver); + if (!localGridRows[rowOver].OnMouseMove(p.X, p.Y, layout.RowHeaders, alignToRight) && gridState[GRIDSTATE_dragging]) + { + // if the row did not use this, see if selection can use it + MouseButtons mouse = MouseButtons; + if (lastRowSelected != -1 && (mouse & MouseButtons.Left) == MouseButtons.Left + && !(((Control.ModifierKeys & Keys.Control) == Keys.Control) && (Control.ModifierKeys & Keys.Alt) == 0)) + { + // ResetSelection() will reset the lastRowSelected too + // + int saveLastRowSelected = lastRowSelected; + ResetSelection(); + lastRowSelected = saveLastRowSelected; + + int lowerRow = Math.Min(lastRowSelected, rowOver); + int upperRow = Math.Max(lastRowSelected, rowOver); + + DataGridRow[] rows = DataGridRows; + for (int i = lowerRow; i <= upperRow; i++) + { + rows[i].Selected = true; + numSelectedRows++; + } + } + } + } + + if (oldRow != rowOver && oldRow != -1) + { + localGridRows[oldRow].OnMouseLeft(layout.RowHeaders, alignToRight); + } + + oldRow = rowOver; + } + + // check parentRows + // + if (location.type == HitTestType.ParentRows) + { + if (parentRows is not null) + { + parentRows.OnMouseMove(e.X, e.Y); + } + } + + if (location.type == HitTestType.Caption) + { + gridState[GRIDSTATE_overCaption] = true; + Rectangle captionRect = layout.Caption; + caption.MouseOver(e.X - captionRect.X, e.Y - captionRect.Y); + return; + } + else + { + if (gridState[GRIDSTATE_overCaption]) + { + gridState[GRIDSTATE_overCaption] = false; + caption.MouseLeft(); + } + } + } + + /// + /// Raises the event. + /// + protected override void OnMouseUp(MouseEventArgs e) + { + base.OnMouseUp(e); + gridState[GRIDSTATE_dragging] = false; + if (listManager is null || myGridTable is null) + { + return; + } + + if (gridState[GRIDSTATE_trackColResize]) + { + ColResizeEnd(e); + } + + if (gridState[GRIDSTATE_trackRowResize]) + { + RowResizeEnd(e); + } + + gridState[GRIDSTATE_trackColResize] = false; + gridState[GRIDSTATE_trackRowResize] = false; + + HitTestInfo ci = HitTest(e.X, e.Y); + if ((ci.type & HitTestType.Caption) == HitTestType.Caption) + { + caption.MouseUp(e.X, e.Y); + } + + // Check column headers + if (ci.type == HitTestType.ColumnHeader) + { + PropertyDescriptor prop = myGridTable.GridColumnStyles[ci.col].PropertyDescriptor; + if (prop == trackColumnHeader) + { + ColumnHeaderClicked(trackColumnHeader); + } + } + + trackColumnHeader = null; + } + + /// + /// Raises the event. + /// + protected override void OnMouseWheel(MouseEventArgs e) + { + base.OnMouseWheel(e); + + if (e is HandledMouseEventArgs) + { + if (((HandledMouseEventArgs)e).Handled) + { + // The application event handler handled the scrolling - don't do anything more. + return; + } + + ((HandledMouseEventArgs)e).Handled = true; + } + + bool wheelingDown = true; + if ((ModifierKeys & Keys.Control) != 0) + { + wheelingDown = false; + } + + if (listManager is null || myGridTable is null) + { + return; + } + + ScrollBar sb = wheelingDown ? vertScrollBar : horizScrollBar; + if (!sb.Visible) + { + return; + } + + // so we scroll. we have to know this, cause otherwise we will call EndEdit + // and that would be wrong. + gridState[GRIDSTATE_isScrolling] = true; + wheelDelta += e.Delta; + float movePerc = (float)wheelDelta / (float)PInvoke.WHEEL_DELTA; + int move = (int)((float)SystemInformation.MouseWheelScrollLines * movePerc); + if (move != 0) + { + wheelDelta = 0; + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: OnMouseWheel move=" + move.ToString(CultureInfo.InvariantCulture)); + if (wheelingDown) + { + int newRow = firstVisibleRow - move; + newRow = Math.Max(0, Math.Min(newRow, DataGridRowsLength - numTotallyVisibleRows)); + ScrollDown(newRow - firstVisibleRow); + } + else + { + int newValue = horizScrollBar.Value + (move < 0 ? 1 : -1) * horizScrollBar.LargeChange; + HorizontalOffset = newValue; + } + } + + gridState[GRIDSTATE_isScrolling] = false; + } + + /// + /// Raises the + /// event. + /// + protected override void OnPaint(PaintEventArgs e) + { + try + { + CheckHierarchyState(); + + if (layout.dirty) + { + ComputeLayout(); + } + + Graphics g = e.Graphics; + + Region clipRegion = g.Clip; + if (layout.CaptionVisible) + { + caption.Paint(g, layout.Caption, isRightToLeft()); + } + + if (layout.ParentRowsVisible) + { + Debug.WriteLineIf(CompModSwitches.DataGridParents.TraceVerbose, "DataGridParents: Painting ParentRows " + layout.ParentRows.ToString()); + g.FillRectangle(SystemBrushes.AppWorkspace, layout.ParentRows); + parentRows.Paint(g, layout.ParentRows, isRightToLeft()); + } + + Rectangle gridRect = layout.Data; + if (layout.RowHeadersVisible) + { + gridRect = Rectangle.Union(gridRect, layout.RowHeaders); + } + + if (layout.ColumnHeadersVisible) + { + gridRect = Rectangle.Union(gridRect, layout.ColumnHeaders); + } + + g.SetClip(gridRect); + PaintGrid(g, gridRect); + g.Clip = clipRegion; + clipRegion.Dispose(); + PaintBorder(g, layout.ClientRectangle); + + g.FillRectangle(DefaultHeaderBackBrush, layout.ResizeBoxRect); + + base.OnPaint(e); // raise paint event + } + catch + { + // at runtime we will rethrow the exception + if (Site is null || !Site.DesignMode) + { + throw; + } + + gridState[GRIDSTATE_exceptionInPaint] = true; + try + { + RTLAwareMessageBox.Show(null, DSR.DataGridExceptionInPaint, null, MessageBoxButtons.OK, + MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, 0); + + if (Visible) + { + BeginUpdateInternal(); + } + + ResetParentRows(); + + Set_ListManager(null, string.Empty, true); + } + finally + { + gridState[GRIDSTATE_exceptionInPaint] = false; + if (Visible) + { + EndUpdateInternal(); + } + } + } + } + + // Since Win32 only invalidates the area that gets uncovered, + // we have to manually invalidate the old border area + /// + /// Raises the event. + /// + protected override void OnResize(EventArgs e) + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: OnResize"); + + if (layout.CaptionVisible) + { + Invalidate(layout.Caption); + } + + if (layout.ParentRowsVisible) + { + parentRows.OnResize(layout.ParentRows); + } + + int borderWidth = BorderWidth; + Rectangle right; + Rectangle bottom; + Rectangle oldClientRectangle = layout.ClientRectangle; + + right = new Rectangle(oldClientRectangle.X + oldClientRectangle.Width - borderWidth, + oldClientRectangle.Y, + borderWidth, + oldClientRectangle.Height); + bottom = new Rectangle(oldClientRectangle.X, + oldClientRectangle.Y + oldClientRectangle.Height - borderWidth, + oldClientRectangle.Width, + borderWidth); + + Rectangle newClientRectangle = ClientRectangle; + if (newClientRectangle.Width != oldClientRectangle.Width) + { + Invalidate(right); + right = new Rectangle(newClientRectangle.X + newClientRectangle.Width - borderWidth, + newClientRectangle.Y, + borderWidth, + newClientRectangle.Height); + Invalidate(right); + } + + if (newClientRectangle.Height != oldClientRectangle.Height) + { + Invalidate(bottom); + bottom = new Rectangle(newClientRectangle.X, + newClientRectangle.Y + newClientRectangle.Height - borderWidth, + newClientRectangle.Width, + borderWidth); + Invalidate(bottom); + } + + //also, invalidate the ResizeBoxRect + if (!layout.ResizeBoxRect.IsEmpty) + { + Invalidate(layout.ResizeBoxRect); + } + + layout.ClientRectangle = newClientRectangle; + + int oldFirstVisibleRow = firstVisibleRow; + base.OnResize(e); + if (isRightToLeft() || oldFirstVisibleRow != firstVisibleRow) + { + Invalidate(); + } + } + + internal void OnRowHeightChanged(DataGridRow row) + { + ClearRegionCache(); + int cy = GetRowTop(row.RowNumber); + if (cy > 0) + { + Rectangle refresh = new Rectangle + { + Y = cy, + X = layout.Inside.X, + Width = layout.Inside.Width, + Height = layout.Inside.Bottom - cy + }; + Invalidate(refresh); + } + } + + internal void ParentRowsDataChanged() + { + Debug.Assert(originalState is not null, "how can we get a list changed event from another listmanager/list while not navigating"); + + // do the reset work that is done in SetDataBindings, set_DataSource, set_DataMember; + parentRows.Clear(); + caption.BackButtonActive = caption.DownButtonActive = caption.BackButtonVisible = false; + caption.SetDownButtonDirection(!layout.ParentRowsVisible); + object dSource = originalState.DataSource; + string dMember = originalState.DataMember; + // we don't need to set the GRIDSTATE_metaDataChanged bit, cause + // the listManager from the originalState should be different from the current listManager + // + // set the originalState to null so that Set_ListManager knows that + // it has to unhook the MetaDataChanged events + originalState = null; + Set_ListManager(dSource, dMember, true); + } + + // =------------------------------------------------------------------ + // = Methods + // =------------------------------------------------------------------ + + private void AbortEdit() + { + Debug.WriteLineIf(CompModSwitches.DataGridEditing.TraceVerbose, "DataGridEditing: \t! AbortEdit"); + + // the same rules from editColumn.OnEdit + // while changing the editControl's visibility, do not + // PerformLayout on the entire DataGrid + gridState[GRIDSTATE_editControlChanging] = true; + + editColumn.Abort(editRow.RowNumber); + + // reset the editControl flag: + gridState[GRIDSTATE_editControlChanging] = false; + + gridState[GRIDSTATE_isEditing] = false; + editRow = null; + editColumn = null; + } + + /// + /// Occurs when the user navigates to a new table. + /// + [SRCategory(nameof(SR.CatAction))] + public event NavigateEventHandler Navigate + { + add => onNavigate += value; + remove => onNavigate -= value; + } + + /// + /// Occurs when a row header is clicked. + /// + protected event EventHandler RowHeaderClick + { + add => onRowHeaderClick += value; + remove => onRowHeaderClick -= value; + } + + /// + /// Adds an event handler for the 'System.Windows.Forms.DataGrid.OnNodeClick' + /// event. + /// + [SRCategory(nameof(SR.CatAction))] + internal event EventHandler NodeClick + { + add => Events.AddHandler(EVENT_NODECLICKED, value); + remove => Events.RemoveHandler(EVENT_NODECLICKED, value); + } + + /// + /// Occurs when the user scrolls the control. + /// + [SRCategory(nameof(SR.CatAction))] + public event EventHandler Scroll + { + add => Events.AddHandler(EVENT_SCROLL, value); + remove => Events.RemoveHandler(EVENT_SCROLL, value); + } + + public override ISite Site + { + get + { + return base.Site; + } + set + { + ISite temp = Site; + base.Site = value; + if (value != temp && !Disposing) + { + // we should site the tables and the columns + // only when our site changes + SubObjectsSiteChange(false); + SubObjectsSiteChange(true); + } + } + } + + internal void AddNewRow() + { + EnsureBound(); + ResetSelection(); + // EndEdit(); + UpdateListManager(); + gridState[GRIDSTATE_inListAddNew] = true; + gridState[GRIDSTATE_inAddNewRow] = true; + try + { + ListManager.AddNew(); + } + catch + { + gridState[GRIDSTATE_inListAddNew] = false; + gridState[GRIDSTATE_inAddNewRow] = false; + PerformLayout(); + InvalidateInside(); + throw; + } + + gridState[GRIDSTATE_inListAddNew] = false; + } + + /// + /// Attempts to + /// put the grid into a state where editing is + /// allowed. + /// + public bool BeginEdit(DataGridColumnStyle gridColumn, int rowNumber) + { + if (DataSource is null || myGridTable is null) + { + return false; + } + + // We deny edit requests if we are already editing a cell. + if (gridState[GRIDSTATE_isEditing]) + { + return false; + } + else + { + int col = -1; + if ((col = myGridTable.GridColumnStyles.IndexOf(gridColumn)) < 0) + { + return false; + } + + CurrentCell = new DataGridCell(rowNumber, col); + ResetSelection(); + Edit(); + return true; + } + } + + /// + /// Specifies the beginning of the initialization code. + /// + public void BeginInit() + { + if (inInit) + { + throw new InvalidOperationException(DSR.DataGridBeginInit); + } + + inInit = true; + } + + private Rectangle CalcRowResizeFeedbackRect(MouseEventArgs e) + { + Rectangle inside = layout.Data; + Rectangle r = new Rectangle(inside.X, e.Y, inside.Width, 3); + r.Y = Math.Min(inside.Bottom - 3, r.Y); + r.Y = Math.Max(r.Y, 0); + return r; + } + + private Rectangle CalcColResizeFeedbackRect(MouseEventArgs e) + { + Rectangle inside = layout.Data; + Rectangle r = new Rectangle(e.X, inside.Y, 3, inside.Height); + r.X = Math.Min(inside.Right - 3, r.X); + r.X = Math.Max(r.X, 0); + return r; + } + + private void CancelCursorUpdate() + { + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: Requesting CancelEdit()"); + if (listManager is not null) + { + EndEdit(); + listManager.CancelCurrentEdit(); + } + } + + private void CheckHierarchyState() + { + if (checkHierarchy && listManager is not null && myGridTable is not null) + { + if (myGridTable is null) + { + // there was nothing to check + return; + } + + for (int j = 0; j < myGridTable.GridColumnStyles.Count; j++) + { + DataGridColumnStyle gridColumn = myGridTable.GridColumnStyles[j]; + } + + checkHierarchy = false; + } + } + + /// + /// The DataGrid caches an array of rectangular areas + /// which represent the area which scrolls left to right. + /// This method is invoked whenever the DataGrid's + /// scrollable regions change in such a way as to require + /// a re-recalculation. + /// + private void ClearRegionCache() + { + cachedScrollableRegion = null; + } + + /// + /// Determines the best fit size for the given column. + /// + private void ColAutoResize(int col) + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: ColAutoResize"); + EndEdit(); + CurrencyManager listManager = this.listManager; + if (listManager is null) + { + return; + } + + int size; + Graphics g = CreateGraphicsInternal(); + try + { + DataGridColumnStyle column = myGridTable.GridColumnStyles[col]; + string columnName = column.HeaderText; + + Font headerFont; + if (myGridTable.IsDefault) + { + headerFont = HeaderFont; + } + else + { + headerFont = myGridTable.HeaderFont; + } + + size = (int)g.MeasureString(columnName, headerFont).Width + layout.ColumnHeaders.Height + 1; // The sort triangle's width is equal to it's height + int rowCount = listManager.Count; + for (int row = 0; row < rowCount; ++row) + { + object value = column.GetColumnValueAtRow(listManager, row); + int width = column.GetPreferredSize(g, value).Width; + if (width > size) + { + size = width; + } + } + + if (column.Width != size) + { + column._width = size; + + ComputeVisibleColumns(); + + bool lastColumnIsLastTotallyVisibleCol = true; + if (lastTotallyVisibleCol != -1) + { + for (int i = lastTotallyVisibleCol + 1; i < myGridTable.GridColumnStyles.Count; i++) + { + if (myGridTable.GridColumnStyles[i].PropertyDescriptor is not null) + { + lastColumnIsLastTotallyVisibleCol = false; + break; + } + } + } + else + { + lastColumnIsLastTotallyVisibleCol = false; + } + + // if the column shrank and the last totally visible column was the last column + // then we need to recompute the horizontalOffset, firstVisibleCol, negOffset. + // lastTotallyVisibleCol remains the last column + if (lastColumnIsLastTotallyVisibleCol && + (negOffset != 0 || horizontalOffset != 0)) + { + // update the column width + column._width = size; + + int cx = 0; + int colCount = myGridTable.GridColumnStyles.Count; + int visibleWidth = layout.Data.Width; + GridColumnStylesCollection cols = myGridTable.GridColumnStyles; + + // assume everything fits + negOffset = 0; + horizontalOffset = 0; + firstVisibleCol = 0; + + for (int i = colCount - 1; i >= 0; i--) + { + if (cols[i].PropertyDescriptor is null) + { + continue; + } + + cx += cols[i].Width; + if (cx > visibleWidth) + { + if (negOffset == 0) + { + firstVisibleCol = i; + negOffset = cx - visibleWidth; + horizontalOffset = negOffset; + numVisibleCols++; + } + else + { + horizontalOffset += cols[i].Width; + } + } + else + { + numVisibleCols++; + } + } + + // refresh the horizontal scrollbar + PerformLayout(); + + // we need to invalidate the layout.Data and layout.ColumnHeaders + Invalidate(Rectangle.Union(layout.Data, layout.ColumnHeaders)); + } + else + { + // need to refresh the scroll bar + PerformLayout(); + + Rectangle rightArea = layout.Data; + if (layout.ColumnHeadersVisible) + { + rightArea = Rectangle.Union(rightArea, layout.ColumnHeaders); + } + + int left = GetColBeg(col); + + if (!isRightToLeft()) + { + rightArea.Width -= left - rightArea.X; + rightArea.X = left; + } + else + { + rightArea.Width -= left; + } + + Invalidate(rightArea); + } + } + } + finally + { + g.Dispose(); + } + + if (horizScrollBar.Visible) + { + horizScrollBar.Value = HorizontalOffset; + } + + // OnColumnResize(EventArgs.Empty); + } + + /// + /// Collapses child relations, if any exist for all rows, or for a + /// specified row. + /// + public void Collapse(int row) + { + SetRowExpansionState(row, false); + } + + private void ColResizeBegin(MouseEventArgs e, int col) + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: ColResizeBegin"); + Debug.Assert(myGridTable is not null, "Column resizing operations can't be called when myGridTable is null."); + + int x = e.X; + EndEdit(); + Rectangle clip = Rectangle.Union(layout.ColumnHeaders, layout.Data); + if (isRightToLeft()) + { + clip.Width = GetColBeg(col) - layout.Data.X - 2; + } + else + { + int leftEdge = GetColBeg(col); + clip.X = leftEdge + 3; + clip.Width = layout.Data.X + layout.Data.Width - leftEdge - 2; + } + + Capture = true; + Cursor.Clip = RectangleToScreen(clip); + gridState[GRIDSTATE_trackColResize] = true; + trackColAnchor = x; + trackColumn = col; + + DrawColSplitBar(e); + lastSplitBar = e; + } + + private void ColResizeMove(MouseEventArgs e) + { + if (lastSplitBar is not null) + { + DrawColSplitBar(lastSplitBar); + lastSplitBar = e; + } + + DrawColSplitBar(e); + } + + private void ColResizeEnd(MouseEventArgs e) + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: ColResizeEnd"); + Debug.Assert(myGridTable is not null, "Column resizing operations can't be called when myGridTable is null."); + + gridState[GRIDSTATE_layoutSuspended] = true; + try + { + if (lastSplitBar is not null) + { + DrawColSplitBar(lastSplitBar); + lastSplitBar = null; + } + + bool rightToLeft = isRightToLeft(); + + int x = rightToLeft ? Math.Max(e.X, layout.Data.X) : Math.Min(e.X, layout.Data.Right + 1); + int delta = x - GetColEnd(trackColumn); + if (rightToLeft) + { + delta = -delta; + } + + if (trackColAnchor != x && delta != 0) + { + DataGridColumnStyle column = myGridTable.GridColumnStyles[trackColumn]; + int proposed = column.Width + delta; + proposed = Math.Max(proposed, 3); + column.Width = proposed; + + // refresh scrolling data: horizontalOffset, negOffset, firstVisibleCol, numVisibleCols, lastTotallyVisibleCol + ComputeVisibleColumns(); + + bool lastColumnIsLastTotallyVisibleCol = true; + for (int i = lastTotallyVisibleCol + 1; i < myGridTable.GridColumnStyles.Count; i++) + { + if (myGridTable.GridColumnStyles[i].PropertyDescriptor is not null) + { + lastColumnIsLastTotallyVisibleCol = false; + break; + } + } + + if (lastColumnIsLastTotallyVisibleCol && + (negOffset != 0 || horizontalOffset != 0)) + { + int cx = 0; + int colCount = myGridTable.GridColumnStyles.Count; + int visibleWidth = layout.Data.Width; + GridColumnStylesCollection cols = myGridTable.GridColumnStyles; + + // assume everything fits + negOffset = 0; + horizontalOffset = 0; + firstVisibleCol = 0; + + for (int i = colCount - 1; i > -1; i--) + { + if (cols[i].PropertyDescriptor is null) + { + continue; + } + + cx += cols[i].Width; + + if (cx > visibleWidth) + { + if (negOffset == 0) + { + negOffset = cx - visibleWidth; + firstVisibleCol = i; + horizontalOffset = negOffset; + numVisibleCols++; + } + else + { + horizontalOffset += cols[i].Width; + } + } + else + { + numVisibleCols++; + } + } + + // and invalidate pretty much everything + Invalidate(Rectangle.Union(layout.Data, layout.ColumnHeaders)); + } + else + { + Rectangle rightArea = Rectangle.Union(layout.ColumnHeaders, layout.Data); + int left = GetColBeg(trackColumn); + rightArea.Width -= rightToLeft ? rightArea.Right - left : left - rightArea.X; + rightArea.X = rightToLeft ? layout.Data.X : left; + Invalidate(rightArea); + } + } + } + finally + { + Cursor.Clip = Rectangle.Empty; + Capture = false; + gridState[GRIDSTATE_layoutSuspended] = false; + } + + PerformLayout(); + + if (horizScrollBar.Visible) + { + horizScrollBar.Value = HorizontalOffset; + } + + // OnColumnResize(EventArgs.Empty); + } + + private void MetaDataChanged() + { + // when we reset the Binding in the grid, we need to clear the parent rows. + // the same goes for all the caption UI: reset it when the datasource changes. + // + parentRows.Clear(); + caption.BackButtonActive = caption.DownButtonActive = caption.BackButtonVisible = false; + caption.SetDownButtonDirection(!layout.ParentRowsVisible); + + gridState[GRIDSTATE_metaDataChanged] = true; + try + { + if (originalState is not null) + { + // set the originalState to null so that Set_ListManager knows that + // it has to unhook the MetaDataChanged events + Set_ListManager(originalState.DataSource, originalState.DataMember, true); + originalState = null; + } + else + { + Set_ListManager(DataSource, DataMember, true); + } + } + finally + { + gridState[GRIDSTATE_metaDataChanged] = false; + } + } + + // =------------------------------------------------------------------ + // = Functions to resize rows + // =------------------------------------------------------------------ + + // will autoResize "row" + private void RowAutoResize(int row) + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: RowAutoResize"); + + EndEdit(); + CurrencyManager listManager = ListManager; + if (listManager is null) + { + return; + } + + Graphics g = CreateGraphicsInternal(); + try + { + GridColumnStylesCollection columns = myGridTable.GridColumnStyles; + DataGridRow resizeRow = DataGridRows[row]; + int rowCount = listManager.Count; + int resizeHeight = 0; + + // compute the height that we should resize to: + int columnsCount = columns.Count; + for (int col = 0; col < columnsCount; col++) + { + object value = columns[col].GetColumnValueAtRow(listManager, row); + resizeHeight = Math.Max(resizeHeight, columns[col].GetPreferredHeight(g, value)); + } + + if (resizeRow.Height != resizeHeight) + { + resizeRow.Height = resizeHeight; + + // needed to refresh scrollbar properties + PerformLayout(); + + Rectangle rightArea = layout.Data; + if (layout.RowHeadersVisible) + { + rightArea = Rectangle.Union(rightArea, layout.RowHeaders); + } + + int top = GetRowTop(row); + rightArea.Height -= rightArea.Y - top; + rightArea.Y = top; + Invalidate(rightArea); + } + } + finally + { + g.Dispose(); + } + + // OnRowResize(EventArgs.Empty); + return; + } + + private void RowResizeBegin(MouseEventArgs e, int row) + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: RowResizeBegin"); + Debug.Assert(myGridTable is not null, "Row resizing operations can't be called when myGridTable is null."); + + int y = e.Y; + EndEdit(); + Rectangle clip = Rectangle.Union(layout.RowHeaders, layout.Data); + int topEdge = GetRowTop(row); + clip.Y = topEdge + 3; + clip.Height = layout.Data.Y + layout.Data.Height - topEdge - 2; + + Capture = true; + Cursor.Clip = RectangleToScreen(clip); + gridState[GRIDSTATE_trackRowResize] = true; + trackRowAnchor = y; + trackRow = row; + + DrawRowSplitBar(e); + lastSplitBar = e; + } + + private void RowResizeMove(MouseEventArgs e) + { + if (lastSplitBar is not null) + { + DrawRowSplitBar(lastSplitBar); + lastSplitBar = e; + } + + DrawRowSplitBar(e); + } + + private void RowResizeEnd(MouseEventArgs e) + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: RowResizeEnd"); + Debug.Assert(myGridTable is not null, "Row resizing operations can't be called when myGridTable is null."); + + try + { + if (lastSplitBar is not null) + { + DrawRowSplitBar(lastSplitBar); + lastSplitBar = null; + } + + int y = Math.Min(e.Y, layout.Data.Y + layout.Data.Height + 1); + int delta = y - GetRowBottom(trackRow); + if (trackRowAnchor != y && delta != 0) + { + DataGridRow row = DataGridRows[trackRow]; + int proposed = row.Height + delta; + proposed = Math.Max(proposed, 3); + row.Height = proposed; + + // needed to refresh scrollbar properties + PerformLayout(); + + Rectangle rightArea = Rectangle.Union(layout.RowHeaders, layout.Data); + int top = GetRowTop(trackRow); + rightArea.Height -= rightArea.Y - top; + rightArea.Y = top; + Invalidate(rightArea); + } + } + finally + { + Cursor.Clip = Rectangle.Empty; + Capture = false; + } + + // OnRowResize(EventArgs.Empty); + } + + /// + /// Fires the ColumnHeaderClicked event and handles column + /// sorting. + /// + private void ColumnHeaderClicked(PropertyDescriptor prop) + { + if (!CommitEdit()) + { + return; + } + + // OnColumnHeaderClick(EventArgs.Empty); + bool allowSorting; + if (myGridTable.IsDefault) + { + allowSorting = AllowSorting; + } + else + { + allowSorting = myGridTable.AllowSorting; + } + + if (!allowSorting) + { + return; + } + + // if (CompModSwitches.DataGridCursor.OutputVerbose) Debug.WriteLine("DataGridCursor: We are about to sort column " + col.ToString()); + ListSortDirection direction = ListManager.GetSortDirection(); + PropertyDescriptor sortColumn = ListManager.GetSortProperty(); + if (sortColumn is not null && sortColumn.Equals(prop)) + { + direction = (direction == ListSortDirection.Ascending) ? ListSortDirection.Descending : ListSortDirection.Ascending; + } + else + { + // defaultSortDirection : ascending + direction = ListSortDirection.Ascending; + } + + if (listManager.Count == 0) + { + return; + } + + ListManager.SetSort(prop, direction); + ResetSelection(); + + InvalidateInside(); + } + + /// + /// Attempts to commit editing if a cell is being edited. + /// Return true if successfully commited editing. + /// Return false if editing can not be completed and the gird must + /// remain in our current Edit state. + /// + private bool CommitEdit() + { + Debug.WriteLineIf(CompModSwitches.DataGridEditing.TraceVerbose, "DataGridEditing: \t CommitEdit " + (editRow is null ? "" : editRow.RowNumber.ToString(CultureInfo.InvariantCulture))); + + // we want to commit the editing if + // 1. the user was editing or navigating around the data grid and + // 2. this is not the result of moving focus inside the data grid and + // 3. if the user was scrolling +#pragma warning disable SA1408 // Conditional expressions should declare precedence + if (!gridState[GRIDSTATE_isEditing] && !gridState[GRIDSTATE_isNavigating] || (gridState[GRIDSTATE_editControlChanging] && !gridState[GRIDSTATE_isScrolling])) +#pragma warning restore SA1408 // Conditional expressions should declare precedence + { + return true; + } + + // the same rules from editColumn.OnEdit + // flag that we are editing the Edit control, so if we get a OnLayout on the + // datagrid side of things while the edit control changes its visibility and bounds + // the datagrid does not perform a layout + gridState[GRIDSTATE_editControlChanging] = true; + + if ((editColumn is not null && editColumn.ReadOnly) || gridState[GRIDSTATE_inAddNewRow]) + { + bool focusTheGrid = false; + if (ContainsFocus) + { + focusTheGrid = true; + } + + if (focusTheGrid && gridState[GRIDSTATE_canFocus]) + { + Focus(); + } + + editColumn.ConcedeFocus(); + + // set the focus back to the grid + if (focusTheGrid && gridState[GRIDSTATE_canFocus] && CanFocus && !Focused) + { + Focus(); + } + + // reset the editControl flag + gridState[GRIDSTATE_editControlChanging] = false; + return true; + } + + bool retVal = editColumn?.Commit(ListManager, currentRow) ?? true; + + // reset the editControl flag + gridState[GRIDSTATE_editControlChanging] = false; + + if (retVal) + { + gridState[GRIDSTATE_isEditing] = false; + } + + return retVal; + } + + /// + /// Figure out how many rows we need to scroll down + /// to move targetRow into visibility. + /// + private int ComputeDeltaRows(int targetRow) + { + //Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "DataGridScrolling: ComputeDeltaRows, firstVisibleRow = " + // + firstVisibleRow.ToString() + ", " + // + "targetRow = " + targetRow.ToString()); + + if (firstVisibleRow == targetRow) + { + return 0; + } + + int dRows = 0; + int firstVisibleRowLogicalTop = -1; + int targetRowLogicalTop = -1; + int nRows = DataGridRowsLength; + int cy = 0; + DataGridRow[] localGridRows = DataGridRows; + + for (int row = 0; row < nRows; ++row) + { + if (row == firstVisibleRow) + { + firstVisibleRowLogicalTop = cy; + } + + if (row == targetRow) + { + targetRowLogicalTop = cy; + } + + if (targetRowLogicalTop != -1 && firstVisibleRowLogicalTop != -1) + { + break; + } + + cy += localGridRows[row].Height; + } + + int targetRowLogicalBottom = targetRowLogicalTop + localGridRows[targetRow].Height; + int dataLogicalBottom = layout.Data.Height + firstVisibleRowLogicalTop; + if (targetRowLogicalBottom > dataLogicalBottom) + { + // we need to move down. + int downDelta = targetRowLogicalBottom - dataLogicalBottom; + firstVisibleRowLogicalTop += downDelta; + } + else if (firstVisibleRowLogicalTop < targetRowLogicalTop) + { + // we don't need to move + return 0; + } + else + { + // we need to move up. + int upDelta = firstVisibleRowLogicalTop - targetRowLogicalTop; + firstVisibleRowLogicalTop -= upDelta; + } + + int newFirstRow = ComputeFirstVisibleRow(firstVisibleRowLogicalTop); + dRows = (newFirstRow - firstVisibleRow); + //Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "DataGridScrolling: ComputeDeltaRows returning " + dRows.ToString()); + return dRows; + } + + /// + /// Given the a logical vertical offset, figure out + /// which row number should be the first fully visible + /// row on or after the offset. + /// + private int ComputeFirstVisibleRow(int firstVisibleRowLogicalTop) + { + int first; + int nRows = DataGridRowsLength; + int cy = 0; + DataGridRow[] localGridRows = DataGridRows; + for (first = 0; first < nRows; ++first) + { + if (cy >= firstVisibleRowLogicalTop) + { + break; + } + + cy += localGridRows[first].Height; + } + + return first; + } + + /// + /// Constructs an updated Layout object. + /// + private void ComputeLayout() + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: ComputeLayout"); + + bool alignLeft = !isRightToLeft(); + Rectangle oldResizeRect = layout.ResizeBoxRect; + + // hide the EditBox + EndEdit(); + + ClearRegionCache(); + + // NOTE : Since Rectangles are structs, then assignment is a + // : copy. Therefore, after saying "Rectangle inside = newLayout.Inside", + // : we must always assign back to newLayout.Inside. + // + + // Important since all of the visibility flags will move + // to the new layout here. + LayoutData newLayout = new LayoutData(layout) + { + // Inside + Inside = ClientRectangle + }; + Rectangle inside = newLayout.Inside; + int borderWidth = BorderWidth; + inside.Inflate(-borderWidth, -borderWidth); + + Rectangle insideLeft = inside; + + // Caption + if (layout.CaptionVisible) + { + int captionHeight = captionFontHeight + 6; + Rectangle cap = newLayout.Caption; + cap = insideLeft; + cap.Height = captionHeight; + insideLeft.Y += captionHeight; + insideLeft.Height -= captionHeight; + + newLayout.Caption = cap; + } + else + { + newLayout.Caption = Rectangle.Empty; + } + + // Parent Rows + if (layout.ParentRowsVisible) + { + Rectangle parents = newLayout.ParentRows; + int parentHeight = parentRows.Height; + parents = insideLeft; + parents.Height = parentHeight; + insideLeft.Y += parentHeight; + insideLeft.Height -= parentHeight; + + newLayout.ParentRows = parents; + } + else + { + newLayout.ParentRows = Rectangle.Empty; + } + + // Headers + // + int columnHeaderHeight = headerFontHeight + 6; + if (layout.ColumnHeadersVisible) + { + Rectangle colHeaders = newLayout.ColumnHeaders; + colHeaders = insideLeft; + colHeaders.Height = columnHeaderHeight; + insideLeft.Y += columnHeaderHeight; + insideLeft.Height -= columnHeaderHeight; + + newLayout.ColumnHeaders = colHeaders; + } + else + { + newLayout.ColumnHeaders = Rectangle.Empty; + } + + bool newRowHeadersVisible = myGridTable.IsDefault ? RowHeadersVisible : myGridTable.RowHeadersVisible; + int newRowHeaderWidth = myGridTable.IsDefault ? RowHeaderWidth : myGridTable.RowHeaderWidth; + newLayout.RowHeadersVisible = newRowHeadersVisible; + if (myGridTable is not null && newRowHeadersVisible) + { + Rectangle rowHeaders = newLayout.RowHeaders; + if (alignLeft) + { + rowHeaders = insideLeft; + rowHeaders.Width = newRowHeaderWidth; + insideLeft.X += newRowHeaderWidth; + insideLeft.Width -= newRowHeaderWidth; + } + else + { + rowHeaders = insideLeft; + rowHeaders.Width = newRowHeaderWidth; + rowHeaders.X = insideLeft.Right - newRowHeaderWidth; + insideLeft.Width -= newRowHeaderWidth; + } + + newLayout.RowHeaders = rowHeaders; + + if (layout.ColumnHeadersVisible) + { + Rectangle topLeft = newLayout.TopLeftHeader; + Rectangle colHeaders = newLayout.ColumnHeaders; + if (alignLeft) + { + topLeft = colHeaders; + topLeft.Width = newRowHeaderWidth; + colHeaders.Width -= newRowHeaderWidth; + colHeaders.X += newRowHeaderWidth; + } + else + { + topLeft = colHeaders; + topLeft.Width = newRowHeaderWidth; + topLeft.X = colHeaders.Right - newRowHeaderWidth; + colHeaders.Width -= newRowHeaderWidth; + } + + newLayout.TopLeftHeader = topLeft; + newLayout.ColumnHeaders = colHeaders; + } + else + { + newLayout.TopLeftHeader = Rectangle.Empty; + } + } + else + { + newLayout.RowHeaders = Rectangle.Empty; + newLayout.TopLeftHeader = Rectangle.Empty; + } + + // The Data region + newLayout.Data = insideLeft; + newLayout.Inside = inside; + + layout = newLayout; + + LayoutScrollBars(); + + // if the user shrank the grid client area, then OnResize invalidated the old + // resize area. however, we need to invalidate the left upper corner in the new ResizeArea + // note that we can't take the Invalidate call from the OnResize method, because if the + // user enlarges the form then the old area will not be invalidated. + // + if (!oldResizeRect.Equals(layout.ResizeBoxRect) && !layout.ResizeBoxRect.IsEmpty) + { + Invalidate(layout.ResizeBoxRect); + } + + layout.dirty = false; + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: " + layout.ToString()); + } + + /// + /// Computes the number of pixels to scroll to scroll from one + /// row to another. + /// + private int ComputeRowDelta(int from, int to) + { + int first = from; + int last = to; + int sign = -1; + if (first > last) + { + first = to; + last = from; + sign = 1; + } + + DataGridRow[] localGridRows = DataGridRows; + int delta = 0; + for (int row = first; row < last; ++row) + { + delta += localGridRows[row].Height; + } + + return sign * delta; + } + + internal int MinimumRowHeaderWidth() + { + return minRowHeaderWidth; + } + + internal void ComputeMinimumRowHeaderWidth() + { + minRowHeaderWidth = errorRowBitmapWidth; // the size of the pencil, star and row selector images are the same as the image for the error bitmap + if (ListHasErrors) + { + minRowHeaderWidth += errorRowBitmapWidth; + } + + if (myGridTable is not null && myGridTable.RelationsList.Count != 0) + { + minRowHeaderWidth += 15; // the size of the plus/minus glyph and spacing around it + } + } + + /// + /// Updates the internal variables with the number of columns visible + /// inside the Grid's client rectangle. + /// + private void ComputeVisibleColumns() + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: ComputeVisibleColumns"); + EnsureBound(); + + GridColumnStylesCollection columns = myGridTable.GridColumnStyles; + + int nGridCols = columns.Count; + int cx = -negOffset; + int visibleColumns = 0; + int visibleWidth = layout.Data.Width; + int curCol = firstVisibleCol; + + // the same problem with negative numbers: + // if the width passed in is negative, then return 0 + // + // added the check for the columns.Count == 0 + // + if (visibleWidth < 0 || columns.Count == 0) + { + numVisibleCols = firstVisibleCol = 0; + lastTotallyVisibleCol = -1; + return; + } + + while (cx < visibleWidth && curCol < nGridCols) + { + // if (columns.Visible && columns.PropertyDescriptor is not null) + if (columns[curCol].PropertyDescriptor is not null) + { + cx += columns[curCol].Width; + } + + curCol++; + visibleColumns++; + } + + numVisibleCols = visibleColumns; + + // if we inflate the data area + // then we paint columns to the left of firstVisibleColumn + if (cx < visibleWidth) + { + for (int i = firstVisibleCol - 1; i > 0; i--) + { + if (cx + columns[i].Width > visibleWidth) + { + break; + } + + // if (columns.Visible && columns.PropertyDescriptor is not null) + if (columns[i].PropertyDescriptor is not null) + { + cx += columns[i].Width; + } + + visibleColumns++; + firstVisibleCol--; + } + + if (numVisibleCols != visibleColumns) + { + Debug.Assert(numVisibleCols < visibleColumns, "the number of visible columns can only grow"); + // is there space for more columns than were visible? + // if so, then we need to repaint Data and ColumnHeaders + Invalidate(layout.Data); + Invalidate(layout.ColumnHeaders); + + // update the number of visible columns to the new reality + numVisibleCols = visibleColumns; + } + } + + lastTotallyVisibleCol = firstVisibleCol + numVisibleCols - 1; + if (cx > visibleWidth) + { + if (numVisibleCols <= 1 || (numVisibleCols == 2 && negOffset != 0)) + { + // no column is entirely visible + lastTotallyVisibleCol = -1; + } + else + { + lastTotallyVisibleCol--; + } + } + } + + /// + /// Determines which column is the first visible given + /// the object's horizontalOffset. + /// + private int ComputeFirstVisibleColumn() + { + int first = 0; + if (horizontalOffset == 0) + { + negOffset = 0; + return 0; + } + + // we will check to see if myGridTables.GridColumns.Count != 0 + // because when we reset the dataGridTable, we don't have any columns, and we still + // call HorizontalOffset = 0, and that will call ComputeFirstVisibleColumn with an empty collection of columns. + if (myGridTable is not null && myGridTable.GridColumnStyles is not null && myGridTable.GridColumnStyles.Count != 0) + { + GridColumnStylesCollection columns = myGridTable.GridColumnStyles; + int cx = 0; + int nGridCols = columns.Count; + + if (columns[0].Width == -1) + { + // the columns are not initialized yet + // +#if DEBUG + for (int i = 0; i < nGridCols; i++) + { + Debug.Assert(columns[i].Width == -1, "the columns' widths should not be initialized"); + } +#endif // DEBUG + negOffset = 0; + return 0; + } + + for (first = 0; first < nGridCols; first++) + { + // if (columns[first].Visible && columns[first].PropertyDescriptor is not null); + if (columns[first].PropertyDescriptor is not null) + { + cx += columns[first].Width; + } + + if (cx > horizontalOffset) + { + break; + } + } + + // first may actually be the number of columns + // in that case all the columns fit in the layout data + if (first == nGridCols) + { + Debug.Assert(cx <= horizontalOffset, "look at the for loop before: we only exit that loop early if the cx is over the horizontal offset"); + negOffset = 0; + return 0; + } + + negOffset = columns[first].Width - (cx - horizontalOffset); + //Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "DataGridScrolling: ComputeFirstVisibleColumn, ret = " + first.ToString() + ", negOffset = " + negOffset.ToString()); + } + + return first; + } + + /// + /// Updates the internal variables with the number of rows visible + /// in a given DataGrid Layout. + /// + private void ComputeVisibleRows() + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: ComputeVisibleRows"); + EnsureBound(); + + Rectangle data = layout.Data; + int visibleHeight = data.Height; + int cy = 0; + int visibleRows = 0; + DataGridRow[] localGridRows = DataGridRows; + int numRows = DataGridRowsLength; + + // when minimizing the dataGrid window, we will get negative values for the + // layout.Data.Width and layout.Data.Height ( is this a + + if (visibleHeight < 0) + { + numVisibleRows = numTotallyVisibleRows = 0; + return; + } + + for (int i = firstVisibleRow; i < numRows; ++i) + { + if (cy > visibleHeight) + { + break; + } + + cy += localGridRows[i].Height; + visibleRows++; + } + + if (cy < visibleHeight) + { + for (int i = firstVisibleRow - 1; i >= 0; i--) + { + int height = localGridRows[i].Height; + if (cy + height > visibleHeight) + { + break; + } + + cy += height; + firstVisibleRow--; + visibleRows++; + } + } + + numVisibleRows = numTotallyVisibleRows = visibleRows; + if (cy > visibleHeight) + { + numTotallyVisibleRows--; + } + + Debug.Assert(numVisibleRows >= 0, "the number of visible rows can't be negative"); + Debug.Assert(numTotallyVisibleRows >= 0, "the number of totally visible rows can't be negative"); + } + + /// + /// Constructs the new instance of the accessibility object for this control. Subclasses + /// should not call base.CreateAccessibilityObject. + /// + protected override AccessibleObject CreateAccessibilityInstance() + { + return new DataGridAccessibleObject(this); + } + + /// + /// Creates a DataGridState representing the child table retrieved + /// from the passed DataRelation. + /// + private DataGridState CreateChildState(string relationName, DataGridRow source) + { + DataGridState dgs = new DataGridState(); + + string newDataMember; + if (string.IsNullOrEmpty(DataMember)) + { + newDataMember = relationName; + } + else + { + newDataMember = DataMember + "." + relationName; + } + + CurrencyManager childLM = (CurrencyManager)BindingContext[DataSource, newDataMember]; + + dgs.DataSource = DataSource; + dgs.DataMember = newDataMember; + dgs.ListManager = childLM; + + dgs.DataGridRows = null; + dgs.DataGridRowsLength = childLM.Count + (policy.AllowAdd ? 1 : 0); + + return dgs; + } + + /// + /// Constructs a Layout object containing the state + /// for a newly constructed DataGrid. + /// + private LayoutData CreateInitialLayoutState() + { + Debug.WriteLineIf(CompModSwitches.DataGridLayout.TraceVerbose, "DataGridLayout: CreateInitialLayoutState"); + LayoutData newLayout = new LayoutData + { + Inside = new Rectangle(), + TopLeftHeader = new Rectangle(), + ColumnHeaders = new Rectangle(), + RowHeaders = new Rectangle(), + Data = new Rectangle(), + Caption = new Rectangle(), + ParentRows = new Rectangle(), + ResizeBoxRect = new Rectangle(), + ColumnHeadersVisible = true, + RowHeadersVisible = true, + CaptionVisible = defaultCaptionVisible, + ParentRowsVisible = defaultParentRowsVisible, + ClientRectangle = ClientRectangle + }; + return newLayout; + } + + /// + /// The DataGrid caches an array of rectangular areas + /// which represent the area which scrolls left to right. + /// This method is invoked whenever the DataGrid needs + /// this scrollable region. + /// + private RECT[] CreateScrollableRegion(Rectangle scroll) + { + if (cachedScrollableRegion is not null) + { + return cachedScrollableRegion; + } + + bool alignToRight = isRightToLeft(); + + using (Region region = new Region(scroll)) + { + int nRows = numVisibleRows; + int cy = layout.Data.Y; + int cx = layout.Data.X; + DataGridRow[] localGridRows = DataGridRows; + for (int r = firstVisibleRow; r < nRows; r++) + { + int rowHeight = localGridRows[r].Height; + Rectangle rowExclude = localGridRows[r].GetNonScrollableArea(); + rowExclude.X += cx; + rowExclude.X = MirrorRectangle(rowExclude, layout.Data, alignToRight); + if (!rowExclude.IsEmpty) + { + region.Exclude(new Rectangle(rowExclude.X, + rowExclude.Y + cy, + rowExclude.Width, + rowExclude.Height)); + } + + cy += rowHeight; + } + + using (Graphics graphics = CreateGraphicsInternal()) + { + HRGN hrgn = (HRGN)region.GetHrgn(graphics); + + if (!hrgn.IsNull) + { + cachedScrollableRegion = hrgn.GetRegionRects(); + region.ReleaseHrgn((IntPtr)hrgn); + } + } + } + + return cachedScrollableRegion; + } + + /// + /// Disposes of the resources (other than memory) used + /// by the . + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (vertScrollBar is not null) + { + vertScrollBar.Dispose(); + } + + if (horizScrollBar is not null) + { + horizScrollBar.Dispose(); + } + + if (toBeDisposedEditingControl is not null) + { + toBeDisposedEditingControl.Dispose(); + toBeDisposedEditingControl = null; + } + + GridTableStylesCollection tables = TableStyles; + if (tables is not null) + { +#if DEBUG + Debug.Assert(myGridTable is null || myGridTable.IsDefault || tables.Contains(myGridTable), "how come that the currentTable is not in the list of tables?"); +#endif // DEBUG + for (int i = 0; i < tables.Count; i++) + { + tables[i].Dispose(); + } + } + } + + base.Dispose(disposing); + } + + /// + /// Draws an XOR region to give UI feedback for Column Resizing. + /// This looks just like the Splitter control's UI when resizing. + /// + private void DrawColSplitBar(MouseEventArgs e) + { + Rectangle r = CalcColResizeFeedbackRect(e); + DrawSplitBar(r); + } + + /// + /// Draws an XOR region to give UI feedback for Row Resizing. + /// This looks just like the Splitter control's UI when resizing. + /// + private void DrawRowSplitBar(MouseEventArgs e) + { + Rectangle r = CalcRowResizeFeedbackRect(e); + DrawSplitBar(r); + } + + /// + /// Draws an XOR region to give UI feedback for Column/Row Resizing. + /// This looks just like the Splitter control's UI when resizing. + /// + private void DrawSplitBar(Rectangle r) + { + using GetDcScope dc = new(HWND, HRGN.Null, GET_DCX_FLAGS.DCX_CACHE | GET_DCX_FLAGS.DCX_LOCKWINDOWUPDATE); + HBRUSH halftone = ControlPaint.CreateHalftoneHBRUSH(); + HGDIOBJ saveBrush = PInvokeCore.SelectObject(dc, halftone); + PInvoke.PatBlt(dc, r.X, r.Y, r.Width, r.Height, ROP_CODE.PATINVERT); + PInvokeCore.SelectObject(dc, saveBrush); + PInvokeCore.DeleteObject(halftone); + } + + /// + /// Begin in-place editing of a cell. Any editing is commited + /// before the new edit takes place. + /// + /// This will always edit the currentCell + /// If you want to edit another cell than the current one, just reset CurrentCell + /// + private void Edit() + { + Edit(null); + } + + private void Edit(string displayText) + { + EnsureBound(); + + // we want to be able to edit a cell which is not visible, as in the case with editing and scrolling + // at the same time. So do not call Ensure Visible + // + // EnsureVisible(currentRow, currentCol); + + bool cellIsVisible = true; + + // whoever needs to call ResetSelection should not rely on + // Edit() to call it; + // + // ResetSelection(); + + EndEdit(); + + Debug.WriteLineIf(CompModSwitches.DataGridEditing.TraceVerbose, "DataGridEditing: Edit, currentRow = " + currentRow.ToString(CultureInfo.InvariantCulture) + + ", currentCol = " + currentCol.ToString(CultureInfo.InvariantCulture) + (displayText is not null ? ", displayText= " + displayText : "")); + + /* allow navigation even if the policy does not allow editing + if (!policy.AllowEdit) + return; + */ + + DataGridRow[] localGridRows = DataGridRows; + + // what do you want to edit when there are no rows? + if (DataGridRowsLength == 0) + { + return; + } + + localGridRows[currentRow].OnEdit(); + editRow = localGridRows[currentRow]; + + // if the list has no columns, then what good is an edit? + if (myGridTable.GridColumnStyles.Count == 0) + { + return; + } + + // what if the currentCol does not have a propDesc? + editColumn = myGridTable.GridColumnStyles[currentCol]; + if (editColumn.PropertyDescriptor is null) + { + return; + } + + Rectangle cellBounds = Rectangle.Empty; + if (currentRow < firstVisibleRow || currentRow > firstVisibleRow + numVisibleRows || + currentCol < firstVisibleCol || currentCol > firstVisibleCol + numVisibleCols - 1 || + (currentCol == firstVisibleCol && negOffset != 0)) + { + cellIsVisible = false; + } + else + { + cellBounds = GetCellBounds(currentRow, currentCol); + } + + gridState[GRIDSTATE_isNavigating] = true; + gridState[GRIDSTATE_isEditing] = false; + + // once we call editColumn.Edit on a DataGridTextBoxColumn + // the edit control will become visible, and its bounds will get set. + // both actions cause Edit.Parent.OnLayout + // so we flag this change, cause we don't want to PerformLayout on the entire DataGrid + // everytime the edit column gets edited + gridState[GRIDSTATE_editControlChanging] = true; + + editColumn.Edit(ListManager, + currentRow, + cellBounds, + myGridTable.ReadOnly || ReadOnly || !policy.AllowEdit, + displayText, + cellIsVisible); + + // reset the editControlChanging to false + gridState[GRIDSTATE_editControlChanging] = false; + } + + /// + /// Requests an end to an edit operation taking place on the + /// + /// control. + /// + public bool EndEdit(DataGridColumnStyle gridColumn, int rowNumber, bool shouldAbort) + { + bool ret = false; + if (gridState[GRIDSTATE_isEditing]) + { + if (gridColumn != editColumn) + { + Debug.WriteLineIf(CompModSwitches.DataGridEditing.TraceVerbose, "DataGridEditing: EndEdit requested on a column we are not editing."); + } + + if (rowNumber != editRow.RowNumber) + { + Debug.WriteLineIf(CompModSwitches.DataGridEditing.TraceVerbose, "DataGridEditing: EndEdit requested on a row we are not editing."); + } + + if (shouldAbort) + { + AbortEdit(); + ret = true; + } + else + { + ret = CommitEdit(); + } + } + + return ret; + } + + /// + /// Ends any editing in progress by attempting to commit and then + /// aborting if not possible. + /// + private void EndEdit() + { + Debug.WriteLineIf(CompModSwitches.DataGridEditing.TraceVerbose, "DataGridEditing: EndEdit"); + + if (!gridState[GRIDSTATE_isEditing] && !gridState[GRIDSTATE_isNavigating]) + { + return; + } + + if (!CommitEdit()) + { + AbortEdit(); + } + } + + // PERF: we attempt to create a ListManager for the DataSource/DateMember combination + // we do this in order to check for a valid DataMember + // if the check succeeds, then it means that we actully put the listManager in the BindingContext's + // list of BindingManagers. this is fine, cause if the check succeds, then Set_ListManager + // will be called, and this will get the listManager from the bindingManagerBase hashTable kept in the BindingContext + // + // this will work if the dataMember does not contain any dots ('.') + // if the dataMember contains dots, then it will be more complicated: maybe part of the binding path + // is valid w/ the new dataSource + // but we can leave w/ this, cause in the designer the dataMember will be only a column name. and the DataSource/DataMember + // properties are for use w/ the designer. + // + private void EnforceValidDataMember(object value) + { + Debug.Assert(value is not null, "we should not have a null dataSource when we want to check for a valid dataMember"); + if (DataMember is null || DataMember.Length == 0) + { + return; + } + + if (BindingContext is null) + { + return; + } + + // + try + { + BindingManagerBase bm = BindingContext[value, dataMember]; + } + catch + { + dataMember = string.Empty; + } + } + + // will be used by the columns to tell the grid that + // editing is taken place (ie, the grid is no longer in the + // editOrNavigateMode) + // + // also, tell the current row to lose child focus + // + internal protected virtual void ColumnStartedEditing(Rectangle bounds) + { + Debug.Assert(currentRow >= firstVisibleRow && currentRow <= firstVisibleRow + numVisibleRows, "how can one edit a row which is invisible?"); + DataGridRow[] localGridRows = DataGridRows; + + if (bounds.IsEmpty && editColumn is DataGridTextBoxColumn && currentRow != -1 && currentCol != -1) + { + // set the bounds on the control + // this will only work w/ our DataGridTexBox control + DataGridTextBoxColumn col = editColumn as DataGridTextBoxColumn; + Rectangle editBounds = GetCellBounds(currentRow, currentCol); + + gridState[GRIDSTATE_editControlChanging] = true; + try + { + col.TextBox.Bounds = editBounds; + } + finally + { + gridState[GRIDSTATE_editControlChanging] = false; + } + } + + if (gridState[GRIDSTATE_inAddNewRow]) + { + int currentRowCount = DataGridRowsLength; + DataGridRow[] newDataGridRows = new DataGridRow[currentRowCount + 1]; + for (int i = 0; i < currentRowCount; i++) + { + newDataGridRows[i] = localGridRows[i]; + } + + // put the AddNewRow + newDataGridRows[currentRowCount] = new DataGridAddNewRow(this, myGridTable, currentRowCount); + SetDataGridRows(newDataGridRows, currentRowCount + 1); + + Edit(); + // put this after the call to edit so that + // CommitEdit knows that the inAddNewRow is true; + gridState[GRIDSTATE_inAddNewRow] = false; + gridState[GRIDSTATE_isEditing] = true; + gridState[GRIDSTATE_isNavigating] = false; + return; + } + + gridState[GRIDSTATE_isEditing] = true; + gridState[GRIDSTATE_isNavigating] = false; + InvalidateRowHeader(currentRow); + + // tell the current row to lose the childFocuse + if (currentRow < localGridRows.Length) + { + localGridRows[currentRow].LoseChildFocus(layout.RowHeaders, isRightToLeft()); + } + } + + internal protected virtual void ColumnStartedEditing(Control editingControl) + { + if (editingControl is null) + { + return; + } + + ColumnStartedEditing(editingControl.Bounds); + } + + /// + /// Displays child relations, if any exist, for all rows or a + /// specific row. + /// + public void Expand(int row) + { + SetRowExpansionState(row, true); + } + + /// + /// Creates a using the specified . + /// + // protected and virtual because the SimpleDropdownDataGrid will override this + protected virtual DataGridColumnStyle CreateGridColumn(PropertyDescriptor prop, bool isDefault) + { + return myGridTable?.CreateGridColumn(prop, isDefault); + } + + protected virtual DataGridColumnStyle CreateGridColumn(PropertyDescriptor prop) + { + return myGridTable?.CreateGridColumn(prop); + } + +#if PARENT_LINKS + + private ListManager ListManagerForChildColumn(ListManager childListManager, PropertyDescriptor prop) + { + /* + DataKey key; + RelationsCollection relCollection = dataColumn.Table.ParentRelations; + */ + + // this will give us the list of properties of the child + PropertyDescriptorCollection propCollection = childListManager.GetItemProperties(); + + int relCount = propCollection.Count; + for (int i=0;i + /// Specifies the end of the initialization code. + /// + public void EndInit() + { + inInit = false; + if (myGridTable is null && ListManager is not null) + { + SetDataGridTable(TableStyles[ListManager.GetListName()], true); // true for forcing column creation + } + + if (myGridTable is not null) + { + myGridTable.DataGrid = this; + } + } + + /// + /// Given a x coordinate, returns the column it is over. + /// + private int GetColFromX(int x) + { + if (myGridTable is null) + { + return -1; + } + + Rectangle inside = layout.Data; + Debug.Assert(x >= inside.X && x < inside.Right, "x must be inside the horizontal bounds of layout.Data"); + + x = MirrorPoint(x, inside, isRightToLeft()); + + GridColumnStylesCollection columns = myGridTable.GridColumnStyles; + int columnCount = columns.Count; + + int cx = inside.X - negOffset; + int col = firstVisibleCol; + while (cx < inside.Width + inside.X && col < columnCount) + { + // if (columns[col].Visible && columns[col].PropertyDescriptor is not null) + if (columns[col].PropertyDescriptor is not null) + { + cx += columns[col].Width; + } + + if (cx > x) + { + return col; + } + + ++col; + } + + return -1; + } + + /// + /// Returns the coordinate of the left edge of the given column + /// Bi-Di: if the grid has the RightToLeft property set to RightToLeft.Yes, this will + /// return what appears as the right edge of the column + /// + internal int GetColBeg(int col) + { + Debug.Assert(myGridTable is not null, "GetColBeg can't be called when myGridTable is null."); + + int offset = layout.Data.X - negOffset; + GridColumnStylesCollection columns = myGridTable.GridColumnStyles; + + int lastCol = Math.Min(col, columns.Count); + for (int i = firstVisibleCol; i < lastCol; ++i) + { + // if (columns[i].Visible && columns[i].PropertyDescriptor is not null) + if (columns[i].PropertyDescriptor is not null) + { + offset += columns[i].Width; + } + } + + return MirrorPoint(offset, layout.Data, isRightToLeft()); + } + + /// + /// Returns the coordinate of the right edge of the given column + /// Bi-Di: if the grid has the RightToLeft property set to RightToLeft.Yes, this will + /// return what appears as the left edge of the column + /// + internal int GetColEnd(int col) + { + // return MirrorPoint(GetColBeg(col) + myGridTable.GridColumnStyles[col].Width, layout.Data, isRightToLeft()); + int colBeg = GetColBeg(col); + Debug.Assert(myGridTable.GridColumnStyles[col].PropertyDescriptor is not null, "why would we need the coordinate of a column that is not visible?"); + int width = myGridTable.GridColumnStyles[col].Width; + return isRightToLeft() ? colBeg - width : colBeg + width; + } + + private int GetColumnWidthSum() + { + int sum = 0; + if (myGridTable is not null && myGridTable.GridColumnStyles is not null) + { + GridColumnStylesCollection columns = myGridTable.GridColumnStyles; + + int columnsCount = columns.Count; + for (int i = 0; i < columnsCount; i++) + { + // if (columns[i].Visible && columns[i].PropertyDescriptor is not null) + if (columns[i].PropertyDescriptor is not null) + { + sum += columns[i].Width; + } + } + } + + return sum; + } + + /// + /// Not all rows in the DataGrid are expandable, + /// this computes which ones are and returns an array + /// of references to them. + /// + private DataGridRelationshipRow[] GetExpandableRows() + { + int nExpandableRows = DataGridRowsLength; + DataGridRow[] localGridRows = DataGridRows; + if (policy.AllowAdd) + { + nExpandableRows = Math.Max(nExpandableRows - 1, 0); + } + + DataGridRelationshipRow[] expandableRows = new DataGridRelationshipRow[nExpandableRows]; + for (int i = 0; i < nExpandableRows; i++) + { + expandableRows[i] = (DataGridRelationshipRow)localGridRows[i]; + } + + return expandableRows; + } + + /// + /// Returns the row number underneath the given y coordinate. + /// + private int GetRowFromY(int y) + { + Rectangle inside = layout.Data; + Debug.Assert(y >= inside.Y && y < inside.Bottom, "y must be inside the vertical bounds of the data"); + + int cy = inside.Y; + int row = firstVisibleRow; + int rowCount = DataGridRowsLength; + DataGridRow[] localGridRows = DataGridRows; + int bottom = inside.Bottom; + while (cy < bottom && row < rowCount) + { + cy += localGridRows[row].Height; + if (cy > y) + { + return row; + } + + ++row; + } + + return -1; + } + + internal Rectangle GetRowHeaderRect() + { + return layout.RowHeaders; + } + + internal Rectangle GetColumnHeadersRect() + { + return layout.ColumnHeaders; + } + + /// + /// Determines where on the control's ClientRectangle a given row is + /// painting to. + /// + private Rectangle GetRowRect(int rowNumber) + { + Rectangle inside = layout.Data; + int cy = inside.Y; + DataGridRow[] localGridRows = DataGridRows; + for (int row = firstVisibleRow; row <= rowNumber; ++row) + { + if (cy > inside.Bottom) + { + break; + } + + if (row == rowNumber) + { + Rectangle rowRect = new Rectangle(inside.X, + cy, + inside.Width, + localGridRows[row].Height); + if (layout.RowHeadersVisible) + { + rowRect.Width += layout.RowHeaders.Width; + rowRect.X -= isRightToLeft() ? 0 : layout.RowHeaders.Width; + } + + return rowRect; + } + + cy += localGridRows[row].Height; + } + + return Rectangle.Empty; + } + + /// + /// Returns the coordinate of the top edge of the given row + /// + private int GetRowTop(int row) + { + DataGridRow[] localGridRows = DataGridRows; + int offset = layout.Data.Y; + int lastRow = Math.Min(row, DataGridRowsLength); + for (int i = firstVisibleRow; i < lastRow; ++i) + { + offset += localGridRows[i].Height; + } + + for (int i = firstVisibleRow; i > lastRow; i--) + { + offset -= localGridRows[i].Height; + } + + return offset; + } + + /// + /// Returns the coordinate of the bottom edge of the given row + /// + private int GetRowBottom(int row) + { + DataGridRow[] localGridRows = DataGridRows; + + return GetRowTop(row) + localGridRows[row].Height; + } + + /// + /// This method is called on methods that need the grid + /// to be bound to a DataTable to work. + /// + private void EnsureBound() + { + if (!Bound) + { + throw new InvalidOperationException(DSR.DataGridUnbound); + } + } + + private void EnsureVisible(int row, int col) + { + if (row < firstVisibleRow + || row >= firstVisibleRow + numTotallyVisibleRows) + { + int dRows = ComputeDeltaRows(row); + ScrollDown(dRows); + } + + if (firstVisibleCol == 0 && numVisibleCols == 0 && lastTotallyVisibleCol == -1) + { + // no columns are displayed whatsoever + // some sanity checks + Debug.Assert(negOffset == 0, " no columns are displayed so the negative offset should be 0"); + return; + } + + int previousFirstVisibleCol = firstVisibleCol; + int previousNegOffset = negOffset; + int previousLastTotallyVisibleCol = lastTotallyVisibleCol; + + while (col < firstVisibleCol +#pragma warning disable SA1408 // Conditional expressions should declare precedence + || col == firstVisibleCol && negOffset != 0 + || lastTotallyVisibleCol == -1 && col > firstVisibleCol + || lastTotallyVisibleCol > -1 && col > lastTotallyVisibleCol) +#pragma warning restore SA1408 // Conditional expressions should declare precedence + { + ScrollToColumn(col); + + if (previousFirstVisibleCol == firstVisibleCol && + previousNegOffset == negOffset && + previousLastTotallyVisibleCol == lastTotallyVisibleCol) + { + // nothing changed since the last iteration + // don't get into an infinite loop + break; + } + + previousFirstVisibleCol = firstVisibleCol; + previousNegOffset = negOffset; + previousLastTotallyVisibleCol = lastTotallyVisibleCol; + + // continue to scroll to the right until the scrollTo column is the totally last visible column or it is the first visible column + } + } + + /// + /// Gets a + /// that specifies the four corners of the selected cell. + /// + public Rectangle GetCurrentCellBounds() + { + DataGridCell current = CurrentCell; + return GetCellBounds(current.RowNumber, current.ColumnNumber); + } + + /// + /// Gets the of the cell specified by row and column number. + /// + public Rectangle GetCellBounds(int row, int col) + { + DataGridRow[] localGridRows = DataGridRows; + Rectangle cellBounds = localGridRows[row].GetCellBounds(col); + cellBounds.Y += GetRowTop(row); + cellBounds.X += layout.Data.X - negOffset; + cellBounds.X = MirrorRectangle(cellBounds, layout.Data, isRightToLeft()); + return cellBounds; + } + + /// + /// Gets the of the cell specified by . + /// + public Rectangle GetCellBounds(DataGridCell dgc) + { + return GetCellBounds(dgc.RowNumber, dgc.ColumnNumber); + } + + // + + internal Rectangle GetRowBounds(DataGridRow row) + { + Rectangle rowBounds = new Rectangle + { + Y = GetRowTop(row.RowNumber), + X = layout.Data.X, + Height = row.Height, + Width = layout.Data.Width + }; + return rowBounds; + } + + /// + /// Gets information, such as row and column number of a + /// clicked point on + /// the grid, + /// using the x + /// and y coordinate passed to the method. + /// + public HitTestInfo HitTest(int x, int y) + { + int topOfData = layout.Data.Y; + HitTestInfo ci = new HitTestInfo(); + + if (layout.CaptionVisible && layout.Caption.Contains(x, y)) + { + ci.type = HitTestType.Caption; + return ci; + } + + if (layout.ParentRowsVisible && layout.ParentRows.Contains(x, y)) + { + ci.type = HitTestType.ParentRows; + return ci; + } + + if (!layout.Inside.Contains(x, y)) + { + return ci; + } + + if (layout.TopLeftHeader.Contains(x, y)) + { + return ci; + } + + // check for column resize + if (layout.ColumnHeaders.Contains(x, y)) + { + ci.type = HitTestType.ColumnHeader; + ci.col = GetColFromX(x); + if (ci.col < 0) + { + return HitTestInfo.Nowhere; + } + + int right = GetColBeg(ci.col + 1); + bool rightToLeft = isRightToLeft(); + if ((rightToLeft && x - right < 8) || (!rightToLeft && right - x < 8)) + { + ci.type = HitTestType.ColumnResize; + } + + return (allowColumnResize ? ci : HitTestInfo.Nowhere); + } + + //check for RowResize: + if (layout.RowHeaders.Contains(x, y)) + { + ci.type = HitTestType.RowHeader; + ci.row = GetRowFromY(y); + if (ci.row < 0) + { + return HitTestInfo.Nowhere; + } + + // find out if the click was a RowResize click + DataGridRow[] localGridRows = DataGridRows; + int bottomBorder = GetRowTop(ci.row) + localGridRows[ci.row].Height; + if (bottomBorder - y - BorderWidth < 2 && !(localGridRows[ci.row] is DataGridAddNewRow)) + { + ci.type = HitTestType.RowResize; + } + + return (allowRowResize ? ci : HitTestInfo.Nowhere); + } + + if (layout.Data.Contains(x, y)) + { + ci.type = HitTestType.Cell; + ci.col = GetColFromX(x); + ci.row = GetRowFromY(y); + if (ci.col < 0 || ci.row < 0) + { + return HitTestInfo.Nowhere; + } + + return ci; + } + + return ci; + } + + /// + /// Gets information, such as row and column number of a + /// clicked point on the grid, about the + /// grid using a specific + /// . + /// + public HitTestInfo HitTest(Point position) + { + return HitTest(position.X, position.Y); + } + + /// + /// Initializes the values for column widths in the table. + /// + private void InitializeColumnWidths() + { + if (myGridTable is null) + { + return; + } + + GridColumnStylesCollection columns = myGridTable.GridColumnStyles; + int numCols = columns.Count; + + // Resize the columns to a approximation of a best fit. + // We find the best fit width of NumRowsForAutoResize rows + // and use it for each column. + int preferredColumnWidth = myGridTable.IsDefault ? PreferredColumnWidth : myGridTable.PreferredColumnWidth; + // if we set the PreferredColumnWidth to something else than AutoColumnSize + // then use that value + // + for (int col = 0; col < numCols; col++) + { + // if the column width is not -1, then this column was initialized already + if (columns[col]._width != -1) + { + continue; + } + + columns[col]._width = preferredColumnWidth; + } + } + + /// + /// Invalidates the scrollable area of the DataGrid. + /// + internal void InvalidateInside() + { + Invalidate(layout.Inside); + } + + /// + /// Invalidates the caption area of the DataGrid. + /// + internal void InvalidateCaption() + { + if (layout.CaptionVisible) + { + Invalidate(layout.Caption); + } + } + + /// + /// Invalidates a rectangle normalized to the caption's + /// visual bounds. + /// + internal void InvalidateCaptionRect(Rectangle r) + { + if (layout.CaptionVisible) + { + Invalidate(r); + } + } + + /// + /// Invalidates the display region of a given DataGridColumn. + /// + internal void InvalidateColumn(int column) + { + GridColumnStylesCollection gridColumns = myGridTable.GridColumnStyles; + if (column < 0 || gridColumns is null || gridColumns.Count <= column) + { + return; + } + + Debug.Assert(gridColumns[column].PropertyDescriptor is not null, "how can we invalidate a column that is invisible?"); + // bail if the column is not visible. + if (column < firstVisibleCol || column > firstVisibleCol + numVisibleCols - 1) + { + return; + } + + Rectangle columnArea = new Rectangle + { + Height = layout.Data.Height, + Width = gridColumns[column].Width, + Y = layout.Data.Y + }; + + int x = layout.Data.X - negOffset; + int gridColumnsCount = gridColumns.Count; + for (int i = firstVisibleCol; i < gridColumnsCount; ++i) + { + if (i == column) + { + break; + } + + x += gridColumns[i].Width; + } + + columnArea.X = x; + columnArea.X = MirrorRectangle(columnArea, layout.Data, isRightToLeft()); + Invalidate(columnArea); + } + + /// + /// Invalidates the parent rows area of the DataGrid + /// + internal void InvalidateParentRows() + { + if (layout.ParentRowsVisible) + { + Invalidate(layout.ParentRows); + } + } + + /// + /// Invalidates a rectangle normalized to the parent + /// rows area's visual bounds. + /// + internal void InvalidateParentRowsRect(Rectangle r) + { + Rectangle parentRowsRect = layout.ParentRows; + Invalidate(r); + if (!parentRowsRect.IsEmpty) + { + //Invalidate(new Rectangle(parentRowsRect.X + r.X, parentRowsRect.Y + r.Y, + // r.Width, r.Height)); + } + } + + /// + /// Invalidate the painting region for the row specified. + /// + internal void InvalidateRow(int rowNumber) + { + Rectangle rowRect = GetRowRect(rowNumber); + if (!rowRect.IsEmpty) + { + Debug.WriteLineIf(CompModSwitches.DataGridPainting.TraceVerbose, "DataGridPainting: Invalidating row " + rowNumber.ToString(CultureInfo.InvariantCulture)); + Invalidate(rowRect); + } + } + + private void InvalidateRowHeader(int rowNumber) + { + if (rowNumber >= firstVisibleRow && rowNumber < firstVisibleRow + numVisibleRows) + { + if (!layout.RowHeadersVisible) + { + return; + } + + Rectangle invalid = new Rectangle + { + Y = GetRowTop(rowNumber), + X = layout.RowHeaders.X, + Width = layout.RowHeaders.Width, + Height = DataGridRows[rowNumber].Height + }; + Invalidate(invalid); + } + } + + // NOTE: + // because of Rtl, we assume that the only place that calls InvalidateRowRect is + // the DataGridRelationshipRow + internal void InvalidateRowRect(int rowNumber, Rectangle r) + { + Rectangle rowRect = GetRowRect(rowNumber); + if (!rowRect.IsEmpty) + { + Debug.WriteLineIf(CompModSwitches.DataGridPainting.TraceVerbose, "DataGridPainting: Invalidating a rect in row " + rowNumber.ToString(CultureInfo.InvariantCulture)); + Rectangle inner = new Rectangle(rowRect.X + r.X, rowRect.Y + r.Y, r.Width, r.Height); + if (vertScrollBar.Visible && isRightToLeft()) + { + inner.X -= vertScrollBar.Width; + } + + Invalidate(inner); + } + } + + /// + /// Gets a value that indicates whether a specified row's node is expanded or collapsed. + /// + public bool IsExpanded(int rowNumber) + { + if (rowNumber < 0 || rowNumber > DataGridRowsLength) + { + throw new ArgumentOutOfRangeException(nameof(rowNumber)); + } + + DataGridRow[] localGridRows = DataGridRows; + + // + + DataGridRow row = localGridRows[rowNumber]; + if (row is DataGridRelationshipRow relRow) + { + return relRow.Expanded; + } + else + { + return false; + } + } + + /// + /// Gets a value indicating whether a + /// specified row is selected. + /// + public bool IsSelected(int row) + { + // + DataGridRow[] localGridRows = DataGridRows; + return localGridRows[row].Selected; + } + + internal static bool IsTransparentColor(Color color) + { + return color.A < 255; + } + + /// + /// Determines if Scrollbars should be visible, + /// updates their bounds and the bounds of all + /// other regions in the DataGrid's Layout. + /// + private void LayoutScrollBars() + { + // if we set the dataSource to null, then take away the scrollbars. + if (listManager is null || myGridTable is null) + { + horizScrollBar.Visible = false; + vertScrollBar.Visible = false; + return; + } + + // Scrollbars are a tricky issue. + // We need to see if we can cram our columns and rows + // in without scrollbars and if they don't fit, we make + // scrollbars visible and then fixup our regions for the + // data and headers. + bool needHorizScrollbar = false; + bool needVertScrollbar = false; + bool recountRows = false; + bool alignToRight = isRightToLeft(); + + int nGridCols = myGridTable.GridColumnStyles.Count; + + // if we call LayoutScrollBars before CreateDataGridRows + // then the columns will have their default width ( 100 ) + // CreateDataGridRows will possibly change the columns' width + // + // and anyway, ComputeVisibleRows will call the DataGridRows accessor + // + DataGridRow[] gridRows = DataGridRows; + + // at this stage, the data grid columns may have their width set to -1 ( ie, their width is uninitialized ) + // make sure that the totalWidth is at least 0 + int totalWidth = Math.Max(0, GetColumnWidthSum()); + + if (totalWidth > layout.Data.Width && !needHorizScrollbar) + { + int horizHeight = horizScrollBar.Height; + layout.Data.Height -= horizHeight; + if (layout.RowHeadersVisible) + { + layout.RowHeaders.Height -= horizHeight; + } + + needHorizScrollbar = true; + } + + int oldFirstVisibleRow = firstVisibleRow; + + ComputeVisibleRows(); + if (numTotallyVisibleRows != DataGridRowsLength && !needVertScrollbar) + { + int vertWidth = vertScrollBar.Width; + layout.Data.Width -= vertWidth; + if (layout.ColumnHeadersVisible) + { + if (alignToRight) + { + layout.ColumnHeaders.X += vertWidth; + } + + layout.ColumnHeaders.Width -= vertWidth; + } + + needVertScrollbar = true; + } + + firstVisibleCol = ComputeFirstVisibleColumn(); + // we compute the number of visible columns only after we set up the vertical scroll bar. + ComputeVisibleColumns(); + + if (needVertScrollbar && totalWidth > layout.Data.Width && !needHorizScrollbar) + { + firstVisibleRow = oldFirstVisibleRow; + int horizHeight = horizScrollBar.Height; + layout.Data.Height -= horizHeight; + if (layout.RowHeadersVisible) + { + layout.RowHeaders.Height -= horizHeight; + } + + needHorizScrollbar = true; + recountRows = true; + } + + if (recountRows) + { + ComputeVisibleRows(); + if (numTotallyVisibleRows != DataGridRowsLength && !needVertScrollbar) + { + int vertWidth = vertScrollBar.Width; + layout.Data.Width -= vertWidth; + if (layout.ColumnHeadersVisible) + { + if (alignToRight) + { + layout.ColumnHeaders.X += vertWidth; + } + + layout.ColumnHeaders.Width -= vertWidth; + } + + needVertScrollbar = true; + } + } + + layout.ResizeBoxRect = new Rectangle(); + if (needVertScrollbar && needHorizScrollbar) + { + Rectangle data = layout.Data; + layout.ResizeBoxRect = new Rectangle(alignToRight ? data.X : data.Right, + data.Bottom, + vertScrollBar.Width, + horizScrollBar.Height); + } + + if (needHorizScrollbar && nGridCols > 0) + { + //Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "DataGridScrolling: foo"); + + int widthNotVisible = totalWidth - layout.Data.Width; + + horizScrollBar.Minimum = 0; + horizScrollBar.Maximum = totalWidth; + horizScrollBar.SmallChange = 1; + horizScrollBar.LargeChange = Math.Max(totalWidth - widthNotVisible, 0); + horizScrollBar.Enabled = Enabled; + horizScrollBar.RightToLeft = RightToLeft; + horizScrollBar.Bounds = new Rectangle(alignToRight ? layout.Inside.X + layout.ResizeBoxRect.Width : layout.Inside.X, + layout.Data.Bottom, + layout.Inside.Width - layout.ResizeBoxRect.Width, + horizScrollBar.Height); + horizScrollBar.Visible = true; + } + else + { + HorizontalOffset = 0; + horizScrollBar.Visible = false; + } + + if (needVertScrollbar) + { + int vertScrollBarTop = layout.Data.Y; + if (layout.ColumnHeadersVisible) + { + vertScrollBarTop = layout.ColumnHeaders.Y; + } + + // if numTotallyVisibleRows == 0 ( the height of the row is bigger than the height of + // the grid ) then scroll in increments of 1. + vertScrollBar.LargeChange = numTotallyVisibleRows != 0 ? numTotallyVisibleRows : 1; + vertScrollBar.Bounds = new Rectangle(alignToRight ? layout.Data.X : layout.Data.Right, + vertScrollBarTop, + vertScrollBar.Width, + layout.Data.Height + layout.ColumnHeaders.Height); + vertScrollBar.Enabled = Enabled; + vertScrollBar.Visible = true; + if (alignToRight) + { + layout.Data.X += vertScrollBar.Width; + } + } + else + { + vertScrollBar.Visible = false; + } + } + + /// + /// Navigates back to the table previously displayed in the grid. + /// + public void NavigateBack() + { + if (!CommitEdit() || parentRows.IsEmpty()) + { + return; + } + + // when navigating back, if the grid is inAddNewRow state, cancel the currentEdit. + // we do not need to recreate the rows cause we are navigating back. + // the grid will catch any exception that happens. + if (gridState[GRIDSTATE_inAddNewRow]) + { + gridState[GRIDSTATE_inAddNewRow] = false; + try + { + listManager.CancelCurrentEdit(); + } + catch + { + } + } + else + { + UpdateListManager(); + } + + DataGridState newState = parentRows.PopTop(); + + ResetMouseState(); + + newState.PullState(this, false); // we do not want to create columns when navigating back + + // we need to have originalState is not null when we process + // Set_ListManager in the NavigateBack/NavigateTo methods. + // otherwise the DataSource_MetaDataChanged event will not get registered + // properly + if (parentRows.GetTopParent() is null) + { + originalState = null; + } + + DataGridRow[] localGridRows = DataGridRows; + // what if the user changed the ReadOnly property + // on the grid while the user was navigating to the child rows? + // + // what if the policy does not allow for allowAdd? + // + if ((ReadOnly || !policy.AllowAdd) == (localGridRows[DataGridRowsLength - 1] is DataGridAddNewRow)) + { + int newDataGridRowsLength = (ReadOnly || !policy.AllowAdd) ? DataGridRowsLength - 1 : DataGridRowsLength + 1; + DataGridRow[] newDataGridRows = new DataGridRow[newDataGridRowsLength]; + for (int i = 0; i < Math.Min(newDataGridRowsLength, DataGridRowsLength); i++) + { + newDataGridRows[i] = DataGridRows[i]; + } + + if (!ReadOnly && policy.AllowAdd) + { + newDataGridRows[newDataGridRowsLength - 1] = new DataGridAddNewRow(this, myGridTable, newDataGridRowsLength - 1); + } + + SetDataGridRows(newDataGridRows, newDataGridRowsLength); + } + + // when we navigate back from a child table, + // it may be the case that in between the user added a tableStyle that is different + // from the one that is currently in the grid + // in that case, we need to reset the dataGridTableStyle in the rows + localGridRows = DataGridRows; + if (localGridRows is not null && localGridRows.Length != 0) + { + DataGridTableStyle dgTable = localGridRows[0].DataGridTableStyle; + if (dgTable != myGridTable) + { + for (int i = 0; i < localGridRows.Length; i++) + { + localGridRows[i].DataGridTableStyle = myGridTable; + } + } + } + + // if we have the default table, when we navigate back + // we also have the default gridColumns, w/ width = -1 + // we need to set the width on the new gridColumns + // + if (myGridTable.GridColumnStyles.Count > 0 && myGridTable.GridColumnStyles[0].Width == -1) + { +#if DEBUG + GridColumnStylesCollection cols = myGridTable.GridColumnStyles; + for (int i = 0; i < cols.Count; i++) + { + Debug.Assert(cols[i].Width == -1, "Sanity check"); + } + + Debug.Assert(myGridTable.IsDefault, "when we navigate to the parent rows and the columns have widths -1 we are using the default table"); +#endif // DEBUG + InitializeColumnWidths(); + } + + // reset the currentRow to the old position in the listmanager: + currentRow = ListManager.Position == -1 ? 0 : ListManager.Position; + + // if the AllowNavigation changed while the user was navigating the + // child tables, so that the new navigation mode does not allow childNavigation anymore + // then reset the rows + if (!AllowNavigation) + { + RecreateDataGridRows(); + } + + caption.BackButtonActive = (parentRows.GetTopParent() is not null) && AllowNavigation; + caption.BackButtonVisible = caption.BackButtonActive; + caption.DownButtonActive = (parentRows.GetTopParent() is not null); + + PerformLayout(); + Invalidate(); + // reposition the scroll bar + if (vertScrollBar.Visible) + { + vertScrollBar.Value = firstVisibleRow; + } + + if (horizScrollBar.Visible) + { + horizScrollBar.Value = HorizontalOffset + negOffset; + } + + Edit(); + OnNavigate(new NavigateEventArgs(false)); + } + + /// + /// Navigates to the table specified by row and relation + /// name. + /// + public void NavigateTo(int rowNumber, string relationName) + { + // do not navigate if AllowNavigation is set to false + if (!AllowNavigation) + { + return; + } + + DataGridRow[] localGridRows = DataGridRows; + if (rowNumber < 0 || rowNumber > DataGridRowsLength - (policy.AllowAdd ? 2 : 1)) + { + throw new ArgumentOutOfRangeException(nameof(rowNumber)); + } + + EnsureBound(); + + DataGridRow source = localGridRows[rowNumber]; + + NavigateTo(relationName, source, false); + } + + internal void NavigateTo(string relationName, DataGridRow source, bool fromRow) + { + // do not navigate if AllowNavigation is set to false + if (!AllowNavigation) + { + return; + } + + // Commit the edit if possible + if (!CommitEdit()) + { + return; + } + + DataGridState childState; + try + { + childState = CreateChildState(relationName, source); + } + catch + { + // if we get an error when creating the RelatedCurrencyManager + // then navigateBack and ignore the exception. + // + NavigateBack(); + return; + } + + // call EndCurrentEdit before navigating. + // if we get an exception, we do not navigate. + // + try + { + listManager.EndCurrentEdit(); + } + catch + { + return; + } + + // Preserve our current state + // we need to do this after the EndCurrentEdit, otherwise the + // DataGridState will get the listChanged event from the EndCurrentEdit + DataGridState dgs = new DataGridState(this) + { + LinkingRow = source + }; + + // we need to update the Position in the ListManager + // ( the RelatedListManager uses only the position in the parentManager + // to create the childRows + // + // before the code was calling CurrentCell = this and such + // we should only call EndCurrentEdit ( which the code was doing anyway ) + // and then set the position in the listManager to the new row. + // + if (source.RowNumber != CurrentRow) + { + listManager.Position = source.RowNumber; + } + + // We save our state if the parent rows stack is empty. + if (parentRows.GetTopParent() is null) + { + originalState = dgs; + } + + parentRows.AddParent(dgs); + + NavigateTo(childState); + + OnNavigate(new NavigateEventArgs(true)); + if (fromRow) + { + // OnLinkClick(EventArgs.Empty); + } + } + + private void NavigateTo(DataGridState childState) + { + // we are navigating... better stop editing. + EndEdit(); + + // also, we are no longer in editOrNavigate mode either + gridState[GRIDSTATE_isNavigating] = false; + + // reset hot tracking + ResetMouseState(); + + // Retrieve the child state + childState.PullState(this, true); // true for creating columns when we navigate to child rows + + if (listManager.Position != currentRow) + { + currentRow = listManager.Position == -1 ? 0 : listManager.Position; + } + + if (parentRows.GetTopParent() is not null) + { + caption.BackButtonActive = AllowNavigation; + caption.BackButtonVisible = caption.BackButtonActive; + caption.DownButtonActive = true; + } + + HorizontalOffset = 0; + PerformLayout(); + Invalidate(); + } + + /// + /// Given a coordinate in the control this method returns + /// the equivalent point for a row. + /// + private Point NormalizeToRow(int x, int y, int row) + { + Debug.Assert(row >= firstVisibleRow && row < firstVisibleRow + numVisibleRows, + "Row " + row.ToString(CultureInfo.InvariantCulture) + "is not visible! firstVisibleRow = " + + firstVisibleRow.ToString(CultureInfo.InvariantCulture) + ", numVisibleRows = " + + numVisibleRows.ToString(CultureInfo.InvariantCulture)); + Point origin = new Point(0, layout.Data.Y); + + DataGridRow[] localGridRows = DataGridRows; + for (int r = firstVisibleRow; r < row; ++r) + { + origin.Y += localGridRows[r].Height; + } + + // when hittesting for the PlusMinus, the code in the DataGridRelationshipRow + // will use real X coordinate ( the one from layout.RowHeaders ) to paint the glyph + // + return new Point(x, y - origin.Y); + } + + internal void OnColumnCollectionChanged(object sender, CollectionChangeEventArgs e) + { + DataGridTableStyle table = (DataGridTableStyle)sender; + if (table.Equals(myGridTable)) + { + // if we changed the column collection, then we need to set the property + // descriptors in the column collection. + // unless the user set the propertyDescriptor in the columnCollection + // + if (!myGridTable.IsDefault) + { + // if the element in the collectionChangeEventArgs is not null + // and the action is refresh, then it means that the user + // set the propDesc. we do not want to override this. + if (e.Action != CollectionChangeAction.Refresh || e.Element is null) + { + PairTableStylesAndGridColumns(listManager, myGridTable, false); + } + } + + Invalidate(); + PerformLayout(); + } + } + + /// + /// Paints column headers. + /// + private void PaintColumnHeaders(Graphics g) + { + bool alignToLeft = isRightToLeft(); + Rectangle boundingRect = layout.ColumnHeaders; + if (!alignToLeft) + { + boundingRect.X -= negOffset; + } + + boundingRect.Width += negOffset; + + int columnHeaderWidth = PaintColumnHeaderText(g, boundingRect); + + if (alignToLeft) + { + boundingRect.X = boundingRect.Right - columnHeaderWidth; + } + + boundingRect.Width = columnHeaderWidth; + if (!FlatMode) + { + ControlPaint.DrawBorder3D(g, boundingRect, Border3DStyle.RaisedInner); + boundingRect.Inflate(-1, -1); + // g.SetPen(OldSystemPens.Control); + // g.OldBrush = (OldSystemBrushes.Hollow); + boundingRect.Width--; + boundingRect.Height--; + g.DrawRectangle(SystemPens.Control, boundingRect); + } + } + + private int PaintColumnHeaderText(Graphics g, Rectangle boundingRect) + { + int cx = 0; + Rectangle textBounds = boundingRect; + GridColumnStylesCollection gridColumns = myGridTable.GridColumnStyles; + bool alignRight = isRightToLeft(); + + int nGridCols = gridColumns.Count; + // for sorting + PropertyDescriptor sortProperty = null; + sortProperty = ListManager.GetSortProperty(); + + // Now paint the column header text! + for (int col = firstVisibleCol; col < nGridCols; ++col) + { + if (gridColumns[col].PropertyDescriptor is null) + { + continue; + } + + if (cx > boundingRect.Width) + { + break; + } + + bool columnSorted = sortProperty is not null && sortProperty.Equals(gridColumns[col].PropertyDescriptor); + TriangleDirection whichWay = TriangleDirection.Up; + if (columnSorted) + { + ListSortDirection direction = ListManager.GetSortDirection(); + if (direction == ListSortDirection.Descending) + { + whichWay = TriangleDirection.Down; + } + } + + if (alignRight) + { + textBounds.Width = gridColumns[col].Width - + (columnSorted ? textBounds.Height : 0); + textBounds.X = boundingRect.Right - cx - textBounds.Width; + } + else + { + textBounds.X = boundingRect.X + cx; + textBounds.Width = gridColumns[col].Width - + (columnSorted ? textBounds.Height : 0); + } + + // at the moment we paint some pixels twice. + // we should not call FilLRectangle, once the real GDI+ is there, we will have no need to do that + + // if the user set the HeaderBackBrush property on the + // dataGrid, then use that property + Brush headerBrush; + if (myGridTable.IsDefault) + { + headerBrush = HeaderBackBrush; + } + else + { + headerBrush = myGridTable.HeaderBackBrush; + } + + g.FillRectangle(headerBrush, textBounds); + // granted, the code would be a lot cleaner if we were using a "new Rectangle" + // but like this will be faster + if (alignRight) + { + textBounds.X -= 2; + textBounds.Y += 2; + } + else + { + textBounds.X += 2; + textBounds.Y += 2; + } + + StringFormat format = new StringFormat(); + + // the columnHeaderText alignment should be the same as + // the alignment in the column + // + HorizontalAlignment colAlignment = gridColumns[col].Alignment; + format.Alignment = colAlignment == HorizontalAlignment.Right ? StringAlignment.Far : + colAlignment == HorizontalAlignment.Center ? StringAlignment.Center : + StringAlignment.Near; + + // part 1, section 1: the column headers should not wrap + format.FormatFlags |= StringFormatFlags.NoWrap; + + if (alignRight) + { + format.FormatFlags |= StringFormatFlags.DirectionRightToLeft; + format.Alignment = StringAlignment.Near; + } + + g.DrawString(gridColumns[col].HeaderText, + myGridTable.IsDefault ? HeaderFont : myGridTable.HeaderFont, + myGridTable.IsDefault ? HeaderForeBrush : myGridTable.HeaderForeBrush, + textBounds, + format); + format.Dispose(); + + if (alignRight) + { + textBounds.X += 2; + textBounds.Y -= 2; + } + else + { + textBounds.X -= 2; + textBounds.Y -= 2; + } + + if (columnSorted) + { + // + + Rectangle triBounds = new Rectangle(alignRight ? textBounds.X - textBounds.Height : textBounds.Right, + textBounds.Y, + textBounds.Height, + textBounds.Height); + + g.FillRectangle(headerBrush, triBounds); + int deflateValue = Math.Max(0, (textBounds.Height - 5) / 2); + triBounds.Inflate(-deflateValue, -deflateValue); + + Pen pen1 = new Pen(BackgroundBrush); + Pen pen2 = new Pen(myGridTable.BackBrush); + Triangle.Paint(g, triBounds, whichWay, headerBrush, pen1, pen2, pen1, true); + pen1.Dispose(); + pen2.Dispose(); + } + + int paintedWidth = textBounds.Width + (columnSorted ? textBounds.Height : 0); + + if (!FlatMode) + { + if (alignRight && columnSorted) + { + textBounds.X -= textBounds.Height; + } + + textBounds.Width = paintedWidth; + + ControlPaint.DrawBorder3D(g, textBounds, Border3DStyle.RaisedInner); + } + + cx += paintedWidth; + } + + // paint the possible exposed portion to the right ( or left, as the case may be) + if (cx < boundingRect.Width) + { + textBounds = boundingRect; + + if (!alignRight) + { + textBounds.X += cx; + } + + textBounds.Width -= cx; + g.FillRectangle(backgroundBrush, textBounds); + } + + return cx; + } + + /// + /// Paints a border around the bouding rectangle given + /// + private void PaintBorder(Graphics g, Rectangle bounds) + { + if (BorderStyle == BorderStyle.None) + { + return; + } + + if (BorderStyle == BorderStyle.Fixed3D) + { + Border3DStyle style = Border3DStyle.Sunken; + ControlPaint.DrawBorder3D(g, bounds, style); + } + else if (BorderStyle == BorderStyle.FixedSingle) + { + Brush br; + + if (myGridTable.IsDefault) + { + br = HeaderForeBrush; + } + else + { + br = myGridTable.HeaderForeBrush; + } + + g.FillRectangle(br, bounds.X, bounds.Y, bounds.Width + 2, 2); + g.FillRectangle(br, bounds.Right - 2, bounds.Y, 2, bounds.Height + 2); + g.FillRectangle(br, bounds.X, bounds.Bottom - 2, bounds.Width + 2, 2); + g.FillRectangle(br, bounds.X, bounds.Y, 2, bounds.Height + 2); + } + else + { + Pen pen = SystemPens.WindowFrame; + bounds.Width--; + bounds.Height--; + g.DrawRectangle(pen, bounds); + } + } + + /// + /// Paints the grid in the bounding rectangle given. + /// This includes the column headers and each visible row. + /// + private void PaintGrid(Graphics g, Rectangle gridBounds) + { + Debug.WriteLineIf(CompModSwitches.DataGridPainting.TraceVerbose, "DataGridPainting: PaintGrid on " + gridBounds.ToString()); + + Rectangle rc = gridBounds; + + if (listManager is not null) + { + if (layout.ColumnHeadersVisible) + { + Region r = g.Clip; + g.SetClip(layout.ColumnHeaders); + PaintColumnHeaders(g); + g.Clip = r; + r.Dispose(); + int columnHeaderHeight = layout.ColumnHeaders.Height; + rc.Y += columnHeaderHeight; + rc.Height -= columnHeaderHeight; + } + + if (layout.TopLeftHeader.Width > 0) + { + if (myGridTable.IsDefault) + { + g.FillRectangle(HeaderBackBrush, layout.TopLeftHeader); + } + else + { + g.FillRectangle(myGridTable.HeaderBackBrush, layout.TopLeftHeader); + } + + if (!FlatMode) + { + ControlPaint.DrawBorder3D(g, layout.TopLeftHeader, Border3DStyle.RaisedInner); + } + } + + PaintRows(g, ref rc); + } + + // paint the possible exposed portion below + if (rc.Height > 0) + { + g.FillRectangle(backgroundBrush, rc); + } + } + + private void DeleteDataGridRows(int deletedRows) + { + if (deletedRows == 0) + { + return; + } + + int currentRowCount = DataGridRowsLength; + int newDataGridRowsLength = currentRowCount - deletedRows + (gridState[GRIDSTATE_inAddNewRow] ? 1 : 0); + DataGridRow[] newDataGridRows = new DataGridRow[newDataGridRowsLength]; + DataGridRow[] gridRows = DataGridRows; + + // the number of selected entries so far in the array + int selectedEntries = 0; + + for (int i = 0; i < currentRowCount; i++) + { + if (gridRows[i].Selected) + { + selectedEntries++; + } + else + { + newDataGridRows[i - selectedEntries] = gridRows[i]; + newDataGridRows[i - selectedEntries].number = i - selectedEntries; + } + } + + if (gridState[GRIDSTATE_inAddNewRow]) + { + newDataGridRows[currentRowCount - selectedEntries] = new DataGridAddNewRow(this, myGridTable, currentRowCount - selectedEntries); + gridState[GRIDSTATE_inAddNewRow] = false; + } + + Debug.Assert(selectedEntries == deletedRows, "all the rows that would have been deleted should have been selected: selectedGridEntries " + selectedEntries.ToString(CultureInfo.InvariantCulture) + " deletedRows " + deletedRows.ToString(CultureInfo.InvariantCulture)); + + SetDataGridRows(newDataGridRows, newDataGridRowsLength); + } + + /// + /// Paints the visible rows on the grid. + /// + private void PaintRows(Graphics g, ref Rectangle boundingRect) + { + int cy = 0; + bool alignRight = isRightToLeft(); + Rectangle rowBounds = boundingRect; + Rectangle dataBounds = Rectangle.Empty; + bool paintRowHeaders = layout.RowHeadersVisible; + Rectangle headerBounds = Rectangle.Empty; + + int numRows = DataGridRowsLength; + DataGridRow[] localGridRows = DataGridRows; + int numCols = myGridTable.GridColumnStyles.Count - firstVisibleCol; + + for (int row = firstVisibleRow; row < numRows; row++) + { + if (cy > boundingRect.Height) + { + break; + } + + rowBounds = boundingRect; + rowBounds.Height = localGridRows[row].Height; + rowBounds.Y = boundingRect.Y + cy; + + // will add some errors +#if false + if (forDebug == 0 || forDebug == 1) + { + object dRowView = listManager[row]; + DataRow dRow= ((DataRowView) dRowView).Row; + // dRow.RowError = "Error " + forDebug.ToString(); + dRow.SetColumnError(forDebug, "another error " + forDebug.ToString()); + + /* + if (localGridRows[row].DataRow is not null) + { + localGridRows[row].DataRow.RowError = "error " + forDebug.ToString(); + localGridRows[row].DataRow.SetColumnError(forDebug, "another error " + forDebug.ToString()); + } + */ + forDebug ++; + } +#endif // false + if (paintRowHeaders) + { + headerBounds = rowBounds; + headerBounds.Width = layout.RowHeaders.Width; + + if (alignRight) + { + headerBounds.X = rowBounds.Right - headerBounds.Width; + } + + if (g.IsVisible(headerBounds)) + { + localGridRows[row].PaintHeader(g, headerBounds, alignRight, gridState[GRIDSTATE_isEditing]); + g.ExcludeClip(headerBounds); + } + + if (!alignRight) + { + rowBounds.X += headerBounds.Width; + } + + rowBounds.Width -= headerBounds.Width; + } + + if (g.IsVisible(rowBounds)) + { + dataBounds = rowBounds; + if (!alignRight) + { + dataBounds.X -= negOffset; + } + + dataBounds.Width += negOffset; + + localGridRows[row].Paint(g, dataBounds, rowBounds, firstVisibleCol, numCols, alignRight); + } + + cy += rowBounds.Height; + } + + boundingRect.Y += cy; + boundingRect.Height -= cy; + } + + /// + /// Gets or sets a value that indicates whether a key should be processed + /// further. + /// + protected override bool ProcessDialogKey(Keys keyData) + { + Debug.WriteLineIf(CompModSwitches.DataGridKeys.TraceVerbose, "DataGridKeys: ProcessDialogKey " + TypeDescriptor.GetConverter(typeof(Keys)).ConvertToString(keyData)); + DataGridRow[] localGridRows = DataGridRows; + if (listManager is not null && DataGridRowsLength > 0 && localGridRows[currentRow].OnKeyPress(keyData)) + { + Debug.WriteLineIf(CompModSwitches.DataGridKeys.TraceVerbose, "DataGridKeys: Current Row ate the keystroke"); + return true; + } + + switch (keyData & Keys.KeyCode) + { + case Keys.Tab: + case Keys.Up: + case Keys.Down: + case Keys.Left: + case Keys.Right: + case Keys.Next: + case Keys.Prior: + case Keys.Enter: + case Keys.Escape: + case Keys.Oemplus: + case Keys.Add: + case Keys.OemMinus: + case Keys.Subtract: + case Keys.Space: + case Keys.Delete: + case Keys.A: + KeyEventArgs ke = new KeyEventArgs(keyData); + if (ProcessGridKey(ke)) + { + return true; + } + + break; + + case Keys.C: + if ((keyData & Keys.Control) != 0 && (keyData & Keys.Alt) == 0) + { + // the user pressed Ctrl-C + if (!Bound) + { + break; + } + + // need to distinguish between selecting a set of rows, and + // selecting just one column. + if (numSelectedRows == 0) + { + // copy the data from one column only + if (currentRow < ListManager.Count) + { + GridColumnStylesCollection columns = myGridTable.GridColumnStyles; + if (currentCol >= 0 && currentCol < columns.Count) + { + DataGridColumnStyle column = columns[currentCol]; + string text = column.GetDisplayText(column.GetColumnValueAtRow(ListManager, currentRow)); + + // copy the data to the clipboard + Clipboard.SetDataObject(text); + return true; + } + } + } + else + { + // the user selected a set of rows to copy the data from + + int numRowsOutputted = 0; // the number of rows written to "text" + string text = string.Empty; + + for (int i = 0; i < DataGridRowsLength; ++i) + { + if (localGridRows[i].Selected) + { + GridColumnStylesCollection columns = myGridTable.GridColumnStyles; + int numCols = columns.Count; + for (int j = 0; j < numCols; j++) + { + DataGridColumnStyle column = columns[j]; + text += column.GetDisplayText(column.GetColumnValueAtRow(ListManager, i)); + + // do not put the delimiter at the end of the last column + if (j < numCols - 1) + { + text += GetOutputTextDelimiter(); + } + } + + // put the hard enter "\r\n" only if this is not the last selected row + if (numRowsOutputted < numSelectedRows - 1) + { + text += "\r\n"; + } + + numRowsOutputted++; + } + } + + // copy the data to the clipboard + Clipboard.SetDataObject(text); + return true; + } + } + + break; + } + + return base.ProcessDialogKey(keyData); + } + + private void DeleteRows(DataGridRow[] localGridRows) + { + int rowsDeleted = 0; + + int currentRowsCount = listManager is null ? 0 : listManager.Count; + + if (Visible) + { + BeginUpdateInternal(); + } + + try + { + if (ListManager is not null) + { + for (int i = 0; i < DataGridRowsLength; i++) + { + if (localGridRows[i].Selected) + { + if (localGridRows[i] is DataGridAddNewRow) + { + Debug.Assert(i == DataGridRowsLength - 1, "the location of addNewRow is " + i.ToString(CultureInfo.InvariantCulture) + " and there are " + DataGridRowsLength.ToString(CultureInfo.InvariantCulture) + " rows "); + localGridRows[i].Selected = false; + } + else + { + ListManager.RemoveAt(i - rowsDeleted); + rowsDeleted++; + } + } + } + } + } + catch + { + // if we got an exception from the back end + // when deleting the rows then we should reset + // our rows and re-throw the exception + // + RecreateDataGridRows(); + gridState[GRIDSTATE_inDeleteRow] = false; + if (Visible) + { + EndUpdateInternal(); + } + + throw; + } + + // keep the copy of the old rows in place + // it may be the case that deleting one row could cause multiple rows to be deleted in the same list + // + if (listManager is not null && currentRowsCount == listManager.Count + rowsDeleted) + { + DeleteDataGridRows(rowsDeleted); + } + else + { + RecreateDataGridRows(); + } + + gridState[GRIDSTATE_inDeleteRow] = false; + if (Visible) + { + EndUpdateInternal(); + } + + if (listManager is not null && currentRowsCount != listManager.Count + rowsDeleted) + { + Invalidate(); + } + } + + // convention: + // if we return -1 it means that the user was going left and there were no visible columns to the left of the current one + // if we return cols.Count + 1 it means that the user was going right and there were no visible columns to the right of the currrent + private int MoveLeftRight(GridColumnStylesCollection cols, int startCol, bool goRight) + { + int i; + if (goRight) + { + for (i = startCol + 1; i < cols.Count; i++) + { + // if (cols[i].Visible && cols[i].PropertyDescriptor is not null) + if (cols[i].PropertyDescriptor is not null) + { + return i; + } + } + + return i; + } + else + { + for (i = startCol - 1; i >= 0; i--) + { + // if (cols[i].Visible && cols[i].PropertyDescriptor is not null) + if (cols[i].PropertyDescriptor is not null) + { + return i; + } + } + + return i; + } + } + + /// + /// Processes keys for grid navigation. + /// + protected bool ProcessGridKey(KeyEventArgs ke) + { + Debug.WriteLineIf(CompModSwitches.DataGridKeys.TraceVerbose, "DataGridKeys: ProcessGridKey " + TypeDescriptor.GetConverter(typeof(Keys)).ConvertToString(ke.KeyCode)); + if (listManager is null || myGridTable is null) + { + return false; + } + + DataGridRow[] localGridRows = DataGridRows; + KeyEventArgs biDiKe = ke; + // check for Bi-Di + // + if (isRightToLeft()) + { + switch (ke.KeyCode) + { + case Keys.Left: + biDiKe = new KeyEventArgs((Keys.Right | ke.Modifiers)); + break; + case Keys.Right: + biDiKe = new KeyEventArgs((Keys.Left | ke.Modifiers)); + break; + default: + break; + } + } + + GridColumnStylesCollection cols = myGridTable.GridColumnStyles; + int firstColumnMarkedVisible = 0; + int lastColumnMarkedVisible = cols.Count; + for (int i = 0; i < cols.Count; i++) + { + if (cols[i].PropertyDescriptor is not null) + { + firstColumnMarkedVisible = i; + break; + } + } + + for (int i = cols.Count - 1; i >= 0; i--) + { + if (cols[i].PropertyDescriptor is not null) + { + lastColumnMarkedVisible = i; + break; + } + } + + switch (biDiKe.KeyCode) + { + case Keys.Tab: + return ProcessTabKey(biDiKe.KeyData); + case Keys.Up: + gridState[GRIDSTATE_childLinkFocused] = false; + if (dataGridRowsLength == 0) + { + return true; + } + + if (biDiKe.Control && !biDiKe.Alt) + { + if (biDiKe.Shift) + { + DataGridRow[] gridRows = DataGridRows; + + int savedCurrentRow = currentRow; + CurrentRow = 0; + + ResetSelection(); + + for (int i = 0; i <= savedCurrentRow; i++) + { + gridRows[i].Selected = true; + } + + numSelectedRows = savedCurrentRow + 1; + // hide the edit box + // + EndEdit(); + return true; + } + + // do not make the parentRowsVisible = false; + // ParentRowsVisible = false; + ResetSelection(); + CurrentRow = 0; + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + return true; + } + else if (biDiKe.Shift) + { + DataGridRow[] gridRows = DataGridRows; + // keep a continous selected region + if (gridRows[currentRow].Selected) + { + if (currentRow >= 1) + { + if (gridRows[currentRow - 1].Selected) + { + if (currentRow >= DataGridRowsLength - 1 || !gridRows[currentRow + 1].Selected) + { + numSelectedRows--; + gridRows[currentRow].Selected = false; + } + } + else + { + numSelectedRows += gridRows[currentRow - 1].Selected ? 0 : 1; + gridRows[currentRow - 1].Selected = true; + } + + CurrentRow--; + } + } + else + { + numSelectedRows++; + gridRows[currentRow].Selected = true; + if (currentRow >= 1) + { + numSelectedRows += gridRows[currentRow - 1].Selected ? 0 : 1; + gridRows[currentRow - 1].Selected = true; + CurrentRow--; + } + } + + // hide the edit box: + // + EndEdit(); + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + return true; + } + else if (biDiKe.Alt) + { + // will need to collapse all child table links + // -1 is for all rows, and false is for collapsing the rows + SetRowExpansionState(-1, false); + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + return true; + } + + ResetSelection(); + CurrentRow -= 1; + Edit(); + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + break; + case Keys.Down: + gridState[GRIDSTATE_childLinkFocused] = false; + if (dataGridRowsLength == 0) + { + return true; + } + + if (biDiKe.Control && !biDiKe.Alt) + { + if (biDiKe.Shift) + { + int savedCurrentRow = currentRow; + CurrentRow = Math.Max(0, DataGridRowsLength - (policy.AllowAdd ? 2 : 1)); + DataGridRow[] gridRows = DataGridRows; + + ResetSelection(); + + for (int i = savedCurrentRow; i <= currentRow; i++) + { + gridRows[i].Selected = true; + } + + numSelectedRows = currentRow - savedCurrentRow + 1; + // hide the edit box + // + EndEdit(); + return true; + } + + // do not make the parentRowsVisible = true; + // ParentRowsVisible = true; + ResetSelection(); + CurrentRow = Math.Max(0, DataGridRowsLength - (policy.AllowAdd ? 2 : 1)); + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + return true; + } + else if (biDiKe.Shift) + { + DataGridRow[] gridRows = DataGridRows; + + // keep a continous selected region + if (gridRows[currentRow].Selected) + { + // -1 because we index from 0 + if (currentRow < DataGridRowsLength - (policy.AllowAdd ? 1 : 0) - 1) + { + if (gridRows[currentRow + 1].Selected) + { + if (currentRow == 0 || !gridRows[currentRow - 1].Selected) + { + numSelectedRows--; + gridRows[currentRow].Selected = false; + } + } + else + { + numSelectedRows += gridRows[currentRow + 1].Selected ? 0 : 1; + gridRows[currentRow + 1].Selected = true; + } + + CurrentRow++; + } + } + else + { + numSelectedRows++; + gridRows[currentRow].Selected = true; + // -1 because we index from 0, and -1 so this is not the last row + // so it adds to -2 + if (currentRow < DataGridRowsLength - (policy.AllowAdd ? 1 : 0) - 1) + { + CurrentRow++; + numSelectedRows += gridRows[currentRow].Selected ? 0 : 1; + gridRows[currentRow].Selected = true; + } + } + + // hide the edit box: + // + EndEdit(); + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + return true; + } + else if (biDiKe.Alt) + { + // will need to expande all child table links + // -1 is for all rows, and true is for expanding the rows + SetRowExpansionState(-1, true); + return true; + } + + ResetSelection(); + Edit(); + CurrentRow += 1; + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + break; + case Keys.OemMinus: + case Keys.Subtract: + gridState[GRIDSTATE_childLinkFocused] = false; + if (biDiKe.Control && !biDiKe.Alt) + { + SetRowExpansionState(-1, false); + return true; + } + + return false; + case Keys.Oemplus: + case Keys.Add: + gridState[GRIDSTATE_childLinkFocused] = false; + if (biDiKe.Control) + { + SetRowExpansionState(-1, true); + // hide the edit box + // + EndEdit(); + return true; + } + + return false; + case Keys.Space: + gridState[GRIDSTATE_childLinkFocused] = false; + if (dataGridRowsLength == 0) + { + return true; + } + + if (biDiKe.Shift) + { + ResetSelection(); + EndEdit(); + DataGridRow[] gridRows = DataGridRows; + gridRows[currentRow].Selected = true; + numSelectedRows = 1; + + return true; + } + + return false; + case Keys.Next: + gridState[GRIDSTATE_childLinkFocused] = false; + if (dataGridRowsLength == 0) + { + return true; + } + + if (biDiKe.Shift) + { + int savedCurrentRow = currentRow; + CurrentRow = Math.Min(DataGridRowsLength - (policy.AllowAdd ? 2 : 1), currentRow + numTotallyVisibleRows); + + DataGridRow[] gridRows = DataGridRows; + for (int i = savedCurrentRow; i <= currentRow; i++) + { + if (!gridRows[i].Selected) + { + gridRows[i].Selected = true; + numSelectedRows++; + } + } + + // hide edit box + // + EndEdit(); + } + else if (biDiKe.Control && !biDiKe.Alt) + { + // map ctrl-pageDown to show the parentRows + ParentRowsVisible = true; + } + else + { + ResetSelection(); + CurrentRow = Math.Min(DataGridRowsLength - (policy.AllowAdd ? 2 : 1), + CurrentRow + numTotallyVisibleRows); + } + + break; + case Keys.Prior: + if (dataGridRowsLength == 0) + { + return true; + } + + gridState[GRIDSTATE_childLinkFocused] = false; + if (biDiKe.Shift) + { + int savedCurrentRow = currentRow; + CurrentRow = Math.Max(0, CurrentRow - numTotallyVisibleRows); + + DataGridRow[] gridRows = DataGridRows; + for (int i = savedCurrentRow; i >= currentRow; i--) + { + if (!gridRows[i].Selected) + { + gridRows[i].Selected = true; + numSelectedRows++; + } + } + + // hide the edit box + // + EndEdit(); + } + else if (biDiKe.Control && !biDiKe.Alt) + { + // map ctrl-pageUp to hide the parentRows + ParentRowsVisible = false; + } + else + { + ResetSelection(); + CurrentRow = Math.Max(0, + CurrentRow - numTotallyVisibleRows); + } + + break; + case Keys.Left: + gridState[GRIDSTATE_childLinkFocused] = false; + ResetSelection(); + if ((biDiKe.Modifiers & Keys.Modifiers) == Keys.Alt) + { + if (Caption.BackButtonVisible) + { + NavigateBack(); + } + + return true; + } + + if ((biDiKe.Modifiers & Keys.Control) == Keys.Control) + { + // we should navigate to the first visible column + CurrentColumn = firstColumnMarkedVisible; + break; + } + + if (currentCol == firstColumnMarkedVisible && currentRow != 0) + { + CurrentRow -= 1; + int newCol = MoveLeftRight(myGridTable.GridColumnStyles, myGridTable.GridColumnStyles.Count, false); + Debug.Assert(newCol != -1, "there should be at least a visible column, right?"); + CurrentColumn = newCol; + } + else + { + int newCol = MoveLeftRight(myGridTable.GridColumnStyles, currentCol, false); + if (newCol == -1) + { + if (currentRow == 0) + { + return true; + } + else + { + // go to the previous row: + CurrentRow -= 1; + CurrentColumn = lastColumnMarkedVisible; + } + } + else + { + CurrentColumn = newCol; + } + } + + break; + case Keys.Right: + gridState[GRIDSTATE_childLinkFocused] = false; + ResetSelection(); + if ((biDiKe.Modifiers & Keys.Control) == Keys.Control && !biDiKe.Alt) + { + // we should navigate to the last column that is marked as Visible + CurrentColumn = lastColumnMarkedVisible; + break; + } + + if (currentCol == lastColumnMarkedVisible && currentRow != DataGridRowsLength - 1) + { + CurrentRow += 1; + // navigate to the first visible column + CurrentColumn = firstColumnMarkedVisible; + } + else + { + int newCol = MoveLeftRight(myGridTable.GridColumnStyles, currentCol, true); + if (newCol == cols.Count + 1) + { + // navigate to the first visible column + // and the next row + // + CurrentColumn = firstColumnMarkedVisible; + CurrentRow++; + } + else + { + CurrentColumn = newCol; + } + } + + break; + case Keys.F2: + gridState[GRIDSTATE_childLinkFocused] = false; + ResetSelection(); + Edit(); + break; +#if DEBUG + case Keys.F12: + gridState[GRIDSTATE_childLinkFocused] = false; + AddNewRow(); + break; +#endif + case Keys.Home: + gridState[GRIDSTATE_childLinkFocused] = false; + if (dataGridRowsLength == 0) + { + return true; + } + + ResetSelection(); + CurrentColumn = 0; + if (biDiKe.Control && !biDiKe.Alt) + { + int currentRowSaved = currentRow; + CurrentRow = 0; + + if (biDiKe.Shift) + { + // Ctrl-Shift-Home will select all the rows up to the first one + DataGridRow[] gridRows = DataGridRows; + for (int i = 0; i <= currentRowSaved; i++) + { + gridRows[i].Selected = true; + numSelectedRows++; + } + + // hide the edit box: + EndEdit(); + } + + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + return true; + } + + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + break; + case Keys.Delete: + gridState[GRIDSTATE_childLinkFocused] = false; + if (policy.AllowRemove && numSelectedRows > 0) + { +#if DEBUG + // when the list is empty, then the position + // in the listManager is -1, and the currentPosition in the grid is 0 + if (ListManager is not null && ListManager.Count > 0) + { + Debug.Assert(ListManager.Position == currentRow, + "Current row out of sync with DataSource", + "The DataSource's Position property should be mirrored by the CurrentCell.RowNumber of the DataGrid."); + } +#endif // DEBUG + + gridState[GRIDSTATE_inDeleteRow] = true; + DeleteRows(localGridRows); + // set the currentRow to the position in the list + currentRow = listManager.Count == 0 ? 0 : listManager.Position; + numSelectedRows = 0; + } + else + { + // if we did not use the the Delete key, let the dataGridTextBox use it + return false; + } + + break; + case Keys.End: + gridState[GRIDSTATE_childLinkFocused] = false; + if (dataGridRowsLength == 0) + { + return true; + } + + ResetSelection(); + // go the the last visible column + CurrentColumn = lastColumnMarkedVisible; + + if (biDiKe.Control && !biDiKe.Alt) + { + int savedCurrentRow = currentRow; + CurrentRow = Math.Max(0, DataGridRowsLength - (policy.AllowAdd ? 2 : 1)); + + if (biDiKe.Shift) + { + // Ctrl-Shift-Home will select all the rows up to the first one + DataGridRow[] gridRows = DataGridRows; + for (int i = savedCurrentRow; i <= currentRow; i++) + { + gridRows[i].Selected = true; + } + + numSelectedRows = currentRow - savedCurrentRow + 1; + // hide the edit box + // + EndEdit(); + } + + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + return true; + } + + Debug.Assert(ListManager.Position == CurrentCell.RowNumber || listManager.Count == 0, "current row out of ssync with DataSource"); + break; + case Keys.Enter: + gridState[GRIDSTATE_childLinkFocused] = false; + ResetSelection(); + + // yield the return key if there is no editing + if (!gridState[GRIDSTATE_isEditing]) + { + return false; + } + + // Ctrl-Enter will call EndCurrentEdit + if ((biDiKe.Modifiers & Keys.Control) != 0 && !biDiKe.Alt) + { + EndEdit(); + HandleEndCurrentEdit(); + Edit(); // put the edit box on the screen + } + else + { + // Do not commit the edit, cause reseting the + // current cell will do that + // + // CommitEdit(); + + CurrentRow = currentRow + 1; + } + + break; + case Keys.A: + gridState[GRIDSTATE_childLinkFocused] = false; + if (biDiKe.Control && !biDiKe.Alt) + { + DataGridRow[] gridRows = DataGridRows; + for (int i = 0; i < DataGridRowsLength; i++) + { + if (gridRows[i] is DataGridRelationshipRow) + { + gridRows[i].Selected = true; + } + } + + numSelectedRows = DataGridRowsLength - (policy.AllowAdd ? 1 : 0); + // hide the edit box + // + EndEdit(); + return true; + } + + return false; + case Keys.Escape: + gridState[GRIDSTATE_childLinkFocused] = false; + ResetSelection(); + if (gridState[GRIDSTATE_isEditing]) + { + // rollback + AbortEdit(); + + // we have to invalidate the row header ( make it display the row selector instead of the pencil ) + if (layout.RowHeadersVisible && currentRow > -1) + { + Rectangle rowHdrRect = GetRowRect(currentRow); + rowHdrRect.Width = layout.RowHeaders.Width; + Invalidate(rowHdrRect); + } + + // now put the edit column back on the screen + Edit(); + } + else + { + // add this protected virtual method for the XML designer team + CancelEditing(); + Edit(); + return false; + } + + break; + } + + return true; + } + + /// + /// Previews a keyboard message and returns a value indicating if the key was + /// consumed. + /// + protected override bool ProcessKeyPreview(ref Message m) + { + if (m.MsgInternal == PInvokeCore.WM_KEYDOWN) + { + KeyEventArgs ke = new KeyEventArgs((Keys)(int)m.WParamInternal | ModifierKeys); + switch (ke.KeyCode) + { + case Keys.Up: + case Keys.Down: + case Keys.Prior: + case Keys.Next: + case Keys.Right: + case Keys.Left: + case Keys.Tab: + case Keys.Escape: + case Keys.Enter: + case Keys.OemMinus: + case Keys.Subtract: + case Keys.Oemplus: + case Keys.Add: + case Keys.Space: + case Keys.Home: + case Keys.End: + case Keys.F2: + case Keys.Delete: + case Keys.A: + return ProcessGridKey(ke); + } + + // Ctrl-Tab will be sent as a tab paired w/ a control on the KeyUp message + // + } + else if (m.MsgInternal == PInvokeCore.WM_KEYUP) + { + KeyEventArgs ke = new KeyEventArgs((Keys)(int)m.WParamInternal | ModifierKeys); + if (ke.KeyCode == Keys.Tab) + { + return ProcessGridKey(ke); + } + } + + return base.ProcessKeyPreview(ref m); + } + + /// + /// Gets a value indicating whether the Tab key should be processed. + /// + protected bool ProcessTabKey(Keys keyData) + { + if (listManager is null || myGridTable is null) + { + return false; + } + + bool wasEditing = false; + int columnCount = myGridTable.GridColumnStyles.Count; + bool biDi = isRightToLeft(); + ResetSelection(); + + // Try to commit changes to cell if we were editing + if (gridState[GRIDSTATE_isEditing]) + { + wasEditing = true; + if (!CommitEdit()) + { + //MessageBox.Show("Could not commit changes! Press Escape to abort edit"); + Edit(); // if we can't commit the value put the edit box so that the user sees where the focus is + return true; + } + } + + if ((keyData & Keys.Control) == Keys.Control) + { + // when the user hits ctrl-alt-tab just ignore it. + if ((keyData & Keys.Alt) == Keys.Alt) + { + return true; + } + + // navigate to the next control in the form + Keys ke = keyData & ~(Keys.Control); + EndEdit(); + + gridState[GRIDSTATE_editControlChanging] = true; + try + { + Focus(); + } + finally + { + gridState[GRIDSTATE_editControlChanging] = false; + } + + return base.ProcessDialogKey(ke); + } + + // see if the child relationships can use this TAB key + DataGridRow[] localRows = DataGridRows; + GridColumnStylesCollection cols = myGridTable.GridColumnStyles; + + int lastColumnMarkedVisible = 0; + int firstColumnMarkedVisible = cols.Count - 1; + // + + if (localRows.Length == 0) + { + EndEdit(); + + return base.ProcessDialogKey(keyData); + } + + for (int i = 0; i < cols.Count; i++) + { + // if (cols[i].Visible && cols[i].PropertyDescriptor is not null) { + if (cols[i].PropertyDescriptor is not null) + { + firstColumnMarkedVisible = i; + break; + } + } + + for (int i = cols.Count - 1; i >= 0; i--) + { + // if (cols[i].Visible && cols[i].PropertyDescriptor is not null) { + if (cols[i].PropertyDescriptor is not null) + { + lastColumnMarkedVisible = i; + break; + } + } + + if (CurrentColumn == lastColumnMarkedVisible) + { + if (gridState[GRIDSTATE_childLinkFocused] || (!gridState[GRIDSTATE_childLinkFocused] && (keyData & Keys.Shift) != Keys.Shift)) + { + if (localRows[CurrentRow].ProcessTabKey(keyData, layout.RowHeaders, isRightToLeft())) + { + if (cols.Count > 0) + { + cols[CurrentColumn].ConcedeFocus(); + } + + gridState[GRIDSTATE_childLinkFocused] = true; + // let the grid regain focus + // introduced because of that BeginInvoke thing in the OnLeave method.... + if (gridState[GRIDSTATE_canFocus] && CanFocus && !Focused) + { + Focus(); + } + + return true; + } + } + + // actually, it turns out that we should leave the + // control if we are in the last row + if ((currentRow == DataGridRowsLength - 1) && ((keyData & Keys.Shift) == 0)) + { + EndEdit(); + return base.ProcessDialogKey(keyData); + } + } + + if (CurrentColumn == firstColumnMarkedVisible) + { + // if the childLink is focused, then navigate within the relations + // in the row, otherwise expand the relations list for the row above + if (!gridState[GRIDSTATE_childLinkFocused]) + { + if (CurrentRow != 0 && (keyData & Keys.Shift) == Keys.Shift) + { + if (localRows[CurrentRow - 1].ProcessTabKey(keyData, layout.RowHeaders, isRightToLeft())) + { + CurrentRow--; + if (cols.Count > 0) + { + cols[CurrentColumn].ConcedeFocus(); + } + + gridState[GRIDSTATE_childLinkFocused] = true; + // let the grid regain focus + // introduced because of that BeginInvoke thing in the OnLeave method.... + if (gridState[GRIDSTATE_canFocus] && CanFocus && !Focused) + { + Focus(); + } + + return true; + } + } + } + else + { + if (localRows[CurrentRow].ProcessTabKey(keyData, layout.RowHeaders, isRightToLeft())) + { + return true; + } + else + { + // we were on the firstColumn, previously the link was focused + // we have to navigate to the last column + gridState[GRIDSTATE_childLinkFocused] = false; + CurrentColumn = lastColumnMarkedVisible; + return true; + } + } + + // if we are on the first cell ( not on the addNewRow ) + // then shift - tab should move to the next control on the form + if (currentRow == 0 && ((keyData & Keys.Shift) == Keys.Shift)) + { + EndEdit(); + return base.ProcessDialogKey(keyData); + } + } + + // move + if ((keyData & Keys.Shift) != Keys.Shift) + { + // forward + if (CurrentColumn == lastColumnMarkedVisible) + { + if (CurrentRow != DataGridRowsLength - 1) + { + CurrentColumn = firstColumnMarkedVisible; + } + + CurrentRow += 1; + } + else + { + int nextCol = MoveLeftRight(cols, currentCol, true); // true for going right; + Debug.Assert(nextCol < cols.Count, "we already checked that we are not at the lastColumnMarkedVisible"); + CurrentColumn = nextCol; + } + } + else + { + // backward + if (CurrentColumn == firstColumnMarkedVisible) + { + if (CurrentRow != 0) + { + CurrentColumn = lastColumnMarkedVisible; + } + + if (!gridState[GRIDSTATE_childLinkFocused]) // + { + CurrentRow--; + } + } + else if (gridState[GRIDSTATE_childLinkFocused] && CurrentColumn == lastColumnMarkedVisible) + { + // part deux: when we hilite the childLink and then press shift-tab, we + // don't want to navigate at the second to last column + InvalidateRow(currentRow); + Edit(); + } + else + { + int prevCol = MoveLeftRight(cols, currentCol, false); // false for going left + Debug.Assert(prevCol != -1, "we already checked that we are not at the first columnMarked visible"); + CurrentColumn = prevCol; + } + } + + // if we got here, then invalidate childLinkFocused + // + gridState[GRIDSTATE_childLinkFocused] = false; + + // Begin another edit if we were editing before + if (wasEditing) + { + ResetSelection(); + Edit(); + } + + return true; + } + + virtual protected void CancelEditing() + { + CancelCursorUpdate(); + // yield the escape key if there is no editing + // make the last row a DataGridAddNewRow + if (gridState[GRIDSTATE_inAddNewRow]) + { + gridState[GRIDSTATE_inAddNewRow] = false; + DataGridRow[] localGridRows = DataGridRows; + + localGridRows[DataGridRowsLength - 1] = new DataGridAddNewRow(this, myGridTable, DataGridRowsLength - 1); + SetDataGridRows(localGridRows, DataGridRowsLength); + } + } + + internal void RecalculateFonts() + { + try + { + linkFont = new Font(Font, FontStyle.Underline); + } + catch + { + } + + fontHeight = Font.Height; + linkFontHeight = LinkFont.Height; + captionFontHeight = CaptionFont.Height; + + if (myGridTable is null || myGridTable.IsDefault) + { + headerFontHeight = HeaderFont.Height; + } + else + { + headerFontHeight = myGridTable.HeaderFont.Height; + } + } + + // the BackButtonClicked event: + // + /// + /// Occurs when the BackButton is clicked. + /// + [ + SRCategory(nameof(SR.CatAction)), + ] + public event EventHandler BackButtonClick + { + add => Events.AddHandler(EVENT_BACKBUTTONCLICK, value); + remove => Events.RemoveHandler(EVENT_BACKBUTTONCLICK, value); + } + + // the DownButtonClick event + // + /// + /// Occurs when the Down button is clicked. + /// + [ + SRCategory(nameof(SR.CatAction)), + ] + public event EventHandler ShowParentDetailsButtonClick + { + add => Events.AddHandler(EVENT_DOWNBUTTONCLICK, value); + remove => Events.RemoveHandler(EVENT_DOWNBUTTONCLICK, value); + } + + private void ResetMouseState() + { + oldRow = -1; + gridState[GRIDSTATE_overCaption] = true; + } + + /// + /// Turns off selection for all rows that are selected. + /// + protected void ResetSelection() + { + if (numSelectedRows > 0) + { + DataGridRow[] localGridRows = DataGridRows; + for (int i = 0; i < DataGridRowsLength; ++i) + { + if (localGridRows[i].Selected) + { + localGridRows[i].Selected = false; + } + } + } + + numSelectedRows = 0; + lastRowSelected = -1; + } + + private void ResetParentRows() + { + parentRows.Clear(); + originalState = null; + caption.BackButtonActive = caption.DownButtonActive = caption.BackButtonVisible = false; + caption.SetDownButtonDirection(!layout.ParentRowsVisible); + } + + /// + /// Re-initializes all UI related state. + /// + private void ResetUIState() + { + gridState[GRIDSTATE_childLinkFocused] = false; + ResetSelection(); + ResetMouseState(); + PerformLayout(); + Invalidate(); // we want to invalidate after we set up the scrollbars + + // invalidate the horizontalscrollbar and the vertical scrollbar + // + if (horizScrollBar.Visible) + { + horizScrollBar.Invalidate(); + } + + if (vertScrollBar.Visible) + { + vertScrollBar.Invalidate(); + } + } + + /// + /// Scrolls the datagrid down an arbritrary number of rows. + /// + private void ScrollDown(int rows) + { + //Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "DataGridScrolling: ScrollDown, rows = " + rows.ToString()); + if (rows != 0) + { + ClearRegionCache(); + + // we should put "dataGridRowsLength -1" + int newFirstRow = Math.Max(0, Math.Min(firstVisibleRow + rows, DataGridRowsLength - 1)); + int oldFirstRow = firstVisibleRow; + firstVisibleRow = newFirstRow; + vertScrollBar.Value = newFirstRow; + bool wasEditing = gridState[GRIDSTATE_isEditing]; + ComputeVisibleRows(); + + if (gridState[GRIDSTATE_isScrolling]) + { + Edit(); + // isScrolling is set to TRUE when the user scrolls. + // once we move the edit box, we finished processing the scroll event, so set isScrolling to FALSE + // to set isScrolling to TRUE, we need another scroll event. + gridState[GRIDSTATE_isScrolling] = false; + } + else + { + EndEdit(); + } + + int deltaY = ComputeRowDelta(oldFirstRow, newFirstRow); + Rectangle rowsRect = layout.Data; + if (layout.RowHeadersVisible) + { + rowsRect = Rectangle.Union(rowsRect, layout.RowHeaders); + } + + RECT scrollArea = rowsRect; + User32.ScrollWindow(this, 0, deltaY, ref scrollArea, ref scrollArea); + OnScroll(EventArgs.Empty); + + if (wasEditing) + { + // invalidate the rowHeader for the + InvalidateRowHeader(currentRow); + } + } + } + + + /// + /// Scrolls the datagrid right an arbritrary number of columns. + /// + private void ScrollRight(int columns) + { + Debug.WriteLineIf(CompModSwitches.DataGridScrolling.TraceVerbose, "DataGridScrolling: ScrollRight, columns = " + columns.ToString(CultureInfo.InvariantCulture)); + int newCol = firstVisibleCol + columns; + + GridColumnStylesCollection gridColumns = myGridTable.GridColumnStyles; + int newColOffset = 0; + int nGridCols = gridColumns.Count; + int nVisibleCols = 0; + + // if we try to scroll past the last totally visible column, + // then the toolTips will dissapear + if (myGridTable.IsDefault) + { + nVisibleCols = nGridCols; + } + else + { + for (int i = 0; i < nGridCols; i++) + { + if (gridColumns[i].PropertyDescriptor is not null) + { + nVisibleCols++; + } + } + } + +#pragma warning disable SA1408 // Conditional expressions should declare precedence + if (lastTotallyVisibleCol == nVisibleCols - 1 && columns > 0 || + firstVisibleCol == 0 && columns < 0 && negOffset == 0) +#pragma warning restore SA1408 // Conditional expressions should declare precedence + { + return; + } + + newCol = Math.Min(newCol, nGridCols - 1); + + for (int i = 0; i < newCol; i++) + { + // if (gridColumns[i].Visible && gridColumns[i].PropertyDescriptor is not null) + if (gridColumns[i].PropertyDescriptor is not null) + { + newColOffset += gridColumns[i].Width; + } + } + + HorizontalOffset = newColOffset; + } + + /// + /// Scrolls a given column into visibility. + /// + private void ScrollToColumn(int targetCol) + { + // do not flush the columns to the left + // so, scroll only as many columns as is necessary. + // + + int dCols = targetCol - firstVisibleCol; + + if (targetCol > lastTotallyVisibleCol && lastTotallyVisibleCol != -1) + { + dCols = targetCol - lastTotallyVisibleCol; + } + + // if only part of the currentCol is visible + // then we should still scroll + if (dCols != 0 || negOffset != 0) + { + ScrollRight(dCols); + } + } + + /// + /// Selects a given row + /// + public void Select(int row) + { + // + Debug.WriteLineIf(CompModSwitches.DataGridSelection.TraceVerbose, "Selecting row " + row.ToString(CultureInfo.InvariantCulture)); + DataGridRow[] localGridRows = DataGridRows; + if (!localGridRows[row].Selected) + { + localGridRows[row].Selected = true; + numSelectedRows++; + } + + // when selecting a row, hide the edit box + // + EndEdit(); + } + + // this function will pair the listManager w/ a table from the TableStylesCollection. + // and for each column in the TableStylesCollection will pair them w/ a propertyDescriptor + // from the listManager + // + // prerequisite: the current table is either the default table, or has the same name as the + // list in the listManager. + // + private void PairTableStylesAndGridColumns(CurrencyManager lm, DataGridTableStyle gridTable, bool forceColumnCreation) + { + PropertyDescriptorCollection props = lm.GetItemProperties(); + GridColumnStylesCollection gridCols = gridTable.GridColumnStyles; + + // ]it is possible to have a dataTable w/ an empty string for a name. + if (!gridTable.IsDefault && string.Compare(lm.GetListName(), gridTable.MappingName, true, CultureInfo.InvariantCulture) == 0) + { + // we will force column creation only at runtime + if (gridTable.GridColumnStyles.Count == 0 && !DesignMode) + { + // we have to create some default columns for each of the propertyDescriptors + // + if (forceColumnCreation) + { + gridTable.SetGridColumnStylesCollection(lm); + } + else + { + gridTable.SetRelationsList(lm); + } + } + else + { + // it may the case that the user will have two lists w/ the same name. + // When switching binding between those different lists, we need to invalidate + // the propertyDescriptors from the current gridColumns + // + for (int i = 0; i < gridCols.Count; i++) + { + gridCols[i].PropertyDescriptor = null; + } + + // pair the propertyDescriptor from each column to the actual property descriptor + // from the listManager + // + for (int i = 0; i < props.Count; i++) + { + DataGridColumnStyle col = gridCols.MapColumnStyleToPropertyName(props[i].Name); + if (col is not null) + { + col.PropertyDescriptor = props[i]; + } + } + + // TableStyle::SetGridColumnStylesCollection will also set the + // relations list in the tableStyle. + gridTable.SetRelationsList(lm); + } + } + else + { + // we should put an assert, that this is the default Table Style +#if DEBUG + Debug.Assert(gridTable.IsDefault, "if we don't have a match, then the dataGRid should have the default table"); +#endif // DEBUG + gridTable.SetGridColumnStylesCollection(lm); + if (gridTable.GridColumnStyles.Count > 0 && gridTable.GridColumnStyles[0].Width == -1) + { +#if DEBUG + GridColumnStylesCollection cols = gridTable.GridColumnStyles; + for (int i = 0; i < cols.Count; i++) + { + Debug.Assert(cols[i].Width == -1, "if one column's width is not initialized, the same should be happening for the rest of the columns"); + } +#endif // DEBUG + InitializeColumnWidths(); + } + } + } + + /// + /// Sets the current GridTable for the DataGrid. + /// This GridTable is the table which is currently + /// being displayed on the grid. + /// + internal void SetDataGridTable(DataGridTableStyle newTable, bool forceColumnCreation) + { + // we have to listen to the dataGridTable for the propertyChangedEvent + if (myGridTable is not null) + { + // unwire the propertyChanged event + UnWireTableStylePropChanged(myGridTable); + + if (myGridTable.IsDefault) + { + // reset the propertyDescriptors on the default table. + myGridTable.GridColumnStyles.ResetPropertyDescriptors(); + + // reset the relationship list from the default table + myGridTable.ResetRelationsList(); + } + } + + myGridTable = newTable; + + WireTableStylePropChanged(myGridTable); + + layout.RowHeadersVisible = newTable.IsDefault ? RowHeadersVisible : newTable.RowHeadersVisible; + + // we need to force the grid into the dataGridTableStyle + // this way the controls in the columns will be parented + // consider this scenario: when the user finished InitializeComponent, it added + // a bunch of tables. all of those tables will have the DataGrid property set to this + // grid. however, in InitializeComponent the tables will not have parented the + // edit controls w/ the grid. + // + // the code in DataGridTextBoxColumn already checks to see if the edits are parented + // before parenting them. + // + if (newTable is not null) + { + newTable.DataGrid = this; + } + + // pair the tableStyles and GridColumns + // + if (listManager is not null) + { + PairTableStylesAndGridColumns(listManager, myGridTable, forceColumnCreation); + } + + // reset the relations UI on the newTable + if (newTable is not null) + { + newTable.ResetRelationsUI(); + } + + // set the isNavigating to false + gridState[GRIDSTATE_isNavigating] = false; + + horizScrollBar.Value = 0; + firstVisibleRow = 0; + currentCol = 0; + // if we add a tableStyle that mapps to the + // current listName, then we should set the currentRow to the + // position in the listManager + if (listManager is null) + { + currentRow = 0; + } + else + { + currentRow = listManager.Position == -1 ? 0 : listManager.Position; + } + + ResetHorizontalOffset(); + negOffset = 0; + ResetUIState(); + + // check the hierarchy + checkHierarchy = true; + } + + /// + /// Scrolls the data area down to make room for the parent rows + /// and lays out the different regions of the DataGrid. + /// + internal void SetParentRowsVisibility(bool visible) + { + Rectangle parentRowsRect = layout.ParentRows; + Rectangle underParentRows = layout.Data; + + if (layout.RowHeadersVisible) + { + underParentRows.X -= isRightToLeft() ? 0 : layout.RowHeaders.Width; + underParentRows.Width += layout.RowHeaders.Width; + } + + if (layout.ColumnHeadersVisible) + { + underParentRows.Y -= layout.ColumnHeaders.Height; + underParentRows.Height += layout.ColumnHeaders.Height; + } + + // hide the Edit Box + EndEdit(); + + if (visible) + { + layout.ParentRowsVisible = true; + PerformLayout(); + Invalidate(); + } + else + { + RECT scrollRECT = new Rectangle(underParentRows.X, underParentRows.Y - layout.ParentRows.Height, underParentRows.Width, underParentRows.Height + layout.ParentRows.Height); + + User32.ScrollWindow(this, 0, -parentRowsRect.Height, ref scrollRECT, ref scrollRECT); + + // If the vertical scrollbar was visible before and not after + // the ScrollWindow call, then we will not get invalidated + // completely. We need to translate the visual bounds of + // the scrollbar's old location up and invalidate. + // + if (vertScrollBar.Visible) + { + Rectangle fixupRect = vertScrollBar.Bounds; + fixupRect.Y -= parentRowsRect.Height; + fixupRect.Height += parentRowsRect.Height; + Invalidate(fixupRect); + } + + Debug.WriteLineIf(CompModSwitches.DataGridParents.TraceVerbose, "DataGridParents: Making parent rows invisible."); + layout.ParentRowsVisible = false; + PerformLayout(); + } + } + + /// + /// Sets whether a row is expanded or not. + /// + private void SetRowExpansionState(int row, bool expanded) + { + if (row < -1 || row > DataGridRowsLength - (policy.AllowAdd ? 2 : 1)) + { + throw new ArgumentOutOfRangeException(nameof(row)); + } + + DataGridRow[] localGridRows = DataGridRows; + if (row == -1) + { + DataGridRelationshipRow[] expandableRows = GetExpandableRows(); + bool repositionEditControl = false; + + for (int r = 0; r < expandableRows.Length; ++r) + { + if (expandableRows[r].Expanded != expanded) + { + expandableRows[r].Expanded = expanded; + repositionEditControl = true; + } + } + + if (repositionEditControl) + { + // we need to reposition the edit control + if (gridState[GRIDSTATE_isNavigating] || gridState[GRIDSTATE_isEditing]) + { + ResetSelection(); + Edit(); + } + } + } + else if (localGridRows[row] is DataGridRelationshipRow expandableRow) + { + if (expandableRow.Expanded != expanded) + { + // we need to reposition the edit control + if (gridState[GRIDSTATE_isNavigating] || gridState[GRIDSTATE_isEditing]) + { + ResetSelection(); + Edit(); + } + + expandableRow.Expanded = expanded; + } + } + } + + private void ObjectSiteChange(IContainer container, IComponent component, bool site) + { + if (site) + { + if (component.Site is null) + { + container.Add(component); + } + } + else + { + if (component.Site is not null && component.Site.Container == container) + { + container.Remove(component); + } + } + } + + public void SubObjectsSiteChange(bool site) + { + DataGrid dgrid = this; + if (dgrid.DesignMode && dgrid.Site is not null) + { + IDesignerHost host = (IDesignerHost)dgrid.Site.GetService(typeof(IDesignerHost)); + if (host is not null) + { + DesignerTransaction trans = host.CreateTransaction(); + try + { + IContainer container = dgrid.Site.Container; + + DataGridTableStyle[] tables = new DataGridTableStyle[dgrid.TableStyles.Count]; + dgrid.TableStyles.CopyTo(tables, 0); + + for (int i = 0; i < tables.Length; i++) + { + DataGridTableStyle table = tables[i]; + ObjectSiteChange(container, table, site); + + DataGridColumnStyle[] columns = new DataGridColumnStyle[table.GridColumnStyles.Count]; + table.GridColumnStyles.CopyTo(columns, 0); + + for (int j = 0; j < columns.Length; j++) + { + DataGridColumnStyle column = columns[j]; + ObjectSiteChange(container, column, site); + } + } + } + finally + { + trans.Commit(); + } + } + } + } + + /// + /// Unselects a given row + /// + public void UnSelect(int row) + { + // + Debug.WriteLineIf(CompModSwitches.DataGridSelection.TraceVerbose, "DataGridSelection: Unselecting row " + row.ToString(CultureInfo.InvariantCulture)); + DataGridRow[] localGridRows = DataGridRows; + if (localGridRows[row].Selected) + { + localGridRows[row].Selected = false; + numSelectedRows--; + } + } + + /// + /// Asks the cursor to update. + /// + private void UpdateListManager() + { + Debug.WriteLineIf(CompModSwitches.DataGridCursor.TraceVerbose, "DataGridCursor: Requesting EndEdit()"); + try + { + if (listManager is not null) + { + EndEdit(); + listManager.EndCurrentEdit(); + } + } + catch + { + } + } + + /// + /// Will return the string that will be used as a delimiter between columns + /// when copying rows contents to the Clipboard. + /// At the moment, return "\t" + /// + protected virtual string GetOutputTextDelimiter() + { + return "\t"; + } + + /// + /// The accessible object class for a DataGrid. The child accessible objects + /// are accessible objects corresponding to the propertygrid entries. + /// + [ComVisible(true)] + internal class DataGridAccessibleObject : ControlAccessibleObject + { + /// + /// Construct a PropertyGridViewAccessibleObject + /// + public DataGridAccessibleObject(DataGrid owner) : base(owner) + { + } + + internal DataGrid DataGrid + { + get + { + return (DataGrid)Owner; + } + } + + private int ColumnCountPrivate + { + get + { + return ((DataGrid)Owner).myGridTable.GridColumnStyles.Count; + } + } + + private int RowCountPrivate + { + get + { + return ((DataGrid)Owner).dataGridRows.Length; + } + } + + public override string Name + { + get + { + // Special case: If an explicit name has been set in the AccessibleName property, use that. + // Note: Any non-null value in AccessibleName overrides the default accessible name logic, + // even an empty string (this is the only way to *force* the accessible name to be blank). + string name = Owner.AccessibleName; + if (name is not null) + { + return name; + } + + // Otherwise just return the default label string, minus any mnemonics + return "DataGrid"; + } + + set + { + // If anyone tries to set the accessible name, just cache the value in the control's + // AccessibleName property. This value will then end up overriding the normal accessible + // name logic, until such time as AccessibleName is set back to null. + Owner.AccessibleName = value; + } + } + + public override AccessibleRole Role + { + get + { + AccessibleRole role = Owner.AccessibleRole; + if (role != AccessibleRole.Default) + { + return role; + } + + return AccessibleRole.Table; + } + } + + public override AccessibleObject GetChild(int index) + { + DataGrid dataGrid = (DataGrid)Owner; + + int cols = ColumnCountPrivate; + int rows = RowCountPrivate; + + if (dataGrid.dataGridRows is null) + { + dataGrid.CreateDataGridRows(); + } + + if (index < 1) + { + return dataGrid.ParentRowsAccessibleObject; + } + else + { + index -= 1; + if (index < cols) + { + return dataGrid.myGridTable.GridColumnStyles[index].HeaderAccessibleObject; + } + else + { + index -= cols; + + if (index < rows) + { + Debug.Assert(dataGrid.dataGridRows[index].RowNumber == index, "Row number is wrong!"); + return dataGrid.dataGridRows[index].AccessibleObject; + } + else + { + index -= rows; + + if (dataGrid.horizScrollBar.Visible) + { + if (index == 0) + { + return dataGrid.horizScrollBar.AccessibilityObject; + } + + index--; + } + + if (dataGrid.vertScrollBar.Visible) + { + if (index == 0) + { + return dataGrid.vertScrollBar.AccessibilityObject; + } + + index--; + } + + int colCount = dataGrid.myGridTable.GridColumnStyles.Count; + int rowCount = dataGrid.dataGridRows.Length; + int currentRow = index / colCount; + int currentCol = index % colCount; + + if (currentRow < dataGrid.dataGridRows.Length && currentCol < dataGrid.myGridTable.GridColumnStyles.Count) + { + return dataGrid.dataGridRows[currentRow].AccessibleObject.GetChild(currentCol); + } + } + } + } + + return null; + } + + public override int GetChildCount() + { + int n = 1 + ColumnCountPrivate + ((DataGrid)Owner).DataGridRowsLength; + if (DataGrid.horizScrollBar.Visible) + { + n++; + } + + if (DataGrid.vertScrollBar.Visible) + { + n++; + } + + n += DataGrid.DataGridRows.Length * DataGrid.myGridTable.GridColumnStyles.Count; + return n; + } + + public override AccessibleObject GetFocused() + { + if (DataGrid.Focused) + { + return GetSelected(); + } + + return null; + } + + public override AccessibleObject GetSelected() + { + if (DataGrid.DataGridRows.Length == 0 || DataGrid.myGridTable.GridColumnStyles.Count == 0) + { + return null; + } + + DataGridCell cell = DataGrid.CurrentCell; + return GetChild(1 + ColumnCountPrivate + cell.RowNumber).GetChild(cell.ColumnNumber); + } + + public override AccessibleObject HitTest(int x, int y) + { + Point client = DataGrid.PointToClient(new Point(x, y)); + HitTestInfo hti = DataGrid.HitTest(client.X, client.Y); + + switch (hti.Type) + { + case HitTestType.RowHeader: + return GetChild(1 + ColumnCountPrivate + hti.Row); + case HitTestType.Cell: + return GetChild(1 + ColumnCountPrivate + hti.Row).GetChild(hti.Column); + case HitTestType.ColumnHeader: + return GetChild(1 + hti.Column); + case HitTestType.ParentRows: + return DataGrid.ParentRowsAccessibleObject; + case HitTestType.None: + case HitTestType.ColumnResize: + case HitTestType.RowResize: + case HitTestType.Caption: + break; + } + + return null; + } + + public override AccessibleObject Navigate(AccessibleNavigation navdir) + { + // We're only handling FirstChild and LastChild here + if (GetChildCount() > 0) + { + switch (navdir) + { + case AccessibleNavigation.FirstChild: + return GetChild(0); + case AccessibleNavigation.LastChild: + return GetChild(GetChildCount() - 1); + } + } + + return null; // Perform default behavior + } + } + + // + // This simple data structure holds all of the layout information + // for the DataGrid. + // + internal class LayoutData + { + internal bool dirty = true; + // region inside the Control's borders. + public Rectangle Inside = Rectangle.Empty; + + public Rectangle RowHeaders = Rectangle.Empty; + + public Rectangle TopLeftHeader = Rectangle.Empty; + public Rectangle ColumnHeaders = Rectangle.Empty; + public Rectangle Data = Rectangle.Empty; + + public Rectangle Caption = Rectangle.Empty; + public Rectangle ParentRows = Rectangle.Empty; + + public Rectangle ResizeBoxRect = Rectangle.Empty; + + public bool ColumnHeadersVisible; + public bool RowHeadersVisible; + public bool CaptionVisible; + public bool ParentRowsVisible; + + // used for resizing. + public Rectangle ClientRectangle = Rectangle.Empty; + + public LayoutData() + { + } + + public LayoutData(LayoutData src) + { + GrabLayout(src); + } + + private void GrabLayout(LayoutData src) + { + Inside = src.Inside; + TopLeftHeader = src.TopLeftHeader; + ColumnHeaders = src.ColumnHeaders; + RowHeaders = src.RowHeaders; + Data = src.Data; + Caption = src.Caption; + ParentRows = src.ParentRows; + ResizeBoxRect = src.ResizeBoxRect; + ColumnHeadersVisible = src.ColumnHeadersVisible; + RowHeadersVisible = src.RowHeadersVisible; + CaptionVisible = src.CaptionVisible; + ParentRowsVisible = src.ParentRowsVisible; + ClientRectangle = src.ClientRectangle; + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(200); + sb.Append(base.ToString()); + sb.Append(" { \n"); + sb.Append("Inside = "); + sb.Append(Inside.ToString()); + sb.Append('\n'); + sb.Append("TopLeftHeader = "); + sb.Append(TopLeftHeader.ToString()); + sb.Append('\n'); + sb.Append("ColumnHeaders = "); + sb.Append(ColumnHeaders.ToString()); + sb.Append('\n'); + sb.Append("RowHeaders = "); + sb.Append(RowHeaders.ToString()); + sb.Append('\n'); + sb.Append("Data = "); + sb.Append(Data.ToString()); + sb.Append('\n'); + sb.Append("Caption = "); + sb.Append(Caption.ToString()); + sb.Append('\n'); + sb.Append("ParentRows = "); + sb.Append(ParentRows.ToString()); + sb.Append('\n'); + sb.Append("ResizeBoxRect = "); + sb.Append(ResizeBoxRect.ToString()); + sb.Append('\n'); + sb.Append("ColumnHeadersVisible = "); + sb.Append(ColumnHeadersVisible); + sb.Append('\n'); + sb.Append("RowHeadersVisible = "); + sb.Append(RowHeadersVisible); + sb.Append('\n'); + sb.Append("CaptionVisible = "); + sb.Append(CaptionVisible); + sb.Append('\n'); + sb.Append("ParentRowsVisible = "); + sb.Append(ParentRowsVisible); + sb.Append('\n'); + sb.Append("ClientRectangle = "); + sb.Append(ClientRectangle.ToString()); + sb.Append(" } "); + return sb.ToString(); + } + } + + /// + /// Contains information + /// about the part of the control the user + /// has clicked. This class cannot be inherited. + /// + public sealed class HitTestInfo + { + internal HitTestType type = HitTestType.None; + + internal int row; + internal int col; + + /// + /// Allows the object to inform you the + /// extent of the grid. + /// + public static readonly HitTestInfo Nowhere = new HitTestInfo(); + + internal HitTestInfo() + { + type = (HitTestType)0; + row = col = -1; + } + + internal HitTestInfo(HitTestType type) + { + this.type = type; + row = col = -1; + } + + /// + /// Gets the number of the clicked column. + /// + public int Column + { + get + { + return col; + } + } + + /// + /// Gets the + /// number of the clicked row. + /// + public int Row + { + get + { + return row; + } + } + + /// + /// Gets the part of the control, other than the row or column, that was + /// clicked. + /// + public HitTestType Type + { + get + { + return type; + } + } + + /// + /// Indicates whether two objects are identical. + /// + public override bool Equals(object obj) + { + if (obj is HitTestInfo ci) + { + return (type == ci.type && + row == ci.row && + col == ci.col); + } + + return false; + } + + /// + /// Gets the hash code for the instance. + /// + public override int GetHashCode() => HashCode.Combine(type, row, col); + + /// + /// Gets the type, row number, and column number. + /// + public override string ToString() + { + return "{ " + ((type).ToString()) + "," + row.ToString(CultureInfo.InvariantCulture) + "," + col.ToString(CultureInfo.InvariantCulture) + "}"; + } + } + + /// + /// Specifies the part of the + /// control the user has clicked. + /// + [Flags] + public enum HitTestType + { + None = 0x00000000, + Cell = 0x00000001, + ColumnHeader = 0x00000002, + RowHeader = 0x00000004, + ColumnResize = 0x00000008, + RowResize = 0x00000010, + Caption = 0x00000020, + ParentRows = 0x00000040 + } + + /// + /// Holds policy information for what the grid can and cannot do. + /// + private class Policy + { + private bool allowAdd = true; + private bool allowEdit = true; + private bool allowRemove = true; + + public Policy() + { + } + + public bool AllowAdd + { + get + { + return allowAdd; + } + set + { + if (allowAdd != value) + { + allowAdd = value; + } + } + } + + public bool AllowEdit + { + get + { + return allowEdit; + } + set + { + if (allowEdit != value) + { + allowEdit = value; + } + } + } + + public bool AllowRemove + { + get + { + return allowRemove; + } + set + { + if (allowRemove != value) + { + allowRemove = value; + } + } + } + + // returns true if the UI needs to be updated (here because addnew has changed) + public bool UpdatePolicy(CurrencyManager listManager, bool gridReadOnly) + { + bool change = false; + // only IBindingList can have an AddNewRow + IBindingList bl = listManager is null ? null : listManager.List as IBindingList; + if (listManager is null) + { + if (!allowAdd) + { + change = true; + } + + allowAdd = allowEdit = allowRemove = true; + } + else + { + if (AllowAdd != listManager.AllowAdd && !gridReadOnly) + { + change = true; + } + + AllowAdd = listManager.AllowAdd && !gridReadOnly && bl is not null && bl.SupportsChangeNotification; + AllowEdit = listManager.AllowEdit && !gridReadOnly; + AllowRemove = listManager.AllowRemove && !gridReadOnly && bl is not null && bl.SupportsChangeNotification; // + } + + return change; + } + } + + // + // Given the x coordinate and the Width of rectangle R1 inside rectangle rect, + // this function returns the x coordinate of the rectangle that + // corresponds to the Bi-Di transformation + // + private int MirrorRectangle(Rectangle R1, Rectangle rect, bool rightToLeft) + { + if (rightToLeft) + { + return rect.Right + rect.X - R1.Right; + } + else + { + return R1.X; + } + } + + // + // Given the x coordinate of a point inside rectangle rect, + // this function returns the x coordinate of the point that + // corresponds to the Bi-Di transformation + // + private int MirrorPoint(int x, Rectangle rect, bool rightToLeft) + { + if (rightToLeft) + { + return rect.Right + rect.X - x; + } + else + { + return x; + } + } + + // This function will return true if the RightToLeft property of the dataGrid is + // set to YES + private bool isRightToLeft() + { + return (RightToLeft == RightToLeft.Yes); + } } - - protected virtual void OnBorderStyleChanged(EventArgs e) { } - - protected virtual void OnCaptionVisibleChanged(EventArgs e) { } - - protected virtual void OnCurrentCellChanged(EventArgs e) { } - - protected virtual void OnFlatModeChanged(EventArgs e) { } - - protected virtual void OnBackgroundColorChanged(EventArgs e) { } - - protected virtual void OnAllowNavigationChanged(EventArgs e) { } - - protected virtual void OnParentRowsVisibleChanged(EventArgs e) { } - - protected virtual void OnParentRowsLabelStyleChanged(EventArgs e) { } - - protected virtual void OnReadOnlyChanged(EventArgs e) { } - - protected void OnNavigate(NavigateEventArgs e) { } - - protected void OnRowHeaderClick(EventArgs e) { } - - protected void OnScroll(EventArgs e) { } - - protected virtual void GridHScrolled(object sender, ScrollEventArgs se) { } - - protected virtual void GridVScrolled(object sender, ScrollEventArgs se) { } - - protected void OnBackButtonClicked(object sender, EventArgs e) { } - - protected virtual void OnDataSourceChanged(EventArgs e) { } - - protected void OnShowParentDetailsButtonClicked(object sender, EventArgs e) { } - - public event NavigateEventHandler Navigate - { - add { } - remove { } - } - - protected event EventHandler RowHeaderClick - { - add { } - remove { } - } - - public event EventHandler Scroll - { - add { } - remove { } - } - - public bool BeginEdit(DataGridColumnStyle gridColumn, int rowNumber) => throw null; - - public void BeginInit() { } - - public void Collapse(int row) { } - - protected internal virtual void ColumnStartedEditing(Rectangle bounds) { } - - protected internal virtual void ColumnStartedEditing(Control editingControl) { } - - public bool EndEdit(DataGridColumnStyle gridColumn, int rowNumber, bool shouldAbort) => throw null; - - public void Expand(int row) { } - - protected virtual DataGridColumnStyle CreateGridColumn(PropertyDescriptor prop, bool isDefault) => throw null; - - protected virtual DataGridColumnStyle CreateGridColumn(PropertyDescriptor prop) => throw null; - - public void EndInit() { } - - public Rectangle GetCurrentCellBounds() => throw null; - - public Rectangle GetCellBounds(int row, int col) => throw null; - - public Rectangle GetCellBounds(DataGridCell dgc) => throw null; - - public HitTestInfo HitTest(int x, int y) => throw null; - - public HitTestInfo HitTest(Point position) => throw null; - - public bool IsExpanded(int rowNumber) => throw null; - - public bool IsSelected(int row) => throw null; - - public void NavigateBack() { } - - public void NavigateTo(int rowNumber, string relationName) { } - - protected bool ProcessGridKey(KeyEventArgs ke) => throw null; - - protected bool ProcessTabKey(Keys keyData) => throw null; - - protected virtual void CancelEditing() { } - - public event EventHandler BackButtonClick - { - add { } - remove { } - } - - public event EventHandler ShowParentDetailsButtonClick - { - add { } - remove { } - } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler TextChanged - { - add { } - remove { } - } - - protected void ResetSelection() { } - - public void Select(int row) { } - - public void SubObjectsSiteChange(bool site) { } - - public void UnSelect(int row) { } - - protected virtual string GetOutputTextDelimiter() => throw null; -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridAddNewRow.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridAddNewRow.cs new file mode 100644 index 00000000000..48af3cd7715 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridAddNewRow.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1510, CSIsNull001, CSIsNull002, WFDEV006 + +#nullable disable + +using System.Drawing; + +namespace System.Windows.Forms; + /// + /// This class fully encapsulates the painting logic for an addnew row + /// appearing in a DataGrid. + /// + internal class DataGridAddNewRow : DataGridRow + { + private bool dataBound; + + public DataGridAddNewRow(DataGrid dGrid, DataGridTableStyle gridTable, int rowNum) + : base(dGrid, gridTable, rowNum) + { + } + + // =------------------------------------------------------------------ + // = Methods + // =------------------------------------------------------------------ + + /// + /// Since the DataView does not return a valid DataRow for + /// a newly added row, the DataGrid sets this property to + /// true to signal that the AddNewRow can safely render + /// row contents and permit editing, etc because a DataRecord + /// exists in the cursor that created this row. + /// + public bool DataBound + { + get + { + return dataBound; + } + set + { + dataBound = value; + } + } + + public override void OnEdit() + { + if (!DataBound) + { + DataGrid.AddNewRow(); + } + } + + public override void OnRowLeave() + { + if (DataBound) + { + DataBound = false; + } + } + + // the addNewRow has nothing to do with losing focus + // + internal override void LoseChildFocus(Rectangle rowHeader, bool alignToRight) + { + } + + // the newDataRow has nothing to do with TAB keys + // + internal override bool ProcessTabKey(Keys keyData, Rectangle rowHeaders, bool alignToRight) + { + return false; + } + + /// + /// Paints the row. + /// + public override int Paint(Graphics g, Rectangle bounds, Rectangle trueRowBounds, int firstVisibleColumn, int columnCount) + { + return Paint(g, bounds, trueRowBounds, firstVisibleColumn, columnCount, false); + } + + public override int Paint(Graphics g, + Rectangle bounds, + Rectangle trueRowBounds, + int firstVisibleColumn, + int columnCount, + bool alignToRight) + { + Rectangle dataBounds = bounds; + DataGridLineStyle gridStyle; + if (dgTable.IsDefault) + { + gridStyle = DataGrid.GridLineStyle; + } + else + { + gridStyle = dgTable.GridLineStyle; + } + + int bWidth = DataGrid == null ? 0 : gridStyle == DataGridLineStyle.Solid ? 1 : 0; + dataBounds.Height -= bWidth; + int cx = base.PaintData(g, dataBounds, firstVisibleColumn, columnCount, alignToRight); + + if (bWidth > 0) + { + PaintBottomBorder(g, bounds, cx, bWidth, alignToRight); + } + + return cx; + } + + protected override void PaintCellContents(Graphics g, Rectangle cellBounds, DataGridColumnStyle column, + Brush backBr, Brush foreBrush, bool alignToRight) + { + if (DataBound) + { + CurrencyManager listManager = DataGrid.ListManager; + column.Paint(g, cellBounds, listManager, RowNumber, alignToRight); + } + else + { + base.PaintCellContents(g, cellBounds, column, backBr, foreBrush, alignToRight); + } + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridBoolColumn.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridBoolColumn.cs index 92c4b866ea2..8b269899f94 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridBoolColumn.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridBoolColumn.cs @@ -1,105 +1,536 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1510, CSIsNull001, CSIsNull002, RS0016, IDE0100, CA1822 + +#nullable disable + using System.ComponentModel; using System.Drawing; namespace System.Windows.Forms; + /// + /// Specifies a column in + /// which each cell contains a check box for representing + /// a boolean value. + /// + public class DataGridBoolColumn : DataGridColumnStyle + { + private const int idealCheckSize = 14; -#nullable disable + private bool isEditing; + private bool isSelected; + private bool allowNull = true; + private int editingRow = -1; + private object currentValue = Convert.DBNull; -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -public class DataGridBoolColumn : DataGridColumnStyle -{ - public DataGridBoolColumn() => throw new PlatformNotSupportedException(); - - public DataGridBoolColumn(PropertyDescriptor prop) => throw new PlatformNotSupportedException(); - - public DataGridBoolColumn(PropertyDescriptor prop, bool isDefault) => throw new PlatformNotSupportedException(); - - [DefaultValue(true)] - public bool AllowNull - { - get => throw null; - set { } - } + private object trueValue = true; + private object falseValue = false; + private object nullValue = Convert.DBNull; - public event EventHandler AllowNullChanged - { - add { } - remove { } - } + private static readonly object EventTrueValue = new object(); + private static readonly object EventFalseValue = new object(); + private static readonly object EventAllowNull = new object(); - [TypeConverter(typeof(StringConverter))] - [DefaultValue(true)] - public object TrueValue - { - get => throw null; - set { } - } + /// + /// Initializes a new instance of the class. + /// + public DataGridBoolColumn() : base() { } - public event EventHandler TrueValueChanged - { - add { } - remove { } - } + /// + /// Initializes a new instance of a with the specified . + /// + public DataGridBoolColumn(PropertyDescriptor prop) + : base(prop) { } - [TypeConverter(typeof(StringConverter))] - [DefaultValue(false)] - public object FalseValue - { - get => throw null; - set { } - } + public DataGridBoolColumn(PropertyDescriptor prop, bool isDefault) + : base(prop, isDefault) { } - public event EventHandler FalseValueChanged - { - add { } - remove { } - } + /// + /// Gets or sets the actual value used when setting the + /// value of the column to . + /// + [TypeConverter(typeof(StringConverter)), + DefaultValue(true)] + public object TrueValue + { + get + { + return trueValue; + } + set + { + if (!trueValue.Equals(value)) + { + trueValue = value; + OnTrueValueChanged(EventArgs.Empty); + Invalidate(); + } + } + } - [TypeConverter(typeof(StringConverter))] - public object NullValue - { - get => throw null; - set { } - } + public event EventHandler TrueValueChanged + { + add => Events.AddHandler(EventTrueValue, value); + remove => Events.RemoveHandler(EventTrueValue, value); + } + + /// + /// Gets or sets the actual value used when setting the value of the column to + /// . + /// + [TypeConverter(typeof(StringConverter)), DefaultValue(false)] + public object FalseValue + { + get + { + return falseValue; + } + set + { + if (!falseValue.Equals(value)) + { + falseValue = value; + OnFalseValueChanged(EventArgs.Empty); + Invalidate(); + } + } + } + + public event EventHandler FalseValueChanged + { + add => Events.AddHandler(EventFalseValue, value); + remove => Events.RemoveHandler(EventFalseValue, value); + } + + /// + /// Gets or sets the actual value used when setting the value of the column to + /// . + /// + [TypeConverter(typeof(StringConverter))] + public object NullValue + { + get + { + return nullValue; + } + set + { + if (!nullValue.Equals(value)) + { + nullValue = value; + OnFalseValueChanged(EventArgs.Empty); + Invalidate(); + } + } + } + + // =------------------------------------------------------------------ + // = Methods + // =------------------------------------------------------------------ + + // when the grid is in addNewRow it means that the user did not start typing + // so there is no data to be pushed back into the backEnd. + // make isEditing false so that in the Commit call we do not do any work. + // + protected internal override void ConcedeFocus() + { + base.ConcedeFocus(); + isSelected = false; + isEditing = false; + } + + private Rectangle GetCheckBoxBounds(Rectangle bounds, bool alignToRight) + { + if (alignToRight) + { + return new Rectangle(bounds.X + ((bounds.Width - idealCheckSize) / 2), + bounds.Y + ((bounds.Height - idealCheckSize) / 2), + bounds.Width < idealCheckSize ? bounds.Width : idealCheckSize, + idealCheckSize); + } + else + { + return new Rectangle(Math.Max(0, bounds.X + ((bounds.Width - idealCheckSize) / 2)), + Math.Max(0, bounds.Y + ((bounds.Height - idealCheckSize) / 2)), + bounds.Width < idealCheckSize ? bounds.Width : idealCheckSize, + idealCheckSize); + } + } + + /// + /// Gets the value at the specified row. + /// + protected internal override object GetColumnValueAtRow(CurrencyManager source, int rowNum) + { + object baseValue = base.GetColumnValueAtRow(source, rowNum); + object value = Convert.DBNull; + if (baseValue.Equals(trueValue)) + { + value = true; + } + else if (baseValue.Equals(falseValue)) + { + value = false; + } + + return value; + } + + private bool IsReadOnly() + { + bool ret = ReadOnly; + if (DataGridTableStyle != null) + { + ret = ret || DataGridTableStyle.ReadOnly; + if (DataGridTableStyle.DataGrid != null) + { + ret = ret || DataGridTableStyle.DataGrid.ReadOnly; + } + } + + return ret; + } + + /// + /// Sets the value a a specified row. + /// + protected internal override void SetColumnValueAtRow(CurrencyManager source, int rowNum, object value) + { + object baseValue = null; + if (true.Equals(value)) + { + baseValue = TrueValue; + } + else if (false.Equals(value)) + { + baseValue = FalseValue; + } + else if (Convert.IsDBNull(value)) + { + baseValue = NullValue; + } - // We need a concrete implementation for an abstract method. - protected internal override Size GetPreferredSize(Graphics g, object value) => throw null; + currentValue = baseValue; + base.SetColumnValueAtRow(source, rowNum, baseValue); + } - protected internal override int GetMinimumHeight() => throw null; + /// + /// Gets the optimum width and height of a cell given + /// a specific value to contain. + /// + protected internal override Size GetPreferredSize(Graphics g, object value) + { + return new Size(idealCheckSize + 2, idealCheckSize + 2); + } - protected internal override int GetPreferredHeight(Graphics g, object value) => throw null; + /// + /// Gets + /// the height of a cell in a column. + /// + protected internal override int GetMinimumHeight() + { + return idealCheckSize + 2; + } - protected internal override void Abort(int rowNum) { } + /// + /// Gets the height used when resizing columns. + /// + protected internal override int GetPreferredHeight(Graphics g, object value) + { + return idealCheckSize + 2; + } - protected internal override bool Commit(CurrencyManager dataSource, int rowNum) => throw null; + /// + /// Initiates a request to interrupt an edit procedure. + /// + protected internal override void Abort(int rowNum) + { + isSelected = false; + isEditing = false; + Invalidate(); + return; + } - protected internal override void Edit(CurrencyManager source, - int rowNum, - Rectangle bounds, - bool readOnly, - string displayText, - bool cellIsVisible) - { } + /// + /// Initiates a request to complete an editing procedure. + /// + protected internal override bool Commit(CurrencyManager dataSource, int rowNum) + { + isSelected = false; + // always invalidate + Invalidate(); + if (!isEditing) + { + return true; + } - protected internal override void Paint(Graphics g, - Rectangle bounds, - CurrencyManager source, - int rowNum, - bool alignToRight) - { } + SetColumnValueAtRow(dataSource, rowNum, currentValue); + isEditing = false; + return true; + } - protected internal override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum) - { } -} + /// + /// Prepares the cell for editing a value. + /// + protected internal override void Edit(CurrencyManager source, + int rowNum, + Rectangle bounds, + bool readOnly, + string displayText, + bool cellIsVisible) + { + // toggle state right now... + isSelected = true; + + // move the focus away from the previous column and give it to the grid + // + DataGrid grid = DataGridTableStyle.DataGrid; + if (!grid.Focused) + { + grid.Focus(); + } + + if (!readOnly && !IsReadOnly()) + { + editingRow = rowNum; + currentValue = GetColumnValueAtRow(source, rowNum); + } + + base.Invalidate(); + } + + /// + /// Provides a handler for determining which key was pressed, and whether to + /// process it. + /// + internal override bool KeyPress(int rowNum, Keys keyData) + { + if (isSelected && editingRow == rowNum && !IsReadOnly()) + { + if ((keyData & Keys.KeyCode) == Keys.Space) + { + ToggleValue(); + Invalidate(); + return true; + } + } + + return base.KeyPress(rowNum, keyData); + } + + /// + /// Indicates whether the a mouse down event occurred at the specified row, at + /// the specified x and y coordinates. + /// + internal override bool MouseDown(int rowNum, int x, int y) + { + base.MouseDown(rowNum, x, y); + if (isSelected && editingRow == rowNum && !IsReadOnly()) + { + ToggleValue(); + Invalidate(); + return true; + } + + return false; + } + + private void OnTrueValueChanged(EventArgs e) + { + if (Events[EventTrueValue] is EventHandler eh) + { + eh(this, e); + } + } + + private void OnFalseValueChanged(EventArgs e) + { + if (Events[EventFalseValue] is EventHandler eh) + { + eh(this, e); + } + } + + private void OnAllowNullChanged(EventArgs e) + { + if (Events[EventAllowNull] is EventHandler eh) + { + eh(this, e); + } + } + + /// + /// Draws the + /// with the given , + /// and row number. + /// + protected internal override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum) + { + Paint(g, bounds, source, rowNum, false); + } + + /// + /// Draws the + /// with the given , , + /// row number, and alignment settings. + /// + protected internal override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, bool alignToRight) + { + Paint(g, bounds, source, rowNum, DataGridTableStyle.BackBrush, DataGridTableStyle.ForeBrush, alignToRight); + } + + /// + /// Draws the with the given , , + /// row number, , and . + /// + protected internal override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, + Brush backBrush, Brush foreBrush, + bool alignToRight) + { + object value = (isEditing && editingRow == rowNum) ? currentValue : GetColumnValueAtRow(source, rowNum); + ButtonState checkedState = ButtonState.Inactive; + if (!Convert.IsDBNull(value)) + { + checkedState = ((bool)value ? ButtonState.Checked : ButtonState.Normal); + } + + Rectangle box = GetCheckBoxBounds(bounds, alignToRight); + + Region r = g.Clip; + g.ExcludeClip(box); + + Brush selectionBrush = DataGridTableStyle.IsDefault ? DataGridTableStyle.DataGrid.SelectionBackBrush : DataGridTableStyle.SelectionBackBrush; + if (isSelected && editingRow == rowNum && !IsReadOnly()) + { + g.FillRectangle(selectionBrush, bounds); + } + else + { + g.FillRectangle(backBrush, bounds); + } + + g.Clip = r; + + if (checkedState == ButtonState.Inactive) + { + ControlPaint.DrawMixedCheckBox(g, box, ButtonState.Checked); + } + else + { + ControlPaint.DrawCheckBox(g, box, checkedState); + } + + // if the column is read only we should still show selection + if (IsReadOnly() && isSelected && source.Position == rowNum) + { + bounds.Inflate(-1, -1); + Pen pen = new Pen(selectionBrush) + { + DashStyle = System.Drawing.Drawing2D.DashStyle.Dash + }; + g.DrawRectangle(pen, bounds); + pen.Dispose(); + // restore the bounds rectangle + bounds.Inflate(1, 1); + } + } + + /// + /// Gets or sets a value indicating whether null values are allowed. + /// + [ + SRCategory(nameof(SR.CatBehavior)), + DefaultValue(true), + ] + public bool AllowNull + { + get + { + return allowNull; + } + set + { + if (allowNull != value) + { + allowNull = value; + // if we do not allow null, and the gridColumn had + // a null in it, discard it + if (!value && Convert.IsDBNull(currentValue)) + { + currentValue = false; + Invalidate(); + } + + OnAllowNullChanged(EventArgs.Empty); + } + } + } + + public event EventHandler AllowNullChanged + { + add => Events.AddHandler(EventAllowNull, value); + remove => Events.RemoveHandler(EventAllowNull, value); + } + + /// + /// Enters a into the column. + /// + protected internal override void EnterNullValue() + { + // do not throw an exception when the column is marked as readOnly or + // does not allowNull + if (!AllowNull || IsReadOnly()) + { + return; + } + + if (currentValue != Convert.DBNull) + { + currentValue = Convert.DBNull; + Invalidate(); + } + } + + private void ResetNullValue() + { + NullValue = Convert.DBNull; + } + + private bool ShouldSerializeNullValue() + { + return nullValue != Convert.DBNull; + } + + private void ToggleValue() + { + if (currentValue is bool && ((bool)currentValue) == false) + { + currentValue = true; + } + else + { + if (AllowNull) + { + if (Convert.IsDBNull(currentValue)) + { + currentValue = false; + } + else + { + currentValue = Convert.DBNull; + } + } + else + { + currentValue = false; + } + } + + // we started editing + isEditing = true; + // tell the dataGrid that things are changing + // we put Rectangle.Empty cause toggle will invalidate the row anyhow + DataGridTableStyle.DataGrid.ColumnStartedEditing(Rectangle.Empty); + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridCaption.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridCaption.cs new file mode 100644 index 00000000000..aa6b8257be2 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridCaption.cs @@ -0,0 +1,923 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1510, CSIsNull001, CSIsNull002, IDE0300, IDE0031, IDE0100, IDE0060, CA1822 + +#nullable disable + +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; +using DSR = System.Windows.Forms.DataGridStrings; + +namespace System.Windows.Forms; + /// + /// Represents a caption in the DataGrid control. + /// + internal class DataGridCaption + { + internal EventHandlerList events; + + private const int xOffset = 3; + private const int yOffset = 1; + private const int textPadding = 2; + private const int buttonToText = 4; + private static readonly ColorMap[] colorMap = new ColorMap[] { new ColorMap() }; + + private static readonly Point minimumBounds = new Point(50, 30); + + private readonly DataGrid dataGrid; + private bool backButtonVisible; + private bool downButtonVisible; + + private SolidBrush backBrush = DefaultBackBrush; + private SolidBrush foreBrush = DefaultForeBrush; + private readonly Pen textBorderPen = DefaultTextBorderPen; + + private string text = string.Empty; + private bool textBorderVisible; + private Font textFont; + + // use the datagridFont when the textFont is not set + // we cache this font ( cause we have to make it bold every time we paint the caption ) + // + private Font dataGridFont; + + private bool backActive; + private bool downActive; + private bool backPressed; + private bool downPressed; + + // if the downButton should point down or not + private bool downButtonDown; + + private static Bitmap leftButtonBitmap; + private static Bitmap leftButtonBitmap_bidi; + private static Bitmap magnifyingGlassBitmap; + + private Rectangle backButtonRect; + private Rectangle downButtonRect; + private Rectangle textRect; + + private CaptionLocation lastMouseLocation = CaptionLocation.Nowhere; + + private EventEntry eventList; + private static readonly object EVENT_BACKWARDCLICKED = new object(); + private static readonly object EVENT_DOWNCLICKED = new object(); + private static readonly object EVENT_CAPTIONCLICKED = new object(); + + internal DataGridCaption(DataGrid dataGrid) + { + this.dataGrid = dataGrid; + downButtonVisible = dataGrid.ParentRowsVisible; + colorMap[0].OldColor = Color.White; + colorMap[0].NewColor = ForeColor; + OnGridFontChanged(); + } + + internal void OnGridFontChanged() + { + if (dataGridFont == null || !dataGridFont.Equals(dataGrid.Font)) + { + try + { + dataGridFont = new Font(dataGrid.Font, FontStyle.Bold); + } + catch + { + } + } + } + + // =------------------------------------------------------------------ + // = Properties + // =------------------------------------------------------------------ + + internal bool BackButtonActive + { + get + { + return backActive; + } + set + { + if (backActive != value) + { + backActive = value; + InvalidateCaptionRect(backButtonRect); + } + } + } + + internal bool DownButtonActive + { + get + { + return downActive; + } + set + { + if (downActive != value) + { + downActive = value; + InvalidateCaptionRect(downButtonRect); + } + } + } + + internal static SolidBrush DefaultBackBrush + { + get + { + return (SolidBrush)SystemBrushes.ActiveCaption; + } + } + + internal static Pen DefaultTextBorderPen + { + get + { + return new Pen(SystemColors.ActiveCaptionText); + } + } + + internal static SolidBrush DefaultForeBrush + { + get + { + return (SolidBrush)SystemBrushes.ActiveCaptionText; + } + } + + internal Color BackColor + { + get + { + return backBrush.Color; + } + set + { + if (!backBrush.Color.Equals(value)) + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "Caption BackColor")); + } + + backBrush = new SolidBrush(value); + Invalidate(); + } + } + } + + internal EventHandlerList Events + { + get + { + if (events == null) + { + events = new EventHandlerList(); + } + + return events; + } + } + + internal Font Font + { + get + { + // use the dataGridFont only if the user + // did not set the CaptionFont + // + if (textFont == null) + { + return dataGridFont; + } + else + { + return textFont; + } + } + set + { + if (textFont == null || !textFont.Equals(value)) + { + textFont = value; + // this property gets called in the constructor before dataGrid has a caption + // and we don't need this special-handling then... + if (dataGrid.Caption != null) + { + dataGrid.RecalculateFonts(); + dataGrid.PerformLayout(); + dataGrid.Invalidate(); // smaller invalidate rect? + } + } + } + } + + internal bool ShouldSerializeFont() + { + return textFont != null && !textFont.Equals(dataGridFont); + } + + internal bool ShouldSerializeBackColor() + { + return !backBrush.Equals(DefaultBackBrush); + } + + internal void ResetBackColor() + { + if (ShouldSerializeBackColor()) + { + backBrush = DefaultBackBrush; + Invalidate(); + } + } + + internal void ResetForeColor() + { + if (ShouldSerializeForeColor()) + { + foreBrush = DefaultForeBrush; + Invalidate(); + } + } + + internal bool ShouldSerializeForeColor() + { + return !foreBrush.Equals(DefaultForeBrush); + } + + internal void ResetFont() + { + textFont = null; + Invalidate(); + } + + internal string Text + { + get + { + return text; + } + set + { + if (value == null) + { + text = string.Empty; + } + else + { + text = value; + } + + Invalidate(); + } + } + + internal bool TextBorderVisible + { + get + { + return textBorderVisible; + } + set + { + textBorderVisible = value; + Invalidate(); + } + } + + internal Color ForeColor + { + get + { + return foreBrush.Color; + } + set + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "Caption ForeColor")); + } + + foreBrush = new SolidBrush(value); + colorMap[0].NewColor = ForeColor; + Invalidate(); + } + } + + internal Point MinimumBounds + { + get + { + return minimumBounds; + } + } + + internal bool BackButtonVisible + { + get + { + return backButtonVisible; + } + set + { + if (backButtonVisible != value) + { + backButtonVisible = value; + Invalidate(); + } + } + } + + internal bool DownButtonVisible + { + get + { + return downButtonVisible; + } + set + { + if (downButtonVisible != value) + { + downButtonVisible = value; + Invalidate(); + } + } + } + + // =------------------------------------------------------------------ + // = Methods + // =------------------------------------------------------------------ + + protected virtual void AddEventHandler(object key, Delegate handler) + { + // Locking 'this' here is ok since this is an internal class. + lock (this) + { + if (handler == null) + { + return; + } + + for (EventEntry e = eventList; e != null; e = e.next) + { + if (e.key == key) + { + e.handler = Delegate.Combine(e.handler, handler); + return; + } + } + + eventList = new EventEntry(eventList, key, handler); + } + } + + /// + /// Adds a listener for the BackwardClicked event. + /// + internal event EventHandler BackwardClicked + { + add => Events.AddHandler(EVENT_BACKWARDCLICKED, value); + remove => Events.RemoveHandler(EVENT_BACKWARDCLICKED, value); + } + + /// + /// Adds a listener for the CaptionClicked event. + /// + internal event EventHandler CaptionClicked + { + add => Events.AddHandler(EVENT_CAPTIONCLICKED, value); + remove => Events.RemoveHandler(EVENT_CAPTIONCLICKED, value); + } + + internal event EventHandler DownClicked + { + add => Events.AddHandler(EVENT_DOWNCLICKED, value); + remove => Events.RemoveHandler(EVENT_DOWNCLICKED, value); + } + + private void Invalidate() + { + if (dataGrid != null) + { + dataGrid.InvalidateCaption(); + } + } + + private void InvalidateCaptionRect(Rectangle r) + { + if (dataGrid != null) + { + dataGrid.InvalidateCaptionRect(r); + } + } + + private void InvalidateLocation(CaptionLocation loc) + { + Rectangle r; + switch (loc) + { + case CaptionLocation.BackButton: + r = backButtonRect; + r.Inflate(1, 1); + InvalidateCaptionRect(r); + break; + case CaptionLocation.DownButton: + r = downButtonRect; + r.Inflate(1, 1); + InvalidateCaptionRect(r); + break; + } + } + + protected void OnBackwardClicked(EventArgs e) + { + if (backActive) + { + ((EventHandler)Events[EVENT_BACKWARDCLICKED])?.Invoke(this, e); + } + } + + protected void OnCaptionClicked(EventArgs e) + { + ((EventHandler)Events[EVENT_CAPTIONCLICKED])?.Invoke(this, e); + } + + protected void OnDownClicked(EventArgs e) + { + if (downActive && downButtonVisible) + { + ((EventHandler)Events[EVENT_DOWNCLICKED])?.Invoke(this, e); + } + } + + private Bitmap GetBitmap(string bitmapName) + { + try + { + return ScaleHelper.GetIconResourceAsDefaultSizeBitmap(typeof(DataGridCaption), bitmapName); + } + catch (Exception e) + { + Debug.Fail("Failed to load bitmap: " + bitmapName, e.ToString()); + return null; + } + } + + private Bitmap GetBackButtonBmp(bool alignRight) + { + if (alignRight) + { + if (leftButtonBitmap_bidi == null) + { + leftButtonBitmap_bidi = GetBitmap("DataGridCaption.backarrow_bidi"); + } + + return leftButtonBitmap_bidi; + } + else + { + if (leftButtonBitmap == null) + { + leftButtonBitmap = GetBitmap("DataGridCaption.backarrow"); + } + + return leftButtonBitmap; + } + } + + private Bitmap GetDetailsBmp() + { + if (magnifyingGlassBitmap == null) + { + magnifyingGlassBitmap = GetBitmap("DataGridCaption.Details"); + } + + return magnifyingGlassBitmap; + } + + protected virtual Delegate GetEventHandler(object key) + { + // Locking 'this' here is ok since this is an internal class. + lock (this) + { + for (EventEntry e = eventList; e != null; e = e.next) + { + if (e.key == key) + { + return e.handler; + } + } + + return null; + } + } + + internal Rectangle GetBackButtonRect(Rectangle bounds, bool alignRight, int downButtonWidth) + { + Bitmap backButtonBmp = GetBackButtonBmp(false); + Size backButtonSize; + lock (backButtonBmp) + { + backButtonSize = backButtonBmp.Size; + } + + return new Rectangle(bounds.Right - xOffset * 4 - downButtonWidth - backButtonSize.Width, + bounds.Y + yOffset + textPadding, + backButtonSize.Width, + backButtonSize.Height); + } + + internal int GetDetailsButtonWidth() + { + int width = 0; + Bitmap detailsBmp = GetDetailsBmp(); + lock (detailsBmp) + { + width = detailsBmp.Size.Width; + } + + return width; + } + + internal Rectangle GetDetailsButtonRect(Rectangle bounds, bool alignRight) + { + Size downButtonSize; + Bitmap detailsBmp = GetDetailsBmp(); + lock (detailsBmp) + { + downButtonSize = detailsBmp.Size; + } + + int downButtonWidth = downButtonSize.Width; + return new Rectangle(bounds.Right - xOffset * 2 - downButtonWidth, + bounds.Y + yOffset + textPadding, + downButtonWidth, + downButtonSize.Height); + } + + /// + /// Called by the dataGrid when it needs the caption + /// to repaint. + /// + internal void Paint(Graphics g, Rectangle bounds, bool alignRight) + { + Size textSize = new Size((int)g.MeasureString(text, Font).Width + 2, Font.Height + 2); + + downButtonRect = GetDetailsButtonRect(bounds, alignRight); + int downButtonWidth = GetDetailsButtonWidth(); + backButtonRect = GetBackButtonRect(bounds, alignRight, downButtonWidth); + + int backButtonArea = backButtonVisible ? backButtonRect.Width + xOffset + buttonToText : 0; + int downButtonArea = downButtonVisible && !dataGrid.ParentRowsIsEmpty() ? downButtonWidth + xOffset + buttonToText : 0; + + int textWidthLeft = bounds.Width - xOffset - backButtonArea - downButtonArea; + + textRect = new Rectangle( + bounds.X, + bounds.Y + yOffset, + Math.Min(textWidthLeft, 2 * textPadding + textSize.Width), + 2 * textPadding + textSize.Height); + + // align the caption text box, downButton, and backButton + // if the RigthToLeft property is set to true + if (alignRight) + { + textRect.X = bounds.Right - textRect.Width; + backButtonRect.X = bounds.X + xOffset * 4 + downButtonWidth; + downButtonRect.X = bounds.X + xOffset * 2; + } + + Debug.WriteLineIf(CompModSwitches.DGCaptionPaint.TraceVerbose, "text size = " + textSize.ToString()); + Debug.WriteLineIf(CompModSwitches.DGCaptionPaint.TraceVerbose, "downButtonWidth = " + downButtonWidth.ToString(CultureInfo.InvariantCulture)); + Debug.WriteLineIf(CompModSwitches.DGCaptionPaint.TraceVerbose, "textWidthLeft = " + textWidthLeft.ToString(CultureInfo.InvariantCulture)); + Debug.WriteLineIf(CompModSwitches.DGCaptionPaint.TraceVerbose, "backButtonRect " + backButtonRect.ToString()); + Debug.WriteLineIf(CompModSwitches.DGCaptionPaint.TraceVerbose, "textRect " + textRect.ToString()); + Debug.WriteLineIf(CompModSwitches.DGCaptionPaint.TraceVerbose, "downButtonRect " + downButtonRect.ToString()); + + // we should use the code that is commented out + // with today's code, there are pixels on the backButtonRect and the downButtonRect + // that are getting painted twice + // + g.FillRectangle(backBrush, bounds); + + if (backButtonVisible) + { + PaintBackButton(g, backButtonRect, alignRight); + if (backActive) + { + if (lastMouseLocation == CaptionLocation.BackButton) + { + backButtonRect.Inflate(1, 1); + ControlPaint.DrawBorder3D(g, backButtonRect, + backPressed ? Border3DStyle.SunkenInner : Border3DStyle.RaisedInner); + } + } + } + + PaintText(g, textRect, alignRight); + + if (downButtonVisible && !dataGrid.ParentRowsIsEmpty()) + { + PaintDownButton(g, downButtonRect); + // the rules have changed, yet again. + // now: if we show the parent rows and the mouse is + // not on top of this icon, then let the icon be depressed. + // if the mouse is pressed over the icon, then show the icon pressed + // if the mouse is over the icon and not pressed, then show the icon SunkenInner; + // + if (lastMouseLocation == CaptionLocation.DownButton) + { + downButtonRect.Inflate(1, 1); + ControlPaint.DrawBorder3D(g, downButtonRect, + downPressed ? Border3DStyle.SunkenInner : Border3DStyle.RaisedInner); + } + } + } + + private void PaintIcon(Graphics g, Rectangle bounds, Bitmap b) + { + ImageAttributes attr = new ImageAttributes(); + attr.SetRemapTable(colorMap, ColorAdjustType.Bitmap); + g.DrawImage(b, bounds, 0, 0, bounds.Width, bounds.Height, GraphicsUnit.Pixel, attr); + attr.Dispose(); + } + + private void PaintBackButton(Graphics g, Rectangle bounds, bool alignRight) + { + Bitmap backButtonBmp = GetBackButtonBmp(alignRight); + lock (backButtonBmp) + { + PaintIcon(g, bounds, backButtonBmp); + } + } + + private void PaintDownButton(Graphics g, Rectangle bounds) + { + Bitmap detailsBmp = GetDetailsBmp(); + lock (detailsBmp) + { + PaintIcon(g, bounds, detailsBmp); + } + } + + private void PaintText(Graphics g, Rectangle bounds, bool alignToRight) + { + Rectangle textBounds = bounds; + + if (textBounds.Width <= 0 || textBounds.Height <= 0) + { + return; + } + + if (textBorderVisible) + { + g.DrawRectangle(textBorderPen, textBounds.X, textBounds.Y, textBounds.Width - 1, textBounds.Height - 1); + textBounds.Inflate(-1, -1); + } + + if (textPadding > 0) + { + Rectangle border = textBounds; + border.Height = textPadding; + g.FillRectangle(backBrush, border); + + border.Y = textBounds.Bottom - textPadding; + g.FillRectangle(backBrush, border); + + border = new Rectangle(textBounds.X, textBounds.Y + textPadding, + textPadding, textBounds.Height - 2 * textPadding); + g.FillRectangle(backBrush, border); + + border.X = textBounds.Right - textPadding; + g.FillRectangle(backBrush, border); + textBounds.Inflate(-textPadding, -textPadding); + } + + g.FillRectangle(backBrush, textBounds); + + // Brush foreBrush = new SolidBrush(dataGrid.CaptionForeColor); + StringFormat format = new StringFormat(); + if (alignToRight) + { + format.FormatFlags |= StringFormatFlags.DirectionRightToLeft; + format.Alignment = StringAlignment.Far; + } + + g.DrawString(text, Font, foreBrush, textBounds, format); + format.Dispose(); + // foreBrush.Dispose(); + } + + private CaptionLocation FindLocation(int x, int y) + { + if (!backButtonRect.IsEmpty) + { + if (backButtonRect.Contains(x, y)) + { + return CaptionLocation.BackButton; + } + } + + if (!downButtonRect.IsEmpty) + { + if (downButtonRect.Contains(x, y)) + { + return CaptionLocation.DownButton; + } + } + + if (!textRect.IsEmpty) + { + if (textRect.Contains(x, y)) + { + return CaptionLocation.Text; + } + } + + return CaptionLocation.Nowhere; + } + + private bool DownButtonDown + { + get + { + return downButtonDown; + } + set + { + if (downButtonDown != value) + { + downButtonDown = value; + InvalidateLocation(CaptionLocation.DownButton); + } + } + } + + internal bool GetDownButtonDirection() + { + return DownButtonDown; + } + + /// + /// Called by the dataGrid when the mouse is pressed + /// inside the caption. + /// + internal void MouseDown(int x, int y) + { + CaptionLocation loc = FindLocation(x, y); + switch (loc) + { + case CaptionLocation.BackButton: + backPressed = true; + InvalidateLocation(loc); + break; + case CaptionLocation.DownButton: + downPressed = true; + InvalidateLocation(loc); + break; + case CaptionLocation.Text: + OnCaptionClicked(EventArgs.Empty); + break; + } + } + + /// + /// Called by the dataGrid when the mouse is released + /// inside the caption. + /// + internal void MouseUp(int x, int y) + { + CaptionLocation loc = FindLocation(x, y); + switch (loc) + { + case CaptionLocation.DownButton: + if (downPressed == true) + { + downPressed = false; + OnDownClicked(EventArgs.Empty); + } + + break; + case CaptionLocation.BackButton: + if (backPressed == true) + { + backPressed = false; + OnBackwardClicked(EventArgs.Empty); + } + + break; + } + } + + /// + /// Called by the dataGrid when the mouse leaves + /// the caption area. + /// + internal void MouseLeft() + { + CaptionLocation oldLoc = lastMouseLocation; + lastMouseLocation = CaptionLocation.Nowhere; + InvalidateLocation(oldLoc); + } + + /// + /// Called by the dataGrid when the mouse is + /// inside the caption. + /// + internal void MouseOver(int x, int y) + { + CaptionLocation newLoc = FindLocation(x, y); + + InvalidateLocation(lastMouseLocation); + InvalidateLocation(newLoc); + lastMouseLocation = newLoc; + } + + protected virtual void RaiseEvent(object key, EventArgs e) + { + Delegate handler = GetEventHandler(key); + if (handler != null) + { + ((EventHandler)handler)(this, e); + } + } + + protected virtual void RemoveEventHandler(object key, Delegate handler) + { + // Locking 'this' here is ok since this is an internal class. + lock (this) + { + if (handler == null) + { + return; + } + + for (EventEntry e = eventList, prev = null; e != null; prev = e, e = e.next) + { + if (e.key == key) + { + e.handler = Delegate.Remove(e.handler, handler); + if (e.handler == null) + { + if (prev == null) + { + eventList = e.next; + } + else + { + prev.next = e.next; + } + } + + return; + } + } + } + } + + protected virtual void RemoveEventHandlers() + { + eventList = null; + } + + internal void SetDownButtonDirection(bool pointDown) + { + DownButtonDown = pointDown; + } + + /// + /// Toggles the direction the "Down Button" is pointing. + /// + internal bool ToggleDownButtonDirection() + { + DownButtonDown = !DownButtonDown; + return DownButtonDown; + } + + internal enum CaptionLocation + { + Nowhere, + BackButton, + DownButton, + Text + } + + private sealed class EventEntry + { + internal EventEntry next; + internal object key; + internal Delegate handler; + + internal EventEntry(EventEntry next, object key, Delegate handler) + { + this.next = next; + this.key = key; + this.handler = handler; + } + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridCell.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridCell.cs index cdf148139bb..bb0b052da3c 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridCell.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridCell.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable CA1066, CA1815, RS0016, IDE0251 + using System.ComponentModel; namespace System.Windows.Forms; @@ -17,21 +19,40 @@ namespace System.Windows.Forms; UrlFormat = Obsoletions.SharedUrlFormat)] [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] -[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "This type is provided for public surface compatibility with .NET Framework only. It can't run.")] public struct DataGridCell { -#pragma warning disable IDE0251 // Make member 'readonly' - applies to `set` methods. - public DataGridCell(int r, int c) => throw new PlatformNotSupportedException(); + public DataGridCell(int r, int c) + { + RowNumber = r; + ColumnNumber = c; + } public int ColumnNumber { - readonly get => throw null; - set { } + readonly get; + set; } public int RowNumber { - readonly get => throw null; - set { } + readonly get; + set; + } + + public override bool Equals(object obj) + { + if (obj is not DataGridCell rhs) + { + return false; + } + + return rhs.RowNumber == RowNumber && rhs.ColumnNumber == ColumnNumber; + } + + public override int GetHashCode() => HashCode.Combine(RowNumber, ColumnNumber); + + public override string ToString() + { + return $"DataGridCell {{RowNumber = {RowNumber}, ColumnNumber = {ColumnNumber}}}"; } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.CompModSwitches.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.CompModSwitches.cs deleted file mode 100644 index 5c87a8e3e1c..00000000000 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.CompModSwitches.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; - -namespace System.Windows.Forms; - -#nullable disable - -public partial class DataGridColumnStyle -{ - /// - /// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] - [Browsable(false)] - protected class CompModSwitches - { - public CompModSwitches() => throw new PlatformNotSupportedException(); - - public static TraceSwitch DGEditColumnEditing => throw new PlatformNotSupportedException(); - } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.DataGridColumnHeaderAccessibleObject.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.DataGridColumnHeaderAccessibleObject.cs deleted file mode 100644 index 16c7313ab19..00000000000 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.DataGridColumnHeaderAccessibleObject.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; -using System.Runtime.InteropServices; - -namespace System.Windows.Forms; - -#nullable disable - -public partial class DataGridColumnStyle -{ - /// - /// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [ComVisible(true)] - [Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] - [Browsable(false)] - protected class DataGridColumnHeaderAccessibleObject : AccessibleObject - { - public DataGridColumnHeaderAccessibleObject(DataGridColumnStyle owner) => throw new PlatformNotSupportedException(); - - public DataGridColumnHeaderAccessibleObject() => throw new PlatformNotSupportedException(); - - protected DataGridColumnStyle Owner => throw null; - } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.cs index 4fdfb9d1b48..408edd07ee6 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridColumnStyle.cs @@ -1,227 +1,784 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; -using System.Drawing; -using System.Drawing.Design; - -namespace System.Windows.Forms; +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1510, CSIsNull001, CSIsNull002, IDE0059, IDE0270, IDE0078, RS0016, IDE0066 #nullable disable -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[ToolboxItem(false)] -[DesignTimeVisible(false)] -[DefaultProperty("Header")] -public abstract partial class DataGridColumnStyle : Component, IDataGridColumnStyleEditingNotificationService -{ - public DataGridColumnStyle() => throw new PlatformNotSupportedException(); - - public DataGridColumnStyle(PropertyDescriptor prop) => throw new PlatformNotSupportedException(); - - [Localizable(true)] - [DefaultValue(HorizontalAlignment.Left)] - public virtual HorizontalAlignment Alignment - { - get => throw null; - set { } - } - - public event EventHandler AlignmentChanged - { - add { } - remove { } - } - - [Browsable(false)] - public AccessibleObject HeaderAccessibleObject => throw null; - - [DefaultValue(null)] - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Advanced)] - public virtual PropertyDescriptor PropertyDescriptor - { - get => throw null; - set { } - } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Advanced)] - public event EventHandler PropertyDescriptorChanged - { - add { } - remove { } - } - - [Browsable(false)] - public virtual DataGridTableStyle DataGridTableStyle => throw null; - - protected int FontHeight => throw null; - - public event EventHandler FontChanged - { - add { } - remove { } - } - - [Localizable(true)] - public virtual string HeaderText - { - get => throw null; - set { } - } - - public event EventHandler HeaderTextChanged - { - add { } - remove { } - } - - [Editor($"System.Windows.Forms.Design.DataGridColumnStyleMappingNameEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))] - [Localizable(true)] - [DefaultValue("")] - public string MappingName - { - get => throw null; - set { } - } - - public event EventHandler MappingNameChanged - { - add { } - remove { } - } - - [Localizable(true)] - public virtual string NullText - { - get => throw null; - set { } - } - - public event EventHandler NullTextChanged - { - add { } - remove { } - } - - [DefaultValue(false)] - public virtual bool ReadOnly - { - get => throw null; - set { } - } - - public event EventHandler ReadOnlyChanged - { - add { } - remove { } - } - - [Localizable(true)] - [DefaultValue(100)] - public virtual int Width - { - get => throw null; - set { } - } +using System.Collections; +using System.ComponentModel; +using System.Drawing; +using System.Runtime.InteropServices; +using DSR = System.Windows.Forms.DataGridStrings; - public event EventHandler WidthChanged +namespace System.Windows.Forms; + /// + /// Specifies the appearance and text formatting and behavior of a control column. + /// + [ToolboxItem(false)] + [DesignTimeVisible(false)] + [DefaultProperty(nameof(HeaderText))] + public abstract class DataGridColumnStyle : Component, IDataGridColumnStyleEditingNotificationService { - add { } - remove { } + private HorizontalAlignment _alignment = HorizontalAlignment.Left; + private PropertyDescriptor _propertyDescriptor; + private DataGridTableStyle _dataGridTableStyle; + private string _mappingName = string.Empty; + private string _headerName = string.Empty; + private bool _invalid; + private string _nullText = DSR.DataGridNullText; + private bool _readOnly; + private bool _updating; + internal int _width = -1; + private AccessibleObject _headerAccessibleObject; + + private static readonly object s_alignmentEvent = new object(); + private static readonly object s_propertyDescriptorEvent = new object(); + private static readonly object s_headerTextEvent = new object(); + private static readonly object s_mappingNameEvent = new object(); + private static readonly object s_nullTextEvent = new object(); + private static readonly object s_readOnlyEvent = new object(); + private static readonly object s_widthEvent = new object(); + + /// + /// In a derived class, initializes a new instance of the + /// class. + /// + public DataGridColumnStyle() + { + } + + /// + /// Initializes a new instance of the + /// class with the specified . + /// + public DataGridColumnStyle(PropertyDescriptor prop) : this() + { + PropertyDescriptor = prop; + if (prop != null) + { + _readOnly = prop.IsReadOnly; + } + } + + private protected DataGridColumnStyle(PropertyDescriptor prop, bool isDefault) : this(prop) + { +#if DEBUG + IsDefault = isDefault; +#endif + if (isDefault && prop != null) + { + // take the header name from the property name + _headerName = prop.Name; + _mappingName = prop.Name; + } + } + +#if DEBUG + internal bool IsDefault { get; } +#endif + + /// + /// Gets or sets the alignment of text in a column. + /// + [SRCategory(nameof(SR.CatDisplay))] + [Localizable(true)] + [DefaultValue(HorizontalAlignment.Left)] + public virtual HorizontalAlignment Alignment + { + get => _alignment; + set + { + SourceGenerated.EnumValidator.Validate(value); + + if (_alignment != value) + { + _alignment = value; + OnAlignmentChanged(EventArgs.Empty); + Invalidate(); + } + } + } + + public event EventHandler AlignmentChanged + { + add => Events.AddHandler(s_alignmentEvent, value); + remove => Events.RemoveHandler(s_alignmentEvent, value); + } + + /// + /// When overridden in a derived class, updates the value of a specified row with + /// the given text. + /// + protected internal virtual void UpdateUI(CurrencyManager source, int rowNum, string displayText) + { + } + + /// + /// Gets or sets the background color of the column. + /// + [Browsable(false)] + public AccessibleObject HeaderAccessibleObject + { + get => _headerAccessibleObject ?? (_headerAccessibleObject = CreateHeaderAccessibleObject()); + } + + /// + /// Gets or sets the that determines the + /// attributes of data displayed by the . + /// + [DefaultValue(null)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Advanced)] + public virtual PropertyDescriptor PropertyDescriptor + { + get => _propertyDescriptor; + set + { + if (_propertyDescriptor != value) + { + _propertyDescriptor = value; + OnPropertyDescriptorChanged(EventArgs.Empty); + } + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)] + public event EventHandler PropertyDescriptorChanged + { + add => Events.AddHandler(s_propertyDescriptorEvent, value); + remove => Events.RemoveHandler(s_propertyDescriptorEvent, value); + } + + protected virtual AccessibleObject CreateHeaderAccessibleObject() + { + return new DataGridColumnHeaderAccessibleObject(this); + } + + /// + /// When overridden in a derived class, sets the + /// control that this column belongs to. + /// + protected virtual void SetDataGrid(DataGrid value) + { + SetDataGridInColumn(value); + } + + /// + /// When overridden in a derived class, sets the + /// for the column. + /// + protected virtual void SetDataGridInColumn(DataGrid value) + { + // we need to set up the PropertyDescriptor + if (PropertyDescriptor == null && value != null) + { + CurrencyManager lm = value.ListManager; + if (lm == null) + { + return; + } + + PropertyDescriptorCollection propCollection = lm.GetItemProperties(); + int propCount = propCollection.Count; + for (int i = 0; i < propCollection.Count; i++) + { + PropertyDescriptor prop = propCollection[i]; + if (!typeof(IList).IsAssignableFrom(prop.PropertyType) && prop.Name.Equals(HeaderText)) + { + PropertyDescriptor = prop; + return; + } + } + } + } + + internal void SetDataGridInternalInColumn(DataGrid value) + { + if (value == null || value.Initializing) + { + return; + } + + SetDataGridInColumn(value); + } + + /// + /// Gets the System.Windows.Forms.DataGridTableStyle for the column. + /// + [Browsable(false)] + public virtual DataGridTableStyle DataGridTableStyle => _dataGridTableStyle; + + internal void SetDataGridTableInColumn(DataGridTableStyle value, bool force) + { + if (_dataGridTableStyle != null && _dataGridTableStyle.Equals(value) && !force) + { + return; + } + + if (value != null && value.DataGrid != null && !value.DataGrid.Initializing) + { + SetDataGridInColumn(value.DataGrid); + } + + _dataGridTableStyle = value; + } + + /// + /// Gets the height of the column's font. + /// + protected int FontHeight + { + get => DataGridTableStyle?.DataGrid?.FontHeight ?? DataGridTableStyle.defaultFontHeight; + } + + public event EventHandler FontChanged + { + add { } + remove { } + } + + /// + /// Gets or sets the text of the column header. + /// + [Localizable(true)] + [SRCategory(nameof(SR.CatDisplay))] + public virtual string HeaderText + { + get => _headerName; + set + { + if (value == null) + { + value = string.Empty; + } + + if (!_headerName.Equals(value)) + { + _headerName = value; + OnHeaderTextChanged(EventArgs.Empty); + // we only invalidate columns that are visible ( ie, their propertyDescriptor is not null) + if (PropertyDescriptor != null) + { + Invalidate(); + } + } + } + } + + public event EventHandler HeaderTextChanged + { + add => Events.AddHandler(s_headerTextEvent, value); + remove => Events.RemoveHandler(s_headerTextEvent, value); + } + + [Editor("System.Windows.Forms.Design.DataGridColumnStyleMappingNameEditor, " + Assemblies.SystemDesign, typeof(Drawing.Design.UITypeEditor))] + [Localizable(true)] + [DefaultValue("")] + public string MappingName + { + get => _mappingName; + set + { + if (value == null) + { + value = string.Empty; + } + + if (!_mappingName.Equals(value)) + { + string originalMappingName = _mappingName; + _mappingName = value; + try + { + _dataGridTableStyle?.GridColumnStyles.CheckForMappingNameDuplicates(this); + } + catch + { + _mappingName = originalMappingName; + throw; + } + + OnMappingNameChanged(EventArgs.Empty); + } + } + } + + public event EventHandler MappingNameChanged + { + add => Events.AddHandler(s_mappingNameEvent, value); + remove => Events.RemoveHandler(s_mappingNameEvent, value); + } + + /// + /// Indicates whether the System.Windows.Forms.DataGridColumnStyle.HeaderText property + /// should be persisted. + /// + private bool ShouldSerializeHeaderText() + { + return (_headerName.Length != 0); + } + + /// + /// Resets the System.Windows.Forms.DataGridColumnStyle.HeaderText to its default value. + /// + public void ResetHeaderText() => HeaderText = string.Empty; + + /// + /// Gets or sets the text that is displayed when the column contains a null + /// value. + /// + [Localizable(true)] + [SRCategory(nameof(SR.CatDisplay))] + public virtual string NullText + { + get => _nullText; + set + { + if (_nullText != value) + { + _nullText = value; + OnNullTextChanged(EventArgs.Empty); + Invalidate(); + } + } + } + + public event EventHandler NullTextChanged + { + add => Events.AddHandler(s_nullTextEvent, value); + remove => Events.RemoveHandler(s_nullTextEvent, value); + } + + /// + /// Gets or sets a value indicating whether the data in the column cannot be edited. + /// + [DefaultValue(false)] + public virtual bool ReadOnly + { + get => _readOnly; + set + { + if (_readOnly != value) + { + _readOnly = value; + OnReadOnlyChanged(EventArgs.Empty); + } + } + } + + public event EventHandler ReadOnlyChanged + { + add => Events.AddHandler(s_readOnlyEvent, value); + remove => Events.RemoveHandler(s_readOnlyEvent, value); + } + + /// + /// Gets or sets the width of the column. + /// + [SRCategory(nameof(SR.CatLayout))] + [Localizable(true)] + [DefaultValue(100)] + public virtual int Width + { + get => _width; + set + { + if (_width != value) + { + _width = value; + DataGrid grid = DataGridTableStyle?.DataGrid; + if (grid != null) + { + // rearrange the scroll bars + grid.PerformLayout(); + + // force the grid to repaint + grid.InvalidateInside(); + } + + OnWidthChanged(EventArgs.Empty); + } + } + } + + public event EventHandler WidthChanged + { + add => Events.AddHandler(s_widthEvent, value); + remove => Events.RemoveHandler(s_widthEvent, value); + } + + /// + /// Suspends the painting of the column until the + /// method is called. + /// + protected void BeginUpdate() => _updating = true; + + /// + /// Resumes the painting of columns suspended by calling the + /// method. + /// + protected void EndUpdate() + { + _updating = false; + if (_invalid) + { + _invalid = false; + Invalidate(); + } + } + + internal virtual string GetDisplayText(object value) => value.ToString(); + + private void ResetNullText() => NullText = DSR.DataGridNullText; + + private bool ShouldSerializeNullText() => !DSR.DataGridNullText.Equals(_nullText); + + /// + /// When overridden in a derived class, gets the optimum width and height of the + /// specified value. + /// + protected internal abstract Size GetPreferredSize(Graphics g, object value); + + /// + /// Gets the minimum height of a row. + /// + protected internal abstract int GetMinimumHeight(); + + /// + /// When overridden in a derived class, gets the height to be used in for + /// automatically resizing columns. + /// + protected internal abstract int GetPreferredHeight(Graphics g, object value); + + /// + /// Gets the value in the specified row from the specified System.Windows.Forms.ListManager. + /// + protected internal virtual object GetColumnValueAtRow(CurrencyManager source, int rowNum) + { + CheckValidDataSource(source); + PropertyDescriptor descriptor = PropertyDescriptor; + if (descriptor == null) + { + throw new InvalidOperationException(DSR.DataGridColumnNoPropertyDescriptor); + } + + return descriptor.GetValue(source[rowNum]); + } + + /// + /// Redraws the column and causes a paint message to be sent to the control. + /// + protected virtual void Invalidate() + { + if (_updating) + { + _invalid = true; + return; + } + + DataGridTableStyle?.InvalidateColumn(this); + } + + /// + /// Checks if the specified DataView is valid. + /// + protected void CheckValidDataSource(CurrencyManager value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + // The code may delete a gridColumn that was editing. + // In that case, we still have to push the value into the backend + // and we only need the propertyDescriptor to push the value. + // (take a look at gridEditAndDeleteEditColumn) + if (PropertyDescriptor == null) + { + throw new InvalidOperationException(string.Format(DSR.DataGridColumnUnbound, HeaderText)); + } + } + + /// + /// When overridden in a derived class, initiates a request to interrrupt an edit + /// procedure. + /// + protected internal abstract void Abort(int rowNum); + + /// + /// When overridden in a derived class, inititates a request to complete an + /// editing procedure. + /// + protected internal abstract bool Commit(CurrencyManager dataSource, int rowNum); + + /// + /// When overridden in a deriving class, prepares a cell for editing. + /// + protected internal virtual void Edit(CurrencyManager source, int rowNum, Rectangle bounds, bool readOnly) + { + Edit(source, rowNum, bounds, readOnly, null, true); + } + + /// + /// Prepares the cell for editing, passing the specified , + /// row number, , argument indicating whether + /// the column is read-only, and the text to display in the new control. + /// + protected internal virtual void Edit(CurrencyManager source, int rowNum, Rectangle bounds, bool readOnly, string displayText) + { + Edit(source, rowNum, bounds, readOnly, displayText, true); + } + + /// + /// When overridden in a deriving class, prepares a cell for editing. + /// + protected internal abstract void Edit(CurrencyManager source, int rowNum, Rectangle bounds, bool readOnly, string displayText, bool cellIsVisible); + + /// + /// Indicates whether the a mouse down event occurred at the specified row, at + /// the specified x and y coordinates. + /// + internal virtual bool MouseDown(int rowNum, int x, int y) + { + return false; + } + + /// + /// When overriden in a derived class, enters a + /// into the column. + /// + protected internal virtual void EnterNullValue() + { + } + + /// + /// Provides a handler for determining which key was pressed, and whether to + /// process it. + /// + internal virtual bool KeyPress(int rowNum, Keys keyData) + { + // if this is read only then do not do anything + if (ReadOnly || (DataGridTableStyle != null && DataGridTableStyle.DataGrid != null && DataGridTableStyle.DataGrid.ReadOnly)) + { + return false; + } + + if (keyData == (Keys.Control | Keys.NumPad0) || keyData == (Keys.Control | Keys.D0)) + { + EnterNullValue(); + return true; + } + + return false; + } + + /// + /// When overridden in a derived class, directs the column to concede focus with + /// an appropriate action. + /// + protected internal virtual void ConcedeFocus() + { + } + + /// + /// Paints the a with the specified + /// , , + /// System.Windows.Forms.CurrencyManager, and row number. + /// + protected internal abstract void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum); + + /// + /// When overridden in a derived class, paints a + /// with the specified , , + /// see Rectangle, row number, and alignment. + /// + protected internal abstract void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, bool alignToRight); + + /// + /// Paints a with the specified , , see System.Data.DataView, row number, background color, foreground color, and alignment. + /// + protected internal virtual void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, + Brush backBrush, Brush foreBrush, bool alignToRight) + { + Paint(g, bounds, source, rowNum, alignToRight); + } + + private void OnPropertyDescriptorChanged(EventArgs e) + { + EventHandler eh = Events[s_propertyDescriptorEvent] as EventHandler; + eh?.Invoke(this, e); + } + + private void OnAlignmentChanged(EventArgs e) + { + EventHandler eh = Events[s_alignmentEvent] as EventHandler; + eh?.Invoke(this, e); + } + + private void OnHeaderTextChanged(EventArgs e) + { + EventHandler eh = Events[s_headerTextEvent] as EventHandler; + eh?.Invoke(this, e); + } + + private void OnMappingNameChanged(EventArgs e) + { + EventHandler eh = Events[s_mappingNameEvent] as EventHandler; + eh?.Invoke(this, e); + } + + private void OnReadOnlyChanged(EventArgs e) + { + EventHandler eh = Events[s_readOnlyEvent] as EventHandler; + eh?.Invoke(this, e); + } + + private void OnNullTextChanged(EventArgs e) + { + EventHandler eh = Events[s_nullTextEvent] as EventHandler; + eh?.Invoke(this, e); + } + + private void OnWidthChanged(EventArgs e) + { + EventHandler eh = Events[s_widthEvent] as EventHandler; + eh?.Invoke(this, e); + } + + /// + /// Sets the value in a specified row with the value from a specified see DataView. + /// + protected internal virtual void SetColumnValueAtRow(CurrencyManager source, int rowNum, object value) + { + CheckValidDataSource(source); + PropertyDescriptor descriptor = PropertyDescriptor; + if (descriptor == null) + { + throw new InvalidOperationException(DSR.DataGridColumnNoPropertyDescriptor); + } + + if (source.Position != rowNum) + { + throw new ArgumentException(DSR.DataGridColumnListManagerPosition, nameof(rowNum)); + } + + if (source[rowNum] is IEditableObject editableObject) + { + editableObject.BeginEdit(); + } + + descriptor.SetValue(source[rowNum], value); + } + + protected internal virtual void ColumnStartedEditing(Control editingControl) + { + DataGridTableStyle?.DataGrid?.ColumnStartedEditing(editingControl); + } + + void IDataGridColumnStyleEditingNotificationService.ColumnStartedEditing(Control editingControl) + { + ColumnStartedEditing(editingControl); + } + + protected internal virtual void ReleaseHostedControl() + { + } + + protected class CompModSwitches + { + private static TraceSwitch dgEditColumnEditing; + + public static TraceSwitch DGEditColumnEditing + { + get + { + if (dgEditColumnEditing == null) + { + dgEditColumnEditing = new TraceSwitch("DGEditColumnEditing", "Editing related tracing"); + } + + return dgEditColumnEditing; + } + } + } + + [ComVisible(true)] + protected class DataGridColumnHeaderAccessibleObject : AccessibleObject + { + public DataGridColumnHeaderAccessibleObject(DataGridColumnStyle owner) : this() + { + Debug.Assert(owner != null, "DataGridColumnHeaderAccessibleObject must have a valid owner DataGridColumn"); + Owner = owner; + } + + public DataGridColumnHeaderAccessibleObject() : base() + { + } + + public override Rectangle Bounds + { + get + { + // we need to get the width and the X coordinate of this column on the screen + // we can't just cache this, cause the column may be moved in the collection + if (Owner.PropertyDescriptor == null) + { + return Rectangle.Empty; + } + + DataGrid dg = DataGrid; + if (dg.DataGridRowsLength == 0) + { + return Rectangle.Empty; + } + + // We need to find this column's offset in the gridColumnCollection. + GridColumnStylesCollection cols = Owner._dataGridTableStyle.GridColumnStyles; + int offset = -1; + for (int i = 0; i < cols.Count; i++) + { + if (cols[i] == Owner) + { + offset = i; + break; + } + } + + Debug.Assert(offset >= 0, "this column must be in a collection, otherwise its bounds are useless"); + Rectangle rect = dg.GetCellBounds(0, offset); + // Now add the Y coordinate of the datagrid.Layout.Data. + // It should be the same as dataGrid.Layout.ColumnHeaders + rect.Y = dg.GetColumnHeadersRect().Y; + return dg.RectangleToScreen(rect); + } + } + + public override string Name => Owner._headerName; + + protected DataGridColumnStyle Owner { get; } + + public override AccessibleObject Parent => DataGrid.AccessibilityObject; + + private DataGrid DataGrid => Owner._dataGridTableStyle.dataGrid; + + public override AccessibleRole Role => AccessibleRole.ColumnHeader; + + public override AccessibleObject Navigate(AccessibleNavigation navdir) + { + switch (navdir) + { + case AccessibleNavigation.Right: + case AccessibleNavigation.Next: + case AccessibleNavigation.Down: + return Parent.GetChild(1 + Owner._dataGridTableStyle.GridColumnStyles.IndexOf(Owner) + 1); + case AccessibleNavigation.Up: + case AccessibleNavigation.Left: + case AccessibleNavigation.Previous: + return Parent.GetChild(1 + Owner._dataGridTableStyle.GridColumnStyles.IndexOf(Owner) - 1); + } + + return null; + } + } } - - protected virtual AccessibleObject CreateHeaderAccessibleObject() => throw null; - - protected virtual void SetDataGrid(DataGrid value) { } - - protected virtual void SetDataGridInColumn(DataGrid value) { } - - protected void BeginUpdate() { } - - protected void EndUpdate() { } - - protected internal abstract Size GetPreferredSize(Graphics g, object value); - - protected internal abstract int GetMinimumHeight(); - - protected internal abstract int GetPreferredHeight(Graphics g, object value); - - protected internal virtual object GetColumnValueAtRow(CurrencyManager source, int rowNum) => throw null; - - protected virtual void Invalidate() { } - - protected void CheckValidDataSource(CurrencyManager value) { } - - protected internal abstract void Abort(int rowNum); - - protected internal abstract bool Commit(CurrencyManager dataSource, int rowNum); - - protected internal virtual void Edit( - CurrencyManager source, - int rowNum, - Rectangle bounds, - bool readOnly) - { } - - protected internal virtual void Edit( - CurrencyManager source, - int rowNum, - Rectangle bounds, - bool readOnly, - string displayText) - { } - - protected internal abstract void Edit( - CurrencyManager source, - int rowNum, - Rectangle bounds, - bool readOnly, - string displayText, - bool cellIsVisible); - - protected internal virtual void EnterNullValue() { } - - protected internal virtual void ConcedeFocus() { } - - protected internal abstract void Paint( - Graphics g, - Rectangle bounds, - CurrencyManager source, - int rowNum, - bool alignToRight); - - protected internal virtual void Paint( - Graphics g, - Rectangle bounds, - CurrencyManager source, - int rowNum, - Brush backBrush, - Brush foreBrush, - bool alignToRight) - { } - - protected internal abstract void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum); - - protected internal virtual void ReleaseHostedControl() { } - - public void ResetHeaderText() { } - - protected internal virtual void SetColumnValueAtRow(CurrencyManager source, int rowNum, object value) { } - - void IDataGridColumnStyleEditingNotificationService.ColumnStartedEditing(Control editingControl) { } - - protected internal virtual void ColumnStartedEditing(Control editingControl) { } - - protected internal virtual void UpdateUI(CurrencyManager source, int rowNum, string displayText) { } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridLineStyle.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridLineStyle.cs index 97fc0aa249f..2e97ef3e786 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridLineStyle.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridLineStyle.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable WFDEV006 + using System.ComponentModel; namespace System.Windows.Forms; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridParentRows.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridParentRows.cs new file mode 100644 index 00000000000..b8b6ea9f68f --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridParentRows.cs @@ -0,0 +1,1409 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1510, CSIsNull001, CSIsNull002, WFDEV006, IDE0300, IDE0031, IDE0059, IDE0078, IDE0060, CA1822 + +#nullable disable + +using System.Collections; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Text; +using DSR = System.Windows.Forms.DataGridStrings; + +namespace System.Windows.Forms; + internal class DataGridParentRows + { + // siting + // + private readonly DataGrid dataGrid; + + // ui + // + // private Color backColor = DataGrid.defaultParentRowsBackColor; + // private Color foreColor = DataGrid.defaultParentRowsForeColor; + + private SolidBrush backBrush = DataGrid.DefaultParentRowsBackBrush; + private SolidBrush foreBrush = DataGrid.DefaultParentRowsForeBrush; + + private readonly int borderWidth = 1; + // private Color borderColor = SystemColors.WindowFrame; + private Brush borderBrush = new SolidBrush(SystemColors.WindowFrame); + + private static Bitmap rightArrow; + private static Bitmap leftArrow; + + private readonly ColorMap[] colorMap = new ColorMap[] { new ColorMap() }; + + // private bool gridLineDots = false; + // private Color gridLineColor = SystemColors.Control; + // private Brush gridLineBrush = SystemBrushes.Control; + private readonly Pen gridLinePen = SystemPens.Control; + + private int totalHeight; + private int textRegionHeight; + + // now that we have left and right arrows, we also have layout + private readonly Layout layout = new Layout(); + + // mouse info + // + // private bool overLeftArrow = false; + // private bool overRightArrow = false; + private bool downLeftArrow; + private bool downRightArrow; + + // a horizOffset of 0 means that the layout for the parent + // rows is left aligned. + // a horizOffset of 1 means that the leftmost unit of information ( let it be a + // table name, a column name or a column value ) is not visible. + // a horizOffset of 2 means that the leftmost 2 units of information are not visible, and so on + // + private int horizOffset; + + // storage for parent row states + // + private readonly ArrayList parents = new ArrayList(); + private int parentsCount; + private readonly ArrayList rowHeights = new ArrayList(); + AccessibleObject accessibleObject; + + internal DataGridParentRows(DataGrid dataGrid) + { + colorMap[0].OldColor = Color.Black; + this.dataGrid = dataGrid; + // UpdateGridLinePen(); + } + + // =------------------------------------------------------------------ + // = Properties + // =------------------------------------------------------------------ + + public AccessibleObject AccessibleObject + { + get + { + if (accessibleObject is null) + { + accessibleObject = new DataGridParentRowsAccessibleObject(this); + } + + return accessibleObject; + } + } + + internal Color BackColor + { + get + { + return backBrush.Color; + } + set + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "Parent Rows BackColor")); + } + + if (value != backBrush.Color) + { + backBrush = new SolidBrush(value); + Invalidate(); + } + } + } + + internal SolidBrush BackBrush + { + get + { + return backBrush; + } + set + { + if (value != backBrush) + { + CheckNull(value, "BackBrush"); + backBrush = value; + Invalidate(); + } + } + } + + internal SolidBrush ForeBrush + { + get + { + return foreBrush; + } + set + { + if (value != foreBrush) + { + CheckNull(value, "BackBrush"); + foreBrush = value; + Invalidate(); + } + } + } + + // since the layout of the parentRows is computed on every paint message, + // we can actually return true ClientRectangle coordinates + internal Rectangle GetBoundsForDataGridStateAccesibility(DataGridState dgs) + { + Rectangle ret = Rectangle.Empty; + int rectY = 0; + for (int i = 0; i < parentsCount; i++) + { + int height = (int)rowHeights[i]; + if (parents[i] == dgs) + { + ret.X = layout.leftArrow.IsEmpty ? layout.data.X : layout.leftArrow.Right; + ret.Height = height; + ret.Y = rectY; + ret.Width = layout.data.Width; + return ret; + } + + rectY += height; + } + + return ret; + } + + /* + internal Color BorderColor { + get { + return borderColor; + } + set { + if (value != borderColor) { + borderColor = value; + Invalidate(); + } + } + } + */ + + internal Brush BorderBrush + { + get + { + return borderBrush; + } + set + { + if (value != borderBrush) + { + borderBrush = value; + Invalidate(); + } + } + } + + /* + internal Brush GridLineBrush { + get { + return gridLineBrush; + } + set { + if (value != gridLineBrush) { + gridLineBrush = value; + UpdateGridLinePen(); + Invalidate(); + } + } + } + + internal bool GridLineDots { + get { + return gridLineDots; + } + set { + if (gridLineDots != value) { + gridLineDots = value; + UpdateGridLinePen(); + Invalidate(); + } + } + } + */ + + internal int Height + { + get + { + return totalHeight; + } + } + + internal Color ForeColor + { + get + { + return foreBrush.Color; + } + set + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, "Parent Rows ForeColor")); + } + + if (value != foreBrush.Color) + { + foreBrush = new SolidBrush(value); + Invalidate(); + } + } + } + + internal bool Visible + { + get + { + return dataGrid.ParentRowsVisible; + } + set + { + dataGrid.ParentRowsVisible = value; + } + } + + // =------------------------------------------------------------------ + // = Methods + // =------------------------------------------------------------------ + + /// + /// Adds a DataGridState object to the top of the list of parents. + /// + internal void AddParent(DataGridState dgs) + { + CurrencyManager childDataSource = (CurrencyManager)dataGrid.BindingContext[dgs.DataSource, dgs.DataMember]; + parents.Add(dgs); + SetParentCount(parentsCount + 1); + Debug.Assert(GetTopParent() is not null, "we should have a parent at least"); + } + + internal void Clear() + { + for (int i = 0; i < parents.Count; i++) + { + DataGridState dgs = parents[i] as DataGridState; + dgs.RemoveChangeNotification(); + } + + parents.Clear(); + rowHeights.Clear(); + totalHeight = 0; + SetParentCount(0); + } + + internal void SetParentCount(int count) + { + parentsCount = count; + dataGrid.Caption.BackButtonVisible = (parentsCount > 0) && (dataGrid.AllowNavigation); + } + + internal void CheckNull(object value, string propName) + { + if (value is null) + { + throw new ArgumentNullException(nameof(propName)); + } + } + + internal void Dispose() + { + gridLinePen.Dispose(); + } + + /// + /// Retrieves the top most parent in the list of parents. + /// + internal DataGridState GetTopParent() + { + if (parentsCount < 1) + { + return null; + } + + return (DataGridState)(((ICloneable)(parents[parentsCount - 1])).Clone()); + } + + /// + /// Determines if there are any parent rows contained in this object. + /// + internal bool IsEmpty() + { + return parentsCount == 0; + } + + /// + /// Similar to GetTopParent() but also removes it. + /// + internal DataGridState PopTop() + { + if (parentsCount < 1) + { + return null; + } + + SetParentCount(parentsCount - 1); + DataGridState ret = (DataGridState)parents[parentsCount]; + ret.RemoveChangeNotification(); + parents.RemoveAt(parentsCount); + return ret; + } + + internal void Invalidate() + { + if (dataGrid is not null) + { + dataGrid.InvalidateParentRows(); + } + } + + internal void InvalidateRect(Rectangle rect) + { + if (dataGrid is not null) + { + Rectangle r = new Rectangle(rect.X, rect.Y, rect.Width + borderWidth, rect.Height + borderWidth); + dataGrid.InvalidateParentRowsRect(r); + } + } + + // called from DataGrid::OnLayout + internal void OnLayout() + { + if (parentsCount == rowHeights.Count) + { + return; + } + + int height = 0; + if (totalHeight == 0) + { + totalHeight += 2 * borderWidth; + } + + // figure out how tall each row's text will be + // + textRegionHeight = (int)dataGrid.Font.Height + 2; + + // make the height of the Column.Font count for the height + // of the parentRows; + // + // if the user wants to programatically + // navigate to a relation in the constructor of the form + // ( ie, when the form does not process PerformLayout ) + // the grid will receive an OnLayout message when there is more + // than one parent in the grid + if (parentsCount > rowHeights.Count) + { + Debug.Assert(parentsCount == rowHeights.Count + 1 || rowHeights.Count == 0, "see comment above for more info"); + int rowHeightsCount = rowHeights.Count; + for (int i = rowHeightsCount; i < parentsCount; i++) + { + DataGridState dgs = (DataGridState)parents[i]; + GridColumnStylesCollection cols = dgs.GridColumnStyles; + + int colsHeight = 0; + + for (int j = 0; j < cols.Count; j++) + { + colsHeight = Math.Max(colsHeight, cols[j].GetMinimumHeight()); + } + + height = Math.Max(colsHeight, textRegionHeight); + + // the height of the bottom border + height++; + rowHeights.Add(height); + + totalHeight += height; + } + } + else + { + Debug.Assert(parentsCount == rowHeights.Count - 1, "we do layout only for push/popTop"); + if (parentsCount == 0) + { + totalHeight = 0; + } + else + { + totalHeight -= (int)rowHeights[rowHeights.Count - 1]; + } + + rowHeights.RemoveAt(rowHeights.Count - 1); + } + } + + private int CellCount() + { + int cellCount = 0; + cellCount = ColsCount(); + + if (dataGrid.ParentRowsLabelStyle == DataGridParentRowsLabelStyle.TableName || + dataGrid.ParentRowsLabelStyle == DataGridParentRowsLabelStyle.Both) + { + cellCount++; + } + + return cellCount; + } + + private void ResetMouseInfo() + { + // overLeftArrow = false; + // overRightArrow = false; + downLeftArrow = false; + downRightArrow = false; + } + + private void LeftArrowClick(int cellCount) + { + if (horizOffset > 0) + { + ResetMouseInfo(); + horizOffset -= 1; + Invalidate(); + } + else + { + ResetMouseInfo(); + InvalidateRect(layout.leftArrow); + } + } + + private void RightArrowClick(int cellCount) + { + if (horizOffset < cellCount - 1) + { + ResetMouseInfo(); + horizOffset += 1; + Invalidate(); + } + else + { + ResetMouseInfo(); + InvalidateRect(layout.rightArrow); + } + } + + // the only mouse clicks that are handled are + // the mouse clicks on the LeftArrow and RightArrow + // + internal void OnMouseDown(int x, int y, bool alignToRight) + { + if (layout.rightArrow.IsEmpty) + { + Debug.Assert(layout.leftArrow.IsEmpty, "we can't have the leftArrow w/o the rightArrow"); + return; + } + + int cellCount = CellCount(); + + if (layout.rightArrow.Contains(x, y)) + { + // draw a nice sunken border around the right arrow area + // we want to keep a cell on the screen + + downRightArrow = true; + + if (alignToRight) + { + LeftArrowClick(cellCount); + } + else + { + RightArrowClick(cellCount); + } + } + else if (layout.leftArrow.Contains(x, y)) + { + downLeftArrow = true; + + if (alignToRight) + { + RightArrowClick(cellCount); + } + else + { + LeftArrowClick(cellCount); + } + } + else + { + if (downLeftArrow) + { + downLeftArrow = false; + InvalidateRect(layout.leftArrow); + } + + if (downRightArrow) + { + downRightArrow = false; + InvalidateRect(layout.rightArrow); + } + } + } + + internal void OnMouseLeave() + { + if (downLeftArrow) + { + downLeftArrow = false; + InvalidateRect(layout.leftArrow); + } + + if (downRightArrow) + { + downRightArrow = false; + InvalidateRect(layout.rightArrow); + } + } + + internal void OnMouseMove(int x, int y) + { + /* + if (!layout.leftArrow.IsEmpty && layout.leftArrow.Contains(x,y)) + { + ResetMouseInfo(); + overLeftArrow = true; + InvalidateRect(layout.leftArrow); + return; + } + if (!layout.rightArrow.IsEmpty && layout.rightArrow.Contains(x,y)) + { + ResetMouseInfo(); + overRightArrow = true; + InvalidateRect(layout.rightArrow); + return; + } + */ + + if (downLeftArrow) + { + downLeftArrow = false; + InvalidateRect(layout.leftArrow); + } + + if (downRightArrow) + { + downRightArrow = false; + InvalidateRect(layout.rightArrow); + } + } + + internal void OnMouseUp(int x, int y) + { + ResetMouseInfo(); + if (!layout.rightArrow.IsEmpty && layout.rightArrow.Contains(x, y)) + { + InvalidateRect(layout.rightArrow); + return; + } + + if (!layout.leftArrow.IsEmpty && layout.leftArrow.Contains(x, y)) + { + InvalidateRect(layout.leftArrow); + return; + } + } + + internal void OnResize(Rectangle oldBounds) + { + Invalidate(); + } + + /// + /// Paints the parent rows + /// + internal void Paint(Graphics g, Rectangle visualbounds, bool alignRight) + { + Rectangle bounds = visualbounds; + // Paint the border around our bounds + if (borderWidth > 0) + { + PaintBorder(g, bounds); + bounds.Inflate(-borderWidth, -borderWidth); + } + + PaintParentRows(g, bounds, alignRight); + } + + private void PaintBorder(Graphics g, Rectangle bounds) + { + Rectangle border = bounds; + + // top + border.Height = borderWidth; + g.FillRectangle(borderBrush, border); + + // bottom + border.Y = bounds.Bottom - borderWidth; + g.FillRectangle(borderBrush, border); + + // left + border = new Rectangle(bounds.X, bounds.Y + borderWidth, + borderWidth, bounds.Height - 2 * borderWidth); + g.FillRectangle(borderBrush, border); + + // right + border.X = bounds.Right - borderWidth; + g.FillRectangle(borderBrush, border); + } + + // will return the width of the text box that will fit all the + // tables names + private int GetTableBoxWidth(Graphics g, Font font) + { + // try to make the font BOLD + Font textFont = font; + try + { + textFont = new Font(font, FontStyle.Bold); + } + catch + { + } + + int width = 0; + for (int row = 0; row < parentsCount; row++) + { + DataGridState dgs = (DataGridState)parents[row]; + // Graphics.MeasureString(...) returns different results for ": " than for " :" + // + string displayTableName = dgs.ListManager.GetListName() + " :"; + int size = (int)g.MeasureString(displayTableName, textFont).Width; + width = Math.Max(size, width); + } + + return width; + } + + // will return the width of the text box that will + // fit all the column names + private int GetColBoxWidth(Graphics g, Font font, int colNum) + { + int width = 0; + + for (int row = 0; row < parentsCount; row++) + { + DataGridState dgs = (DataGridState)parents[row]; + GridColumnStylesCollection columns = dgs.GridColumnStyles; + if (colNum < columns.Count) + { + // Graphics.MeasureString(...) returns different results for ": " than for " :" + // + string colName = columns[colNum].HeaderText + " :"; + int size = (int)g.MeasureString(colName, font).Width; + width = Math.Max(size, width); + } + } + + return width; + } + + // will return the width of the best fit for the column + // + private int GetColDataBoxWidth(Graphics g, int colNum) + { + int width = 0; + for (int row = 0; row < parentsCount; row++) + { + DataGridState dgs = (DataGridState)parents[row]; + GridColumnStylesCollection columns = dgs.GridColumnStyles; + if (colNum < columns.Count) + { + object value = columns[colNum].GetColumnValueAtRow((CurrencyManager)dataGrid.BindingContext[dgs.DataSource, dgs.DataMember], + dgs.LinkingRow.RowNumber); + int size = columns[colNum].GetPreferredSize(g, value).Width; + width = Math.Max(size, width); + } + } + + return width; + } + + // will return the count of the table with the largest number of columns + private int ColsCount() + { + int colNum = 0; + for (int row = 0; row < parentsCount; row++) + { + DataGridState dgs = (DataGridState)parents[row]; + colNum = Math.Max(colNum, dgs.GridColumnStyles.Count); + } + + return colNum; + } + + // will return the total width required to paint the parentRows + private int TotalWidth(int tableNameBoxWidth, int[] colsNameWidths, int[] colsDataWidths) + { + int totalWidth = 0; + totalWidth += tableNameBoxWidth; + Debug.Assert(colsNameWidths.Length == colsDataWidths.Length, "both arrays are as long as the largest column count in dgs"); + for (int i = 0; i < colsNameWidths.Length; i++) + { + totalWidth += colsNameWidths[i]; + totalWidth += colsDataWidths[i]; + } + + // let 3 pixels in between datacolumns + // see DonnaWa + totalWidth += 3 * (colsNameWidths.Length - 1); + return totalWidth; + } + + // computes the layout for the parent rows + // + private void ComputeLayout(Rectangle bounds, int tableNameBoxWidth, int[] colsNameWidths, int[] colsDataWidths) + { + int totalWidth = TotalWidth(tableNameBoxWidth, colsNameWidths, colsDataWidths); + if (totalWidth > bounds.Width) + { + layout.leftArrow = new Rectangle(bounds.X, bounds.Y, 15, bounds.Height); + layout.data = new Rectangle(layout.leftArrow.Right, bounds.Y, bounds.Width - 30, bounds.Height); + layout.rightArrow = new Rectangle(layout.data.Right, bounds.Y, 15, bounds.Height); + } + else + { + layout.data = bounds; + layout.leftArrow = Rectangle.Empty; + layout.rightArrow = Rectangle.Empty; + } + } + + private void PaintParentRows(Graphics g, Rectangle bounds, bool alignToRight) + { + // variables needed for aligning the table and column names + int tableNameBoxWidth = 0; + int numCols = ColsCount(); + int[] colsNameWidths = new int[numCols]; + int[] colsDataWidths = new int[numCols]; + + // compute the size of the box that will contain the tableName + // + if (dataGrid.ParentRowsLabelStyle == DataGridParentRowsLabelStyle.TableName || + dataGrid.ParentRowsLabelStyle == DataGridParentRowsLabelStyle.Both) + { + tableNameBoxWidth = GetTableBoxWidth(g, dataGrid.Font); + } + + // initialiaze the arrays that contain the column names and the column size + // + for (int i = 0; i < numCols; i++) + { + if (dataGrid.ParentRowsLabelStyle == DataGridParentRowsLabelStyle.ColumnName || + dataGrid.ParentRowsLabelStyle == DataGridParentRowsLabelStyle.Both) + { + colsNameWidths[i] = GetColBoxWidth(g, dataGrid.Font, i); + } + else + { + colsNameWidths[i] = 0; + } + + colsDataWidths[i] = GetColDataBoxWidth(g, i); + } + + // compute the layout + // + ComputeLayout(bounds, tableNameBoxWidth, colsNameWidths, colsDataWidths); + + // paint the navigation arrows, if necessary + // + if (!layout.leftArrow.IsEmpty) + { + g.FillRectangle(BackBrush, layout.leftArrow); + PaintLeftArrow(g, layout.leftArrow, alignToRight); + } + + // paint the parent rows: + // + Rectangle rowBounds = layout.data; + for (int row = 0; row < parentsCount; ++row) + { + rowBounds.Height = (int)rowHeights[row]; + if (rowBounds.Y > bounds.Bottom) + { + break; + } + + int paintedWidth = PaintRow(g, rowBounds, row, dataGrid.Font, alignToRight, tableNameBoxWidth, colsNameWidths, colsDataWidths); + if (row == parentsCount - 1) + { + break; + } + + // draw the grid line below + g.DrawLine(gridLinePen, rowBounds.X, rowBounds.Bottom, + rowBounds.X + paintedWidth, + rowBounds.Bottom); + rowBounds.Y += rowBounds.Height; + } + + if (!layout.rightArrow.IsEmpty) + { + g.FillRectangle(BackBrush, layout.rightArrow); + PaintRightArrow(g, layout.rightArrow, alignToRight); + } + } + + private Bitmap GetBitmap(string bitmapName) + { + try + { + return ScaleHelper.GetIconResourceAsDefaultSizeBitmap(typeof(DataGridParentRows), bitmapName); + } + catch (Exception e) + { + Debug.Fail("Failed to load bitmap: " + bitmapName, e.ToString()); + return null; + } + } + + private Bitmap GetRightArrowBitmap() + { + if (rightArrow is null) + { + rightArrow = GetBitmap("DataGridParentRows.RightArrow"); + } + + return rightArrow; + } + + private Bitmap GetLeftArrowBitmap() + { + if (leftArrow is null) + { + leftArrow = GetBitmap("DataGridParentRows.LeftArrow"); + } + + return leftArrow; + } + + private void PaintBitmap(Graphics g, Bitmap b, Rectangle bounds) + { + // center the bitmap in the bounds: + int bmpX = bounds.X + (bounds.Width - b.Width) / 2; + int bmpY = bounds.Y + (bounds.Height - b.Height) / 2; + Rectangle bmpRect = new Rectangle(bmpX, bmpY, b.Width, b.Height); + + g.FillRectangle(BackBrush, bmpRect); + + // now draw the bitmap + ImageAttributes attr = new ImageAttributes(); + colorMap[0].NewColor = ForeColor; + attr.SetRemapTable(colorMap, ColorAdjustType.Bitmap); + g.DrawImage(b, bmpRect, 0, 0, bmpRect.Width, bmpRect.Height, GraphicsUnit.Pixel, attr); + attr.Dispose(); + } + + /* + private void PaintOverButton(Graphics g, Rectangle bounds) + { + } + */ + + private void PaintDownButton(Graphics g, Rectangle bounds) + { + g.DrawLine(Pens.Black, bounds.X, bounds.Y, bounds.X + bounds.Width, bounds.Y); // the top + g.DrawLine(Pens.White, bounds.X + bounds.Width, bounds.Y, bounds.X + bounds.Width, bounds.Y + bounds.Height); // the right side + g.DrawLine(Pens.White, bounds.X + bounds.Width, bounds.Y + bounds.Height, bounds.X, bounds.Y + bounds.Height); // the right side + g.DrawLine(Pens.Black, bounds.X, bounds.Y + bounds.Height, bounds.X, bounds.Y); // the left side + } + + private void PaintLeftArrow(Graphics g, Rectangle bounds, bool alignToRight) + { + Bitmap bmp = GetLeftArrowBitmap(); + // paint the border around this bitmap if this is the case + // + /* + if (overLeftArrow) + { + Debug.Assert(!downLeftArrow, "can both of those happen?"); + PaintOverButton(g, bounds); + layout.leftArrow.Inflate(-1,-1); + } + */ + if (downLeftArrow) + { + PaintDownButton(g, bounds); + layout.leftArrow.Inflate(-1, -1); + lock (bmp) + { + PaintBitmap(g, bmp, bounds); + } + + layout.leftArrow.Inflate(1, 1); + } + else + { + lock (bmp) + { + PaintBitmap(g, bmp, bounds); + } + } + } + + private void PaintRightArrow(Graphics g, Rectangle bounds, bool alignToRight) + { + Bitmap bmp = GetRightArrowBitmap(); + // paint the border around this bitmap if this is the case + // + /* + if (overRightArrow) + { + Debug.Assert(!downRightArrow, "can both of those happen?"); + PaintOverButton(g, bounds); + layout.rightArrow.Inflate(-1,-1); + } + */ + if (downRightArrow) + { + PaintDownButton(g, bounds); + layout.rightArrow.Inflate(-1, -1); + lock (bmp) + { + PaintBitmap(g, bmp, bounds); + } + + layout.rightArrow.Inflate(1, 1); + } + else + { + lock (bmp) + { + PaintBitmap(g, bmp, bounds); + } + } + } + + private int PaintRow(Graphics g, Rectangle bounds, int row, Font font, bool alignToRight, + int tableNameBoxWidth, int[] colsNameWidths, int[] colsDataWidths) + { + DataGridState dgs = (DataGridState)parents[row]; + Rectangle paintBounds = bounds; + Rectangle rowBounds = bounds; + paintBounds.Height = (int)rowHeights[row]; + rowBounds.Height = (int)rowHeights[row]; + + int paintedWidth = 0; + // used for scrolling: when paiting, we will skip horizOffset cells in the dataGrid ParentRows + int skippedCells = 0; + + // paint the table name + if (dataGrid.ParentRowsLabelStyle == DataGridParentRowsLabelStyle.TableName || + dataGrid.ParentRowsLabelStyle == DataGridParentRowsLabelStyle.Both) + { + if (skippedCells < horizOffset) + { + // skip this + skippedCells++; + } + else + { + paintBounds.Width = Math.Min(paintBounds.Width, tableNameBoxWidth); + paintBounds.X = MirrorRect(bounds, paintBounds, alignToRight); + string displayTableName = dgs.ListManager.GetListName() + ": "; + PaintText(g, paintBounds, displayTableName, font, true, alignToRight); // true is for painting bold + paintedWidth += paintBounds.Width; + } + } + + if (paintedWidth >= bounds.Width) + { + return bounds.Width; // we painted everything + } + + rowBounds.Width -= paintedWidth; + rowBounds.X += alignToRight ? 0 : paintedWidth; + paintedWidth += PaintColumns(g, rowBounds, dgs, font, alignToRight, colsNameWidths, colsDataWidths, skippedCells); + + // paint the possible space left after columns + if (paintedWidth < bounds.Width) + { + paintBounds.X = bounds.X + paintedWidth; + paintBounds.Width = bounds.Width - paintedWidth; + paintBounds.X = MirrorRect(bounds, paintBounds, alignToRight); + g.FillRectangle(BackBrush, paintBounds); + } + + return paintedWidth; + } + + private int PaintColumns(Graphics g, Rectangle bounds, DataGridState dgs, Font font, bool alignToRight, + int[] colsNameWidths, int[] colsDataWidths, int skippedCells) + { + Rectangle paintBounds = bounds; + Rectangle rowBounds = bounds; + GridColumnStylesCollection cols = dgs.GridColumnStyles; + int cx = 0; + + for (int i = 0; i < cols.Count; i++) + { + if (cx >= bounds.Width) + { + break; + } + + // paint the column name, if we have to + if (dataGrid.ParentRowsLabelStyle == DataGridParentRowsLabelStyle.ColumnName || + dataGrid.ParentRowsLabelStyle == DataGridParentRowsLabelStyle.Both) + { + if (skippedCells < horizOffset) + { + // skip this column + } + else + { + paintBounds.X = bounds.X + cx; + paintBounds.Width = Math.Min(bounds.Width - cx, colsNameWidths[i]); + paintBounds.X = MirrorRect(bounds, paintBounds, alignToRight); + + string colName = cols[i].HeaderText + ": "; + PaintText(g, paintBounds, colName, font, false, alignToRight); // false is for not painting bold + + cx += paintBounds.Width; + } + } + + if (cx >= bounds.Width) + { + break; + } + + if (skippedCells < horizOffset) + { + // skip this cell + skippedCells++; + } + else + { + // paint the cell contents + paintBounds.X = bounds.X + cx; + paintBounds.Width = Math.Min(bounds.Width - cx, colsDataWidths[i]); + paintBounds.X = MirrorRect(bounds, paintBounds, alignToRight); + + // when we paint the data grid parent rows, we want to paint the data at the position + // stored in the currency manager. + cols[i].Paint(g, paintBounds, (CurrencyManager)dataGrid.BindingContext[dgs.DataSource, dgs.DataMember], + dataGrid.BindingContext[dgs.DataSource, dgs.DataMember].Position, BackBrush, ForeBrush, alignToRight); + + cx += paintBounds.Width; + + // draw the line to the right (or left, according to alignRight) + // + g.DrawLine(new Pen(SystemColors.ControlDark), + alignToRight ? paintBounds.X : paintBounds.Right, + paintBounds.Y, + alignToRight ? paintBounds.X : paintBounds.Right, + paintBounds.Bottom); + + // this is how wide the line is.... + cx++; + + // put 3 pixels in between columns + // see DonnaWa + // + if (i < cols.Count - 1) + { + paintBounds.X = bounds.X + cx; + paintBounds.Width = Math.Min(bounds.Width - cx, 3); + paintBounds.X = MirrorRect(bounds, paintBounds, alignToRight); + + g.FillRectangle(BackBrush, paintBounds); + cx += 3; + } + } + } + + return cx; + } + + /// + /// Draws on the screen the text. It is used only to paint the Table Name and the column Names + /// Returns the width of bounding rectangle that was passed in + /// + private int PaintText(Graphics g, Rectangle textBounds, string text, Font font, bool bold, bool alignToRight) + { + Font textFont = font; + if (bold) + { + try + { + textFont = new Font(font, FontStyle.Bold); + } + catch { } + } + else + { + textFont = font; + } + + // right now, we paint the entire box, cause it will be used anyway + g.FillRectangle(BackBrush, textBounds); + StringFormat format = new StringFormat(); + if (alignToRight) + { + format.FormatFlags |= StringFormatFlags.DirectionRightToLeft; + format.Alignment = StringAlignment.Far; + } + + format.FormatFlags |= StringFormatFlags.NoWrap; + // part 1, section 3: put the table and the column name in the + // parent rows at the same height as the dataGridTextBoxColumn draws the string + // + textBounds.Offset(0, 2); + textBounds.Height -= 2; + g.DrawString(text, textFont, ForeBrush, textBounds, format); + format.Dispose(); + return textBounds.Width; + } + + // will return the X coordinate of the containedRect mirrored within the surroundingRect + // according to the value of alignToRight + private int MirrorRect(Rectangle surroundingRect, Rectangle containedRect, bool alignToRight) + { + Debug.Assert(containedRect.X >= surroundingRect.X && containedRect.Right <= surroundingRect.Right, "containedRect is not contained in surroundingRect"); + if (alignToRight) + { + return surroundingRect.Right - containedRect.Right + surroundingRect.X; + } + else + { + return containedRect.X; + } + } + + private class Layout + { + public Rectangle data; + public Rectangle leftArrow; + public Rectangle rightArrow; + + public Layout() + { + data = Rectangle.Empty; + leftArrow = Rectangle.Empty; + rightArrow = Rectangle.Empty; + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(200); + sb.Append("ParentRows Layout: \n"); + sb.Append("data = "); + sb.Append(data.ToString()); + sb.Append("\n leftArrow = "); + sb.Append(leftArrow.ToString()); + sb.Append("\n rightArrow = "); + sb.Append(rightArrow.ToString()); + sb.Append('\n'); + + return sb.ToString(); + } + } + + [ComVisible(true)] + protected internal class DataGridParentRowsAccessibleObject : AccessibleObject + { + readonly DataGridParentRows owner; + + public DataGridParentRowsAccessibleObject(DataGridParentRows owner) : base() + { + Debug.Assert(owner is not null, "DataGridParentRowsAccessibleObject must have a valid owner"); + this.owner = owner; + } + + internal DataGridParentRows Owner + { + get + { + return owner; + } + } + + public override Rectangle Bounds + { + get + { + return owner.dataGrid.RectangleToScreen(owner.dataGrid.ParentRowsBounds); + } + } + + public override string DefaultAction + { + get + { + return SR.AccDGNavigateBack; + } + } + + public override string Name + { + get + { + return SR.AccDGParentRows; + } + } + + public override AccessibleObject Parent + { + get + { + return owner.dataGrid.AccessibilityObject; + } + } + + public override AccessibleRole Role + { + get + { + return AccessibleRole.List; + } + } + + public override AccessibleStates State + { + get + { + AccessibleStates state = AccessibleStates.ReadOnly; + + if (owner.parentsCount == 0) + { + state |= AccessibleStates.Invisible; + } + + if (owner.dataGrid.ParentRowsVisible) + { + state |= AccessibleStates.Expanded; + } + else + { + state |= AccessibleStates.Collapsed; + } + + return state; + } + } + + public override string Value + { + get + { + return null; + } + } + + public override void DoDefaultAction() + { + owner.dataGrid.NavigateBack(); + } + + public override AccessibleObject GetChild(int index) + { + return ((DataGridState)owner.parents[index]).ParentRowAccessibleObject; + } + + public override int GetChildCount() + { + return owner.parentsCount; + } + + /// + /// Returns the currently focused child, if any. + /// Returns this if the object itself is focused. + /// + public override AccessibleObject GetFocused() + { + return null; + } + + internal AccessibleObject GetNext(AccessibleObject child) + { + int children = GetChildCount(); + bool hit = false; + + for (int i = 0; i < children; i++) + { + if (hit) + { + return GetChild(i); + } + + if (GetChild(i) == child) + { + hit = true; + } + } + + return null; + } + + internal AccessibleObject GetPrev(AccessibleObject child) + { + int children = GetChildCount(); + bool hit = false; + + for (int i = children - 1; i >= 0; i--) + { + if (hit) + { + return GetChild(i); + } + + if (GetChild(i) == child) + { + hit = true; + } + } + + return null; + } + + /// + /// Navigate to the next or previous grid entry. + /// + public override AccessibleObject Navigate(AccessibleNavigation navdir) + { + switch (navdir) + { + case AccessibleNavigation.Right: + case AccessibleNavigation.Next: + case AccessibleNavigation.Down: + return Parent.GetChild(1); + case AccessibleNavigation.Up: + case AccessibleNavigation.Left: + case AccessibleNavigation.Previous: + return Parent.GetChild(GetChildCount() - 1); + case AccessibleNavigation.FirstChild: + if (GetChildCount() > 0) + { + return GetChild(0); + } + + break; + case AccessibleNavigation.LastChild: + if (GetChildCount() > 0) + { + return GetChild(GetChildCount() - 1); + } + + break; + } + + return null; + } + + public override void Select(AccessibleSelection flags) + { + } + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridParentRowsLabel.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridParentRowsLabel.cs index b8ecd8d2075..80145504be7 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridParentRowsLabel.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridParentRowsLabel.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable WFDEV006 + using System.ComponentModel; namespace System.Windows.Forms; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridPreferredColumnWidthTypeConverter.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridPreferredColumnWidthTypeConverter.cs index b1af37e754e..81ffb0f587a 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridPreferredColumnWidthTypeConverter.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridPreferredColumnWidthTypeConverter.cs @@ -1,24 +1,76 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable RS0016 + +#nullable disable + using System.ComponentModel; +using System.Globalization; namespace System.Windows.Forms; + public class DataGridPreferredColumnWidthTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == typeof(string) || sourceType == typeof(int)) + { + return true; + } + else + { + return false; + } + } -#nullable disable + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (destinationType == typeof(string)) + { + if (value.GetType() == typeof(int)) + { + int pulica = (int)value; + if (pulica == -1) + { + return "AutoColumnResize (-1)"; + } + else + { + return pulica.ToString(CultureInfo.CurrentCulture); + } + } + else + { + return base.ConvertTo(context, culture, value, destinationType); + } + } + else + { + return base.ConvertTo(context, culture, value, destinationType); + } + } -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -public class DataGridPreferredColumnWidthTypeConverter : TypeConverter -{ - // Added explicit constructor to prevent the creation of a default constructor by the compiler. - public DataGridPreferredColumnWidthTypeConverter() => throw new PlatformNotSupportedException(); -} + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value.GetType() == typeof(string)) + { + string text = value.ToString(); + if (text.Equals("AutoColumnResize (-1)")) + { + return -1; + } + else + { + return int.Parse(text, CultureInfo.CurrentCulture); + } + } + else if (value.GetType() == typeof(int)) + { + return (int)value; + } + else + { + throw GetConvertFromException(value); + } + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridRelationshipRow.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridRelationshipRow.cs new file mode 100644 index 00000000000..3304b2273d1 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridRelationshipRow.cs @@ -0,0 +1,1281 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1510, CSIsNull001, CSIsNull002, IDE0100, IDE0059, IDE0051, IDE0060, IDE0036, CA1822, CA1823 + +#nullable disable + +using System.Collections; +using System.ComponentModel; +using System.Drawing; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace System.Windows.Forms; + /// + /// This class fully encapsulates the painting logic for a row + /// appearing in a DataGrid. + /// + internal class DataGridRelationshipRow : DataGridRow + { + private const bool defaultOpen = false; + private const int expandoBoxWidth = 14; + private const int indentWidth = 20; + // private const int relationshipSpacing = 1; + private const int triangleSize = 5; + + private bool expanded = defaultOpen; + // private bool hasRelationships = false; + // private Font linkFont = null; + // private new DataGrid dataGrid; // Currently used only to obtain a Graphics object for measuring text + + // private Rectangle relationshipRect = Rectangle.Empty; + // private int relationshipHeight = 0; + + // relationships + // we should get this directly from the dgTable. + // private ArrayList relationships; + // private int focusedRelation = -1; + // private int focusedTextWidth; + + public DataGridRelationshipRow(DataGrid dataGrid, DataGridTableStyle dgTable, int rowNumber) + : base(dataGrid, dgTable, rowNumber) + { + // this.dataGrid = dataGrid; + // linkFont = dataGrid.LinkFont; + // relationshipHeight = dataGrid.LinkFontHeight + this.dgTable.relationshipSpacing; + + // if (DataGrid.AllowNavigation) { + // hasRelationships = dgTable.RelationsList.Count > 0; + // } + } + + internal protected override int MinimumRowHeight(GridColumnStylesCollection cols) + { + /* + if (DataGrid != null && DataGrid.LinkFontHeight + this.dgTable.relationshipSpacing != relationshipHeight) { + relationshipRect = Rectangle.Empty; + relationshipHeight = DataGrid.LinkFontHeight + this.dgTable.relationshipSpacing; + } + */ + + return base.MinimumRowHeight(cols) + (expanded ? GetRelationshipRect().Height : 0); + } + + internal protected override int MinimumRowHeight(DataGridTableStyle dgTable) + { + /* + if (DataGrid != null && DataGrid.LinkFontHeight + this.dgTable.relationshipSpacing != relationshipHeight) { + relationshipRect = Rectangle.Empty; + relationshipHeight = DataGrid.LinkFontHeight + this.dgTable.relationshipSpacing; + } + */ + + return base.MinimumRowHeight(dgTable) + (expanded ? GetRelationshipRect().Height : 0); + } + + // =------------------------------------------------------------------ + // = Properties + // =------------------------------------------------------------------ + + public virtual bool Expanded + { + get + { + return expanded; + } + set + { + if (expanded == value) + { + return; + } + + if (expanded) + { + Collapse(); + } + else + { + Expand(); + } + } + } + + /* + private Color BorderColor { + get { + if (DataGrid == null) + return Color.Empty; + return DataGrid.GridLineColor; + } + } + */ + +#if FALSE + private int BorderWidth { + get { + DataGrid dataGrid = this.DataGrid; + if (dataGrid == null) + return 0; + // if the user set the GridLineStyle property on the dataGrid. + // then use the value of that property + DataGridLineStyle gridStyle; + int gridLineWidth; + if (this.dgTable.IsDefault) { + gridStyle = this.DataGrid.GridLineStyle; + gridLineWidth = this.DataGrid.GridLineWidth; + } else { + gridStyle = this.dgTable.GridLineStyle; + gridLineWidth = this.dgTable.GridLineWidth; + } + + if (gridStyle == DataGridLineStyle.None) + return 0; + + return gridLineWidth; + } + } +#endif //FALSE + + private int FocusedRelation + { + get + { + return dgTable.FocusedRelation; + } + set + { + dgTable.FocusedRelation = value; + } + } + + // =------------------------------------------------------------------ + // = Methods + // =------------------------------------------------------------------ + + private void Collapse() + { + Debug.Assert(dgTable.DataGrid.AllowNavigation, "how can the user collapse the relations if the grid does not allow navigation?"); + if (expanded) + { + expanded = false; + // relationshipRect = Rectangle.Empty; + FocusedRelation = -1; + DataGrid.OnRowHeightChanged(this); + } + } + + protected override AccessibleObject CreateAccessibleObject() + { + return new DataGridRelationshipRowAccessibleObject(this); + } + + private void Expand() + { + Debug.Assert(dgTable.DataGrid.AllowNavigation, "how can the user expand the relations if the grid does not allow navigation?"); + if (expanded == false + && DataGrid != null + && dgTable != null + && dgTable.RelationsList.Count > 0) + { + expanded = true; + FocusedRelation = -1; + + // relationshipRect = Rectangle.Empty; + DataGrid.OnRowHeightChanged(this); + } + } + + public override int Height + { + get + { + int height = base.Height; + if (expanded) + { + return height + GetRelationshipRect().Height; + } + else + { + return height; + } + } + set + { + // we should use the RelationshipRect only when the row is expanded + if (expanded) + { + base.Height = value - GetRelationshipRect().Height; + } + else + { + base.Height = value; + } + } + } + + // so the edit box will not paint under the + // grid line of the row + public override Rectangle GetCellBounds(int col) + { + Rectangle cellBounds = base.GetCellBounds(col); + // decrement base.Height by 1, so the edit box will not + // paint over the bottom line. + cellBounds.Height = base.Height - 1; + return cellBounds; + } + + /// + /// Given an origin, this procedure returns + /// a rectangle that describes the location of an outline box. + /// + private Rectangle GetOutlineRect(int xOrigin, int yOrigin) + { + Rectangle outline = new Rectangle(xOrigin + 2, + yOrigin + 2, + 9, + 9); + return outline; + } + + public override Rectangle GetNonScrollableArea() + { + if (expanded) + { + return GetRelationshipRect(); + } + else + { + return Rectangle.Empty; + } + } + + private Rectangle GetRelationshipRect() + { + Debug.Assert(expanded, "we should need this rectangle only when the row is expanded"); + Rectangle ret = dgTable.RelationshipRect; + ret.Y = base.Height - dgTable.BorderWidth; + return ret; + } + +#if FALSE + private Rectangle GetRelationshipRect() { + if (relationshipRect.IsEmpty) { + Debug.WriteLineIf(CompModSwitches.DGRelationShpRowLayout.TraceVerbose, "GetRelationshipRect grinding away"); + if (!expanded) { + return(relationshipRect = new Rectangle(0,0,0,0)); + } + Graphics g = DataGrid.CreateGraphicsInternal(); + relationshipRect = new Rectangle(); + relationshipRect.X = 0; //indentWidth; + relationshipRect.Y = base.Height - this.dgTable.BorderWidth; + + // Determine the width of the widest relationship name + int longestRelationship = 0; + for (int r = 0; r < this.dgTable.RelationsList.Count; ++r) { + int rwidth = (int) Math.Ceiling(g.MeasureString(((string) this.dgTable.RelationsList[r]), this.DataGrid.LinkFont).Width); + if (rwidth > longestRelationship) + longestRelationship = rwidth; + } + + g.Dispose(); + + relationshipRect.Width = longestRelationship + 5; + relationshipRect.Width += 2; // relationshipRect border; + relationshipRect.Height = this.dgTable.BorderWidth + relationshipHeight * this.dgTable.RelationsList.Count; + relationshipRect.Height += 2; // relationship border + if (this.dgTable.RelationsList.Count > 0) + relationshipRect.Height += 2 * System.Windows.Forms.DataGridTableStyle.relationshipSpacing; + } + return relationshipRect; + } + +#endif// FALSE + + private Rectangle GetRelationshipRectWithMirroring() + { + Rectangle relRect = GetRelationshipRect(); + bool rowHeadersVisible = dgTable.IsDefault ? DataGrid.RowHeadersVisible : dgTable.RowHeadersVisible; + if (rowHeadersVisible) + { + int rowHeaderWidth = dgTable.IsDefault ? DataGrid.RowHeaderWidth : dgTable.RowHeaderWidth; + relRect.X += DataGrid.GetRowHeaderRect().X + rowHeaderWidth; + } + + relRect.X = MirrorRelationshipRectangle(relRect, DataGrid.GetRowHeaderRect(), DataGrid.RightToLeft == RightToLeft.Yes); + return relRect; + } + + /// + /// Called by the DataGrid when a click occurs in the row's client + /// area. The coordinates are normalized to the rectangle's top + /// left point. + /// + private bool PointOverPlusMinusGlyph(int x, int y, Rectangle rowHeaders, bool alignToRight) + { + if (dgTable == null || dgTable.DataGrid == null || !dgTable.DataGrid.AllowNavigation) + { + return false; + } + + Rectangle insideRowHeaders = rowHeaders; + if (!DataGrid.FlatMode) + { + insideRowHeaders.Inflate(-1, -1); + } + + Rectangle outline = GetOutlineRect(insideRowHeaders.Right - expandoBoxWidth, 0); + + outline.X = MirrorRectangle(outline.X, outline.Width, insideRowHeaders, alignToRight); + + return outline.Contains(x, y); + } + + public override bool OnMouseDown(int x, int y, Rectangle rowHeaders, bool alignToRight) + { + bool rowHeadersVisible = dgTable.IsDefault ? DataGrid.RowHeadersVisible : dgTable.RowHeadersVisible; + if (rowHeadersVisible) + { + if (PointOverPlusMinusGlyph(x, y, rowHeaders, alignToRight)) + { + if (dgTable.RelationsList.Count == 0) + { + return false; + } + else if (expanded) + { + Collapse(); + } + else + { + Expand(); + } + + DataGrid.OnNodeClick(EventArgs.Empty); + return true; + } + } + + if (!expanded) + { + return base.OnMouseDown(x, y, rowHeaders, alignToRight); + } + + // hit test for relationships + Rectangle relRect = GetRelationshipRectWithMirroring(); + + if (relRect.Contains(x, y)) + { + int r = RelationFromY(y); + if (r != -1) + { + // first, reset the FocusedRelation + FocusedRelation = -1; + DataGrid.NavigateTo(((string)dgTable.RelationsList[r]), this, true); + } + + // DataGrid.OnLinkClick(EventArgs.Empty); + return true; + } + + return base.OnMouseDown(x, y, rowHeaders, alignToRight); + } + + public override bool OnMouseMove(int x, int y, Rectangle rowHeaders, bool alignToRight) + { + if (!expanded) + { + return false; + } + + Rectangle relRect = GetRelationshipRectWithMirroring(); + + if (relRect.Contains(x, y)) + { + DataGrid.Cursor = Cursors.Hand; + return true; + } + + DataGrid.Cursor = Cursors.Default; + return base.OnMouseMove(x, y, rowHeaders, alignToRight); + } + + // this function will not invalidate all of the + // row + public override void OnMouseLeft(Rectangle rowHeaders, bool alignToRight) + { + if (!expanded) + { + return; + } + + Rectangle relRect = GetRelationshipRect(); + relRect.X += rowHeaders.X + dgTable.RowHeaderWidth; + relRect.X = MirrorRelationshipRectangle(relRect, rowHeaders, alignToRight); + + if (FocusedRelation != -1) + { + InvalidateRowRect(relRect); + FocusedRelation = -1; + } + } + + public override void OnMouseLeft() + { + if (!expanded) + { + return; + } + + if (FocusedRelation != -1) + { + InvalidateRow(); + FocusedRelation = -1; + } + + base.OnMouseLeft(); + } + + /// + /// Called by the DataGrid when a keypress occurs on a row with "focus." + /// + public override bool OnKeyPress(Keys keyData) + { + // ignore the shift key if it is not paired w/ the TAB key + if ((keyData & Keys.Modifiers) == Keys.Shift && (keyData & Keys.KeyCode) != Keys.Tab) + { + return false; + } + + switch (keyData & Keys.KeyCode) + { + case Keys.F5: + if (dgTable == null || dgTable.DataGrid == null || !dgTable.DataGrid.AllowNavigation) + { + return false; + } + + if (expanded) + { + Collapse(); + } + else + { + Expand(); + } + + FocusedRelation = -1; + return true; + + // to make the gridTest run w/ the numLock key on + // + case Keys.NumLock: + if (FocusedRelation != -1) + { + return false; + } + else + { + return base.OnKeyPress(keyData); + } + + case Keys.Enter: + if (FocusedRelation != -1) + { + // somebody set the relation number up already + // navigate to the relation + DataGrid.NavigateTo(((string)dgTable.RelationsList[FocusedRelation]), this, true); + + // now reset the FocusedRelation + FocusedRelation = -1; + return true; + } + else + { + return false; + } + + case Keys.Tab: + return false; + + default: + FocusedRelation = -1; + return base.OnKeyPress(keyData); + } + } + + // will reset the FocusedRelation and will invalidate the + // rectangle so that the linkFont is no longer shown + internal override void LoseChildFocus(Rectangle rowHeaders, bool alignToRight) + { + // we only invalidate stuff if the row is expanded. + if (FocusedRelation == -1 || !expanded) + { + return; + } + + FocusedRelation = -1; + Rectangle relRect = GetRelationshipRect(); + relRect.X += rowHeaders.X + dgTable.RowHeaderWidth; + relRect.X = MirrorRelationshipRectangle(relRect, rowHeaders, alignToRight); + InvalidateRowRect(relRect); + } + + // here is the logic for FOCUSED: + // + // first the dataGrid gets the KeyPress. + // the dataGrid passes it to the currentRow. if it is anything other + // than Enter or TAB, the currentRow resets the FocusedRelation variable. + // + // Then the dataGrid takes another look at the TAB key and if it is the case + // it passes it to the row. If the dataRelationshipRow can become focused, + // then it eats the TAB key, otherwise it will give it back to the dataGrid. + // + internal override bool ProcessTabKey(Keys keyData, Rectangle rowHeaders, bool alignToRight) + { + Debug.Assert((keyData & Keys.Control) != Keys.Control, "the DataGridRelationshipRow only handles TAB and TAB-SHIFT"); + Debug.Assert((keyData & Keys.Alt) != Keys.Alt, "the DataGridRelationshipRow only handles TAB and TAB-SHIFT"); + + // if there are no relationships, this row can't do anything with the + // key + if (dgTable.RelationsList.Count == 0 || dgTable.DataGrid == null || !dgTable.DataGrid.AllowNavigation) + { + return false; + } + + // expand the relationship box + if (!expanded) + { + Expand(); + } + + if ((keyData & Keys.Shift) == Keys.Shift) + { + if (FocusedRelation == 0) + { + // if user hits TAB-SHIFT and the focus was on the first relationship then + // reset FocusedRelation and let the dataGrid use the key + // + // consider: Microsoft: if the relationships box is expanded, should we collapse it on leave? + FocusedRelation = -1; + return false; + } + + // we need to invalidate the relationshipRectangle, and cause the linkFont to move + // to the next relation + Rectangle relRect = GetRelationshipRect(); + relRect.X += rowHeaders.X + dgTable.RowHeaderWidth; + relRect.X = MirrorRelationshipRectangle(relRect, rowHeaders, alignToRight); + InvalidateRowRect(relRect); + + if (FocusedRelation == -1) + { + // is the first time that the user focuses on this + // set of relationships + FocusedRelation = dgTable.RelationsList.Count - 1; + } + else + { + FocusedRelation--; + } + + return true; + } + else + { + if (FocusedRelation == dgTable.RelationsList.Count - 1) + { + // if the user hits TAB and the focus was on the last relationship then + // reset FocusedRelation and let the dataGrid use the key + // + // consider: Microsoft: if the relationships box is expanded, should we collapse it on leave? + FocusedRelation = -1; + return false; + } + + // we need to invalidate the relationshipRectangle, and cause the linkFont to move + // to the next relation + Rectangle relRect = GetRelationshipRect(); + relRect.X += rowHeaders.X + dgTable.RowHeaderWidth; + relRect.X = MirrorRelationshipRectangle(relRect, rowHeaders, alignToRight); + InvalidateRowRect(relRect); + + FocusedRelation++; + return true; + } + } + + /// + /// Paints the row. + /// + public override int Paint(Graphics g, Rectangle bounds, Rectangle trueRowBounds, int firstVisibleColumn, int numVisibleColumns) + { + return Paint(g, bounds, trueRowBounds, firstVisibleColumn, numVisibleColumns, false); + } + + public override int Paint(Graphics g, + Rectangle bounds, // negative offsetted row bounds + Rectangle trueRowBounds, // real row bounds. + int firstVisibleColumn, + int numVisibleColumns, + bool alignToRight) + { + if (CompModSwitches.DGRelationShpRowPaint.TraceVerbose) + { + Debug.WriteLine("Painting row " + RowNumber.ToString(CultureInfo.InvariantCulture) + " with bounds " + bounds.ToString()); + } + + int bWidth = dgTable.BorderWidth; + + // paint the data cells + Rectangle dataBounds = bounds; + dataBounds.Height = base.Height - bWidth; + int dataWidth = PaintData(g, dataBounds, firstVisibleColumn, numVisibleColumns, alignToRight); + int dataWidthOffsetted = dataWidth + bounds.X - trueRowBounds.X; + + dataBounds.Offset(0, bWidth); // use bWidth, not 1 + if (bWidth > 0) + { + PaintBottomBorder(g, dataBounds, dataWidth, bWidth, alignToRight); + } + + if (expanded && dgTable.RelationsList.Count > 0) + { + // paint the relationships + Rectangle relationBounds = new Rectangle(trueRowBounds.X, + dataBounds.Bottom, + trueRowBounds.Width, + trueRowBounds.Height - dataBounds.Height - 2 * bWidth); + PaintRelations(g, relationBounds, trueRowBounds, dataWidthOffsetted, + firstVisibleColumn, numVisibleColumns, alignToRight); + relationBounds.Height += 1; + if (bWidth > 0) + { + PaintBottomBorder(g, relationBounds, dataWidthOffsetted, bWidth, alignToRight); + } + } + + return dataWidth; + } + + protected override void PaintCellContents(Graphics g, Rectangle cellBounds, DataGridColumnStyle column, + Brush backBr, Brush foreBrush, bool alignToRight) + { + CurrencyManager listManager = DataGrid.ListManager; + + // painting the error.. + // + string errString = string.Empty; + Rectangle bounds = cellBounds; + object errInfo = DataGrid.ListManager[number]; + if (errInfo is IDataErrorInfo) + { + errString = ((IDataErrorInfo)errInfo)[column.PropertyDescriptor.Name]; + } + + if (!string.IsNullOrEmpty(errString)) + { + Bitmap bmp = GetErrorBitmap(); + Rectangle errRect; + lock (bmp) + { + errRect = PaintIcon(g, bounds, true, alignToRight, bmp, backBr); + } + + // paint the errors correctly when RTL = true + if (alignToRight) + { + bounds.Width -= errRect.Width + xOffset; + } + else + { + bounds.X += errRect.Width + xOffset; + } + + DataGrid.ToolTipProvider.AddToolTip(errString, (IntPtr)(DataGrid.ToolTipId++), errRect); + } + + column.Paint(g, bounds, listManager, RowNumber, backBr, foreBrush, alignToRight); + } + + public override void PaintHeader(Graphics g, Rectangle bounds, bool alignToRight, bool isDirty) + { + DataGrid grid = DataGrid; + + Rectangle insideBounds = bounds; + + if (!grid.FlatMode) + { + ControlPaint.DrawBorder3D(g, insideBounds, Border3DStyle.RaisedInner); + insideBounds.Inflate(-1, -1); + } + + if (dgTable.IsDefault) + { + PaintHeaderInside(g, insideBounds, DataGrid.HeaderBackBrush, alignToRight, isDirty); + } + else + { + PaintHeaderInside(g, insideBounds, dgTable.HeaderBackBrush, alignToRight, isDirty); + } + } + + public void PaintHeaderInside(Graphics g, Rectangle bounds, Brush backBr, bool alignToRight, bool isDirty) + { + // paint the row header + bool paintPlusMinus = dgTable.RelationsList.Count > 0 && dgTable.DataGrid.AllowNavigation; + int rowHeaderBoundsX = MirrorRectangle(bounds.X, + bounds.Width - (paintPlusMinus ? expandoBoxWidth : 0), + bounds, alignToRight); + + if (!alignToRight) + { + Debug.Assert(bounds.X == rowHeaderBoundsX, "what's up doc?"); + } + + Rectangle rowHeaderBounds = new Rectangle(rowHeaderBoundsX, + bounds.Y, + bounds.Width - (paintPlusMinus ? expandoBoxWidth : 0), + bounds.Height); + + base.PaintHeader(g, rowHeaderBounds, alignToRight, isDirty); + + // Paint the expando on the right + int expandoBoxX = MirrorRectangle(bounds.X + rowHeaderBounds.Width, expandoBoxWidth, bounds, alignToRight); + + if (!alignToRight) + { + Debug.Assert(rowHeaderBounds.Right == expandoBoxX, "what's up doc?"); + } + + Rectangle expandoBox = new Rectangle(expandoBoxX, + bounds.Y, + expandoBoxWidth, + bounds.Height); + if (paintPlusMinus) + { + PaintPlusMinusGlyph(g, expandoBox, backBr, alignToRight); + } + } + + /// + /// Paints the relationships below the data area. + /// + private void PaintRelations(Graphics g, Rectangle bounds, Rectangle trueRowBounds, + int dataWidth, int firstCol, int nCols, bool alignToRight) + { + // Calculate the relationship rect. + // relationshipRect = Rectangle.Empty; + Rectangle relRect = GetRelationshipRect(); + //relRect.Offset(trueRowBounds.X, trueRowBounds.Y); + relRect.X = alignToRight ? bounds.Right - relRect.Width : bounds.X; + relRect.Y = bounds.Y; + int paintedWidth = Math.Max(dataWidth, relRect.Width); + + // Paint the stuff to the right , or left (Bi-Di) of the relationship rect. + Region r = g.Clip; + g.ExcludeClip(relRect); + + g.FillRectangle(GetBackBrush(), + alignToRight ? bounds.Right - dataWidth : bounds.X, + bounds.Y, + dataWidth, + bounds.Height); + + // Paint the relations' text + g.SetClip(bounds); + + relRect.Height -= dgTable.BorderWidth; // use bWidth not 1 + g.DrawRectangle(SystemPens.ControlText, relRect.X, relRect.Y, relRect.Width - 1, relRect.Height - 1); + relRect.Inflate(-1, -1); + + int cy = PaintRelationText(g, relRect, alignToRight); + + if (cy < relRect.Height) + { + g.FillRectangle(GetBackBrush(), relRect.X, relRect.Y + cy, relRect.Width, relRect.Height - cy); + } + + g.Clip = r; + + // paint any exposed area to the right or to the left (BI-DI) + if (paintedWidth < bounds.Width) + { + int bWidth; + if (dgTable.IsDefault) + { + bWidth = DataGrid.GridLineWidth; + } + else + { + bWidth = dgTable.GridLineWidth; + } + + g.FillRectangle(DataGrid.BackgroundBrush, + alignToRight ? bounds.X : bounds.X + paintedWidth, + bounds.Y, + bounds.Width - paintedWidth - bWidth + 1, // + 1 cause the relationship rectangle was deflated + bounds.Height); + + // Paint the border to the right of each cell + if (bWidth > 0) + { + Brush br; + // if the user changed the gridLineColor on the dataGrid + // from the defaultValue, then use that value; + if (dgTable.IsDefault) + { + br = DataGrid.GridLineBrush; + } + else + { + br = dgTable.GridLineBrush; + } + + g.FillRectangle(br, + alignToRight ? bounds.Right - bWidth - paintedWidth : bounds.X + paintedWidth - bWidth, + bounds.Y, + bWidth, + bounds.Height); + } + } + } + + private int PaintRelationText(Graphics g, Rectangle bounds, bool alignToRight) + { + g.FillRectangle(GetBackBrush(), bounds.X, bounds.Y, bounds.Width, System.Windows.Forms.DataGridTableStyle.relationshipSpacing); + + int relationshipHeight = dgTable.RelationshipHeight; + Rectangle textBounds = new Rectangle(bounds.X, bounds.Y + System.Windows.Forms.DataGridTableStyle.relationshipSpacing, + bounds.Width, + relationshipHeight); + int cy = System.Windows.Forms.DataGridTableStyle.relationshipSpacing; + for (int r = 0; r < dgTable.RelationsList.Count; ++r) + { + if (cy > bounds.Height) + { + break; + } + + Brush textBrush = dgTable.IsDefault ? DataGrid.LinkBrush : dgTable.LinkBrush; + + Font textFont = DataGrid.Font; + textBrush = dgTable.IsDefault ? DataGrid.LinkBrush : dgTable.LinkBrush; + textFont = DataGrid.LinkFont; + + g.FillRectangle(GetBackBrush(), textBounds); + + StringFormat format = new StringFormat(); + if (alignToRight) + { + format.FormatFlags |= StringFormatFlags.DirectionRightToLeft; + format.Alignment = StringAlignment.Far; + } + + g.DrawString(((string)dgTable.RelationsList[r]), textFont, textBrush, textBounds, + format); + if (r == FocusedRelation && number == DataGrid.CurrentCell.RowNumber) + { + textBounds.Width = dgTable.FocusedTextWidth; + ControlPaint.DrawFocusRectangle(g, textBounds, ((SolidBrush)textBrush).Color, ((SolidBrush)GetBackBrush()).Color); + textBounds.Width = bounds.Width; + } + + format.Dispose(); + + textBounds.Y += relationshipHeight; + cy += textBounds.Height; + } + + return cy; + } + + private void PaintPlusMinusGlyph(Graphics g, Rectangle bounds, Brush backBr, bool alignToRight) + { + if (CompModSwitches.DGRelationShpRowPaint.TraceVerbose) + { + Debug.WriteLine("PlusMinusGlyph painting in bounds -> " + bounds.ToString()); + } + + Rectangle outline = GetOutlineRect(bounds.X, bounds.Y); + + outline = Rectangle.Intersect(bounds, outline); + if (outline.IsEmpty) + { + return; + } + + g.FillRectangle(backBr, bounds); + + if (CompModSwitches.DGRelationShpRowPaint.TraceVerbose) + { + Debug.WriteLine("Painting PlusMinusGlyph with outline -> " + outline.ToString()); + } + + // draw the +/- box + Pen drawPen = dgTable.IsDefault ? DataGrid.HeaderForePen : dgTable.HeaderForePen; + g.DrawRectangle(drawPen, outline.X, outline.Y, outline.Width - 1, outline.Height - 1); + + int indent = 2; + // draw the - + g.DrawLine(drawPen, + outline.X + indent, outline.Y + outline.Width / 2, + outline.Right - indent - 1, outline.Y + outline.Width / 2); // -1 on the y coordinate + + if (!expanded) + { + // draw the vertical line to make + + g.DrawLine(drawPen, + outline.X + outline.Height / 2, outline.Y + indent, + outline.X + outline.Height / 2, outline.Bottom - indent - 1); // -1... hinting + } + else + { + Point[] points = new Point[3]; + points[0] = new Point(outline.X + outline.Height / 2, outline.Bottom); + + points[1] = new Point(points[0].X, bounds.Y + 2 * indent + base.Height); + + points[2] = new Point(alignToRight ? bounds.X : bounds.Right, + points[1].Y); + g.DrawLines(drawPen, points); + } + } + + private int RelationFromY(int y) + { + int relation = -1; + int relationshipHeight = dgTable.RelationshipHeight; + Rectangle relRect = GetRelationshipRect(); + int cy = base.Height - dgTable.BorderWidth + System.Windows.Forms.DataGridTableStyle.relationshipSpacing; + while (cy < relRect.Bottom) + { + if (cy > y) + { + break; + } + + cy += relationshipHeight; + relation++; + } + + if (relation >= dgTable.RelationsList.Count) + { + return -1; + } + + return relation; + } + + // given the relRect and the rowHeader, this function will return the + // X coordinate of the relationship rectangle as it should appear on the screen + private int MirrorRelationshipRectangle(Rectangle relRect, Rectangle rowHeader, bool alignToRight) + { + if (alignToRight) + { + return rowHeader.X - relRect.Width; + } + else + { + return relRect.X; + } + } + + // given the X and Width of a rectangle R1 contained in rect, + // this will return the X coordinate of the rectangle that corresponds to R1 in Bi-Di transformation + private int MirrorRectangle(int x, int width, Rectangle rect, bool alignToRight) + { + if (alignToRight) + { + return rect.Right + rect.X - width - x; + } + else + { + return x; + } + } + + [ComVisible(true)] + protected class DataGridRelationshipRowAccessibleObject : DataGridRowAccessibleObject + { + public DataGridRelationshipRowAccessibleObject(DataGridRow owner) : base(owner) + { + } + + protected override void AddChildAccessibleObjects(IList children) + { + base.AddChildAccessibleObjects(children); + DataGridRelationshipRow row = (DataGridRelationshipRow)Owner; + if (row.dgTable.RelationsList != null) + { + for (int i = 0; i < row.dgTable.RelationsList.Count; i++) + { + children.Add(new DataGridRelationshipAccessibleObject(row, i)); + } + } + } + + private DataGridRelationshipRow RelationshipRow + { + get + { + return (DataGridRelationshipRow)Owner; + } + } + + public override string DefaultAction + { + get + { + if (RelationshipRow.dgTable.RelationsList.Count > 0) + { + if (RelationshipRow.Expanded) + { + return SR.AccDGCollapse; + } + else + { + return SR.AccDGExpand; + } + } + + return null; + } + } + + public override AccessibleStates State + { + get + { + AccessibleStates state = base.State; + if (RelationshipRow.dgTable.RelationsList.Count > 0) + { + if (((DataGridRelationshipRow)Owner).Expanded) + { + state |= AccessibleStates.Expanded; + } + else + { + state |= AccessibleStates.Collapsed; + } + } + + return state; + } + } + + public override void DoDefaultAction() + { + if (RelationshipRow.dgTable.RelationsList.Count > 0) + { + ((DataGridRelationshipRow)Owner).Expanded = !((DataGridRelationshipRow)Owner).Expanded; + } + } + + public override AccessibleObject GetFocused() + { + DataGridRelationshipRow row = (DataGridRelationshipRow)Owner; + int focusRel = row.dgTable.FocusedRelation; + if (focusRel == -1) + { + return base.GetFocused(); + } + else + { + return GetChild(GetChildCount() - row.dgTable.RelationsList.Count + focusRel); + } + } + } + + [ComVisible(true)] + protected class DataGridRelationshipAccessibleObject : AccessibleObject + { + readonly DataGridRelationshipRow owner; + readonly int relationship; + + public DataGridRelationshipAccessibleObject(DataGridRelationshipRow owner, int relationship) : base() + { + Debug.Assert(owner != null, "DataGridRelationshipAccessibleObject must have a valid owner DataGridRow"); + this.owner = owner; + this.relationship = relationship; + } + + public override Rectangle Bounds + { + get + { + Rectangle rowBounds = DataGrid.GetRowBounds(owner); + + Rectangle bounds = owner.Expanded ? owner.GetRelationshipRectWithMirroring() : Rectangle.Empty; + bounds.Y += owner.dgTable.RelationshipHeight * relationship; + bounds.Height = owner.Expanded ? owner.dgTable.RelationshipHeight : 0; // when the row is collapsed the height of the relationship object should be 0 + // GetRelationshipRectWithMirroring will use the row headers width + if (!owner.Expanded) + { + bounds.X += rowBounds.X; + } + + bounds.Y += rowBounds.Y; + + return owner.DataGrid.RectangleToScreen(bounds); + } + } + + public override string Name + { + get + { + return (string)owner.dgTable.RelationsList[relationship]; + } + } + + protected DataGridRelationshipRow Owner + { + get + { + return owner; + } + } + + public override AccessibleObject Parent + { + get + { + return owner.AccessibleObject; + } + } + + protected DataGrid DataGrid + { + get + { + return owner.DataGrid; + } + } + + public override AccessibleRole Role + { + get + { + return AccessibleRole.Link; + } + } + + public override AccessibleStates State + { + get + { + DataGridRow[] dgRows = DataGrid.DataGridRows; + if (Array.IndexOf(dgRows, owner) == -1) + { + return AccessibleStates.Unavailable; + } + + AccessibleStates state = AccessibleStates.Selectable + | AccessibleStates.Focusable + | AccessibleStates.Linked; + + if (!owner.Expanded) + { + state |= AccessibleStates.Invisible; + } + + if (DataGrid.Focused && Owner.dgTable.FocusedRelation == relationship) + { + state |= AccessibleStates.Focused; + } + + return state; + } + } + + public override string Value + { + get + { + DataGridRow[] dgRows = DataGrid.DataGridRows; + if (Array.IndexOf(dgRows, owner) == -1) + { + return null; + } + else + { + return (string)owner.dgTable.RelationsList[relationship]; + } + } + set + { + // not supported + } + } + + public override string DefaultAction + { + get + { + return SR.AccDGNavigate; + } + } + + public override void DoDefaultAction() + { + ((DataGridRelationshipRow)Owner).Expanded = true; + owner.FocusedRelation = -1; + DataGrid.NavigateTo((string)owner.dgTable.RelationsList[relationship], owner, true); + DataGrid.BeginInvoke(new MethodInvoker(ResetAccessibilityLayer)); + } + + private void ResetAccessibilityLayer() + { + ((DataGrid.DataGridAccessibleObject)DataGrid.AccessibilityObject).NotifyClients(AccessibleEvents.Reorder, 0); + ((DataGrid.DataGridAccessibleObject)DataGrid.AccessibilityObject).NotifyClients(AccessibleEvents.Focus, DataGrid.CurrentCellAccIndex); + ((DataGrid.DataGridAccessibleObject)DataGrid.AccessibilityObject).NotifyClients(AccessibleEvents.Selection, DataGrid.CurrentCellAccIndex); + } + + /// + /// Navigate to the next or previous grid entry. + /// + public override AccessibleObject Navigate(AccessibleNavigation navdir) + { + switch (navdir) + { + case AccessibleNavigation.Right: + case AccessibleNavigation.Next: + case AccessibleNavigation.Down: + if (relationship + 1 < owner.dgTable.RelationsList.Count) + { + return Parent.GetChild(Parent.GetChildCount() - owner.dgTable.RelationsList.Count + relationship + 1); + } + + break; + case AccessibleNavigation.Up: + case AccessibleNavigation.Left: + case AccessibleNavigation.Previous: + if (relationship > 0) + { + return Parent.GetChild(Parent.GetChildCount() - owner.dgTable.RelationsList.Count + relationship - 1); + } + + break; + } + + return null; + } + + public override void Select(AccessibleSelection flags) + { + // Focus the PropertyGridView window + // + if ((flags & AccessibleSelection.TakeFocus) == AccessibleSelection.TakeFocus) + { + DataGrid.Focus(); + } + + if ((flags & AccessibleSelection.TakeSelection) == AccessibleSelection.TakeSelection) + { + Owner.FocusedRelation = relationship; + } + } + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridRow.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridRow.cs new file mode 100644 index 00000000000..70e20dbd4ff --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridRow.cs @@ -0,0 +1,1199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1510, CSIsNull001, CSIsNull002, WFDEV006, IDE0036, IDE0300, SA1129, IDE0270, IDE0078, IDE0059, IDE0060, CA1822 + +#nullable disable + +using System.Collections; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using DSR = System.Windows.Forms.DataGridStrings; + +namespace System.Windows.Forms; + /// + /// Encapsulates the painting logic for a new row added to a control. + /// + internal abstract class DataGridRow : MarshalByRefObject + { + internal protected int number; // row number + private bool selected; + private int height; + // protected DataRow dataRow; + private IntPtr tooltipID = new IntPtr(-1); + private string tooltip = string.Empty; + AccessibleObject accessibleObject; + + // for accessibility... + // + // internal DataGrid dataGrid; + + // will need this for the painting information ( row header color ) + // + protected DataGridTableStyle dgTable; + + // we will be mapping only the black color to + // the HeaderForeColor + // + private static readonly ColorMap[] colorMap = new ColorMap[] { new ColorMap() }; + + // bitmaps + // + private static Bitmap rightArrow; + private static Bitmap leftArrow; + private static Bitmap errorBmp; + private static Bitmap pencilBmp; + private static Bitmap starBmp; + protected const int xOffset = 3; + protected const int yOffset = 2; + + /// + /// Initializes a new instance of a . + /// + public DataGridRow(DataGrid dataGrid, DataGridTableStyle dgTable, int rowNumber) + { + if (dataGrid == null || dgTable.DataGrid == null) + { + throw new ArgumentNullException(nameof(dataGrid)); + } + + if (rowNumber < 0) + { + throw new ArgumentException(DSR.DataGridRowRowNumber, nameof(rowNumber)); + } + + // this.dataGrid = dataGrid; + number = rowNumber; + + // map the black color in the pictures to the DataGrid's HeaderForeColor + // + colorMap[0].OldColor = Color.Black; + colorMap[0].NewColor = dgTable.HeaderForeColor; + + this.dgTable = dgTable; + height = MinimumRowHeight(dgTable); + } + + public AccessibleObject AccessibleObject + { + get + { + if (accessibleObject == null) + { + accessibleObject = CreateAccessibleObject(); + } + + return accessibleObject; + } + } + + protected virtual AccessibleObject CreateAccessibleObject() + { + return new DataGridRowAccessibleObject(this); + } + + internal protected virtual int MinimumRowHeight(DataGridTableStyle dgTable) + { + return MinimumRowHeight(dgTable.GridColumnStyles); + } + + internal protected virtual int MinimumRowHeight(GridColumnStylesCollection columns) + { + int h = dgTable.IsDefault ? DataGrid.PreferredRowHeight : dgTable.PreferredRowHeight; + + try + { + if (dgTable.DataGrid.DataSource != null) + { + int nCols = columns.Count; + for (int i = 0; i < nCols; ++i) + { + // if (columns[i].Visible && columns[i].PropertyDescriptor != null) + if (columns[i].PropertyDescriptor != null) + { + h = Math.Max(h, columns[i].GetMinimumHeight()); + } + } + } + } + catch + { + } + + return h; + } + + // =------------------------------------------------------------------ + // = Properties + // =------------------------------------------------------------------ + + /// + /// Gets the control the row belongs to. + /// + public DataGrid DataGrid + { + get + { + return dgTable.DataGrid; + } + } + + internal DataGridTableStyle DataGridTableStyle + { + get + { + return dgTable; + } + set + { + dgTable = value; + } + } + + /* + public DataGridTable DataGridTable { + get { + return dgTable; + } + } + */ + + /* + public DataRow DataRow { + get { + return dataRow; + } + } + */ + + /// + /// Gets or sets the height of the row. + /// + public virtual int Height + { + get + { + return height; + } + set + { + // the height of the row should be at least 0. + // this way, if the row has a relationship list and the user resizes the row such that + // the new height does not accomodate the height of the relationship list + // the row will at least show the relationship list ( and not paint on the portion of the row above this one ) + height = Math.Max(0, value); + // when we resize the row, or when we set the PreferredRowHeigth on the + // DataGridTableStyle, we change the height of the Row, which will cause to invalidate, + // then the grid itself will do another invalidate call. + dgTable.DataGrid.OnRowHeightChanged(this); + } + } + + /// + /// Gets the row's number. + /// + public int RowNumber + { + get + { + return number; + } + } + + /// + /// Gets or sets a value indicating whether the row is selected. + /// + public virtual bool Selected + { + get + { + return selected; + } + set + { + selected = value; + InvalidateRow(); + } + } + + // =------------------------------------------------------------------ + // = Methods + // =------------------------------------------------------------------ + + /// + /// Gets the bitmap associated with the row. + /// + protected Bitmap GetBitmap(string bitmapName) + { + try + { + return ScaleHelper.GetIconResourceAsDefaultSizeBitmap(typeof(DataGridCaption), bitmapName); + } + catch (Exception e) + { + Debug.Fail("Failed to load bitmap: " + bitmapName, e.ToString()); + throw; + } + } + + /// + /// When overridden in a derived class, gets the + /// where a cell's contents gets painted. + /// + public virtual Rectangle GetCellBounds(int col) + { + int firstVisibleCol = dgTable.DataGrid.FirstVisibleColumn; + int cx = 0; + Rectangle cellBounds = new Rectangle(); + GridColumnStylesCollection columns = dgTable.GridColumnStyles; + if (columns != null) + { + for (int i = firstVisibleCol; i < col; i++) + { + if (columns[i].PropertyDescriptor != null) + { + cx += columns[i].Width; + } + } + + int borderWidth = dgTable.GridLineWidth; + cellBounds = new Rectangle(cx, + 0, + columns[col].Width - borderWidth, + Height - borderWidth); + } + + return cellBounds; + } + + /// + /// When overridden in a derived class, gets the of the non-scrollable area of + /// the row. + /// + public virtual Rectangle GetNonScrollableArea() + { + return Rectangle.Empty; + } + + /// + /// Gets or sets the bitmap displayed in the row header of a new row. + /// + protected Bitmap GetStarBitmap() + { + if (starBmp == null) + { + starBmp = GetBitmap("DataGridRow.star"); + } + + return starBmp; + } + + /// + /// Gets or sets the bitmap displayed in the row header that indicates a row can + /// be edited. + /// + protected Bitmap GetPencilBitmap() + { + if (pencilBmp == null) + { + pencilBmp = GetBitmap("DataGridRow.pencil"); + } + + return pencilBmp; + } + + /// + /// Gets or sets the bitmap displayed on a row with an error. + /// + protected Bitmap GetErrorBitmap() + { + if (errorBmp == null) + { + errorBmp = GetBitmap("DataGridRow.error"); + } + + return errorBmp; + } + + protected Bitmap GetLeftArrowBitmap() + { + if (leftArrow == null) + { + leftArrow = GetBitmap("DataGridRow.left"); + } + + return leftArrow; + } + + protected Bitmap GetRightArrowBitmap() + { + if (rightArrow == null) + { + rightArrow = GetBitmap("DataGridRow.right"); + } + + return rightArrow; + } + + public virtual void InvalidateRow() + { + dgTable.DataGrid.InvalidateRow(number); + } + + public virtual void InvalidateRowRect(Rectangle r) + { + dgTable.DataGrid.InvalidateRowRect(number, r); + } + + /// + /// When overridden in a derived class, notifies the grid that an edit will + /// occur. + /// + public virtual void OnEdit() + { + } + + /// + /// When overridden in a derived class, called by the control when a key press occurs on a row with focus. + /// + public virtual bool OnKeyPress(Keys keyData) + { + int currentColIndex = dgTable.DataGrid.CurrentCell.ColumnNumber; + GridColumnStylesCollection columns = dgTable.GridColumnStyles; + if (columns != null && currentColIndex >= 0 && currentColIndex < columns.Count) + { + DataGridColumnStyle currentColumn = columns[currentColIndex]; + if (currentColumn.KeyPress(RowNumber, keyData)) + { + return true; + } + } + + return false; + } + + /// + /// Called by the when a click occurs in the row's client area + /// specifed by the x and y coordinates and the specified + /// . + /// + public virtual bool OnMouseDown(int x, int y, Rectangle rowHeaders) + { + return OnMouseDown(x, y, rowHeaders, false); + } + + /// + /// When overridden in a derived class, is called by the when a click occurs + /// in the row's + /// client area, specified by x and y coordinates. + /// + public virtual bool OnMouseDown(int x, int y, Rectangle rowHeaders, bool alignToRight) + { + // if we call base.OnMouseDown, then the row could not use this + // mouse click at all. in that case LoseChildFocus, so the edit control + // will become visible + LoseChildFocus(rowHeaders, alignToRight); + + // we did not use this click at all. + return false; + } + + public virtual bool OnMouseMove(int x, int y, Rectangle rowHeaders) + { + return false; + } + + /// + /// When overridden in a derived class, is called by the when + /// the mouse moves within the row's client area. + /// + public virtual bool OnMouseMove(int x, int y, Rectangle rowHeaders, bool alignToRight) + { + return false; + } + + public virtual void OnMouseLeft(Rectangle rowHeaders, bool alignToRight) + { + } + + public virtual void OnMouseLeft() + { + } + + /// + /// When overridden in a derived class, causes the RowEnter event to occur. + /// + public virtual void OnRowEnter() { } + public virtual void OnRowLeave() { } + + // processes the Tab Key + // returns TRUE if the TAB key is processed + internal abstract bool ProcessTabKey(Keys keyData, Rectangle rowHeaders, bool alignToRight); + + // tells the dataGridRow that it lost the focus + internal abstract void LoseChildFocus(Rectangle rowHeaders, bool alignToRight); + + /// + /// Paints the row. + /// + public abstract int Paint(Graphics g, + Rectangle dataBounds, + Rectangle rowBounds, + int firstVisibleColumn, + int numVisibleColumns); + + public abstract int Paint(Graphics g, + Rectangle dataBounds, + Rectangle rowBounds, + int firstVisibleColumn, + int numVisibleColumns, + bool alignToRight); + + /// + /// Draws a border on the bottom DataGrid.GridLineWidth pixels + /// of the bounding rectangle passed in. + /// + protected virtual void PaintBottomBorder(Graphics g, Rectangle bounds, int dataWidth) + { + PaintBottomBorder(g, bounds, dataWidth, dgTable.GridLineWidth, false); + } + + protected virtual void PaintBottomBorder(Graphics g, Rectangle bounds, int dataWidth, int borderWidth, bool alignToRight) + { + // paint bottom border + Rectangle bottomBorder = new Rectangle(alignToRight ? bounds.Right - dataWidth : bounds.X, + bounds.Bottom - borderWidth, + dataWidth, + borderWidth); + + g.FillRectangle(dgTable.IsDefault ? DataGrid.GridLineBrush : dgTable.GridLineBrush, bottomBorder); + + // paint any exposed region to the right + if (dataWidth < bounds.Width) + { + g.FillRectangle(dgTable.DataGrid.BackgroundBrush, + alignToRight ? bounds.X : bottomBorder.Right, + bottomBorder.Y, + bounds.Width - bottomBorder.Width, + borderWidth); + } + } + + /// + /// Paints the row. + /// + public virtual int PaintData(Graphics g, + Rectangle bounds, + int firstVisibleColumn, + int columnCount) + { + return PaintData(g, bounds, firstVisibleColumn, columnCount, false); + } + + public virtual int PaintData(Graphics g, + Rectangle bounds, + int firstVisibleColumn, + int columnCount, + bool alignToRight) + { + Debug.WriteLineIf(CompModSwitches.DGRowPaint.TraceVerbose, "Painting DataGridAddNewRow: bounds = " + bounds.ToString()); + + Rectangle cellBounds = bounds; + int bWidth = dgTable.IsDefault ? DataGrid.GridLineWidth : dgTable.GridLineWidth; + int cx = 0; + + DataGridCell current = dgTable.DataGrid.CurrentCell; + + GridColumnStylesCollection columns = dgTable.GridColumnStyles; + int nCols = columns.Count; + for (int col = firstVisibleColumn; col < nCols; ++col) + { + if (cx > bounds.Width) + { + break; + } + + // if (!columns[col].Visible || columns[col].PropertyDescriptor == null) + if (columns[col].PropertyDescriptor == null || columns[col].Width <= 0) + { + continue; + } + + cellBounds.Width = columns[col].Width - bWidth; + + if (alignToRight) + { + cellBounds.X = bounds.Right - cx - cellBounds.Width; + } + else + { + cellBounds.X = bounds.X + cx; + } + + // Paint the data with the the DataGridColumn + Brush backBr = BackBrushForDataPaint(ref current, columns[col], col); + Brush foreBrush = ForeBrushForDataPaint(ref current, columns[col], col); + + PaintCellContents(g, + cellBounds, + columns[col], + backBr, + foreBrush, + alignToRight); + + // Paint the border to the right of each cell + if (bWidth > 0) + { + g.FillRectangle(dgTable.IsDefault ? DataGrid.GridLineBrush : dgTable.GridLineBrush, + alignToRight ? cellBounds.X - bWidth : cellBounds.Right, + cellBounds.Y, + bWidth, + cellBounds.Height); + } + + cx += cellBounds.Width + bWidth; + } + + // Paint any exposed area to the right ( or left ) of the data cell area + if (cx < bounds.Width) + { + g.FillRectangle(dgTable.DataGrid.BackgroundBrush, + alignToRight ? bounds.X : bounds.X + cx, + bounds.Y, + bounds.Width - cx, + bounds.Height); + } + + return cx; + } + + protected virtual void PaintCellContents(Graphics g, Rectangle cellBounds, DataGridColumnStyle column, + Brush backBr, Brush foreBrush) + { + PaintCellContents(g, cellBounds, column, backBr, foreBrush, false); + } + + protected virtual void PaintCellContents(Graphics g, Rectangle cellBounds, DataGridColumnStyle column, + Brush backBr, Brush foreBrush, bool alignToRight) + { + g.FillRectangle(backBr, cellBounds); + } + + // + // This function will do the following: if paintIcon is set to true, then + // will draw the image on the RowHeader. if paintIcon is set to false, + // then this function will fill the rectangle on which otherwise will + // have been drawn the image + // + // will return the rectangle that includes the Icon + // + protected Rectangle PaintIcon(Graphics g, Rectangle visualBounds, bool paintIcon, bool alignToRight, Bitmap bmp) + { + return PaintIcon(g, visualBounds, paintIcon, alignToRight, bmp, + dgTable.IsDefault ? DataGrid.HeaderBackBrush : dgTable.HeaderBackBrush); + } + + protected Rectangle PaintIcon(Graphics g, Rectangle visualBounds, bool paintIcon, bool alignToRight, Bitmap bmp, Brush backBrush) + { + Size bmpSize = bmp.Size; + Rectangle bmpRect = new Rectangle(alignToRight ? visualBounds.Right - xOffset - bmpSize.Width : visualBounds.X + xOffset, + visualBounds.Y + yOffset, + bmpSize.Width, + bmpSize.Height); + g.FillRectangle(backBrush, visualBounds); + if (paintIcon) + { + colorMap[0].NewColor = dgTable.IsDefault ? DataGrid.HeaderForeColor : dgTable.HeaderForeColor; + colorMap[0].OldColor = Color.Black; + ImageAttributes attr = new ImageAttributes(); + attr.SetRemapTable(colorMap, ColorAdjustType.Bitmap); + g.DrawImage(bmp, bmpRect, 0, 0, bmpRect.Width, bmpRect.Height, GraphicsUnit.Pixel, attr); + // g.DrawImage(bmp, bmpRect); + attr.Dispose(); + } + + return bmpRect; + } + + // assume that the row is not aligned to right, and that the row is not dirty + public virtual void PaintHeader(Graphics g, Rectangle visualBounds) + { + PaintHeader(g, visualBounds, false); + } + + // assume that the row is not dirty + public virtual void PaintHeader(Graphics g, Rectangle visualBounds, bool alignToRight) + { + PaintHeader(g, visualBounds, alignToRight, false); + } + + public virtual void PaintHeader(Graphics g, Rectangle visualBounds, bool alignToRight, bool rowIsDirty) + { + Rectangle bounds = visualBounds; + + // paint the first part of the row header: the Arror or Pencil/Star + Bitmap bmp; + if (this is DataGridAddNewRow) + { + bmp = GetStarBitmap(); + lock (bmp) + { + bounds.X += PaintIcon(g, bounds, true, alignToRight, bmp).Width + xOffset; + } + + return; + } + else if (rowIsDirty) + { + bmp = GetPencilBitmap(); + lock (bmp) + { + bounds.X += PaintIcon(g, bounds, RowNumber == DataGrid.CurrentCell.RowNumber, alignToRight, bmp).Width + xOffset; + } + } + else + { + bmp = alignToRight ? GetLeftArrowBitmap() : GetRightArrowBitmap(); + lock (bmp) + { + bounds.X += PaintIcon(g, bounds, RowNumber == DataGrid.CurrentCell.RowNumber, alignToRight, bmp).Width + xOffset; + } + } + + // Paint the error icon + // + object errorInfo = DataGrid.ListManager[number]; + if (!(errorInfo is IDataErrorInfo)) + { + return; + } + + string errString = ((IDataErrorInfo)errorInfo).Error; + if (errString == null) + { + errString = string.Empty; + } + + if (tooltip != errString) + { + if (!string.IsNullOrEmpty(tooltip)) + { + DataGrid.ToolTipProvider.RemoveToolTip(tooltipID); + tooltip = string.Empty; + tooltipID = new IntPtr(-1); + } + } + + if (string.IsNullOrEmpty(errString)) + { + return; + } + + // we now have an error string: paint the errorIcon and add the tooltip + Rectangle errRect; + bmp = GetErrorBitmap(); + lock (bmp) + { + errRect = PaintIcon(g, bounds, true, alignToRight, bmp); + } + + bounds.X += errRect.Width + xOffset; + + tooltip = errString; + tooltipID = (IntPtr)((int)DataGrid.ToolTipId++); + DataGrid.ToolTipProvider.AddToolTip(tooltip, tooltipID, errRect); + } + + protected Brush GetBackBrush() + { + Brush br = dgTable.IsDefault ? DataGrid.BackBrush : dgTable.BackBrush; + if (DataGrid.LedgerStyle && (RowNumber % 2 == 1)) + { + br = dgTable.IsDefault ? DataGrid.AlternatingBackBrush : dgTable.AlternatingBackBrush; + } + + return br; + } + + /// + /// Returns the BackColor and TextColor that the Graphics object should use + /// for the appropriate values for a given row and column when painting the data. + /// + protected Brush BackBrushForDataPaint(ref DataGridCell current, DataGridColumnStyle gridColumn, int column) + { + Brush backBr = GetBackBrush(); + + if (Selected) + { + backBr = dgTable.IsDefault ? DataGrid.SelectionBackBrush : dgTable.SelectionBackBrush; + } + + /* + if (RowNumber == current.RowNumber && column == current.ColumnNumber) { + backBr = grid.CurrentCellBackBrush; + } + */ + return backBr; + } + + protected Brush ForeBrushForDataPaint(ref DataGridCell current, DataGridColumnStyle gridColumn, int column) + { + // Brush foreBrush = gridColumn.ForeBrush; + Brush foreBrush = dgTable.IsDefault ? DataGrid.ForeBrush : dgTable.ForeBrush; + + if (Selected) + { + foreBrush = dgTable.IsDefault ? DataGrid.SelectionForeBrush : dgTable.SelectionForeBrush; + } + + /* + if (RowNumber == current.RowNumber && column == current.ColumnNumber) { + foreColor = grid.CurrentCellForeColor; + } + */ + return foreBrush; + } + + [ComVisible(true)] + protected class DataGridRowAccessibleObject : AccessibleObject + { + ArrayList cells; + readonly DataGridRow owner; + + internal static string CellToDisplayString(DataGrid grid, int row, int column) + { + if (column < grid.myGridTable.GridColumnStyles.Count) + { + return grid.myGridTable.GridColumnStyles[column].PropertyDescriptor.Converter.ConvertToString(grid[row, column]); + } + else + { + return ""; + } + } + + internal static object DisplayStringToCell(DataGrid grid, int row, int column, string value) + { + if (column < grid.myGridTable.GridColumnStyles.Count) + { + return grid.myGridTable.GridColumnStyles[column].PropertyDescriptor.Converter.ConvertFromString(value); + } + + // ignore... + // + return null; + } + + public DataGridRowAccessibleObject(DataGridRow owner) : base() + { + Debug.Assert(owner != null, "DataGridRowAccessibleObject must have a valid owner DataGridRow"); + this.owner = owner; + DataGrid grid = DataGrid; + Debug.WriteLineIf(DataGrid.DataGridAcc.TraceVerbose, "Create row accessible object"); + + EnsureChildren(); + } + + private void EnsureChildren() + { + if (cells == null) + { + // default size... little extra for relationships... + // + cells = new ArrayList(DataGrid.myGridTable.GridColumnStyles.Count + 2); + AddChildAccessibleObjects(cells); + } + } + + protected virtual void AddChildAccessibleObjects(IList children) + { + Debug.WriteLineIf(DataGrid.DataGridAcc.TraceVerbose, "Create row's accessible children"); + Debug.Indent(); + GridColumnStylesCollection cols = DataGrid.myGridTable.GridColumnStyles; + int len = cols.Count; + Debug.WriteLineIf(DataGrid.DataGridAcc.TraceVerbose, len + " columns present"); + for (int i = 0; i < len; i++) + { + children.Add(CreateCellAccessibleObject(i)); + } + + Debug.Unindent(); + } + + protected virtual AccessibleObject CreateCellAccessibleObject(int column) + { + return new DataGridCellAccessibleObject(owner, column); + } + + public override Rectangle Bounds + { + get + { + return DataGrid.RectangleToScreen(DataGrid.GetRowBounds(owner)); + } + } + + public override string Name + { + get + { + if (owner is DataGridAddNewRow) + { + return SR.AccDGNewRow; + } + else + { + return DataGridRowAccessibleObject.CellToDisplayString(DataGrid, owner.RowNumber, 0); + } + } + } + + protected DataGridRow Owner + { + get + { + return owner; + } + } + + public override AccessibleObject Parent + { + get + { + return DataGrid.AccessibilityObject; + } + } + + private DataGrid DataGrid + { + get + { + return owner.DataGrid; + } + } + + public override AccessibleRole Role + { + get + { + return AccessibleRole.Row; + } + } + + public override AccessibleStates State + { + get + { + AccessibleStates state = AccessibleStates.Selectable | AccessibleStates.Focusable; + + // Determine focus + // + if (DataGrid.CurrentCell.RowNumber == owner.RowNumber) + { + state |= AccessibleStates.Focused; + } + + // Determine selected + // + if (DataGrid.CurrentRowIndex == owner.RowNumber) + { + state |= AccessibleStates.Selected; + } + + return state; + } + } + + public override string Value + { + get + { + return Name; + } + } + + public override AccessibleObject GetChild(int index) + { + if (index < cells.Count) + { + return (AccessibleObject)cells[index]; + } + + return null; + } + + public override int GetChildCount() + { + return cells.Count; + } + + /// + /// Returns the currently focused child, if any. + /// Returns this if the object itself is focused. + /// + public override AccessibleObject GetFocused() + { + if (DataGrid.Focused) + { + DataGridCell cell = DataGrid.CurrentCell; + if (cell.RowNumber == owner.RowNumber) + { + return (AccessibleObject)cells[cell.ColumnNumber]; + } + } + + return null; + } + + /// + /// Navigate to the next or previous grid entry.entry. + /// + public override AccessibleObject Navigate(AccessibleNavigation navdir) + { + switch (navdir) + { + case AccessibleNavigation.Down: + case AccessibleNavigation.Right: + case AccessibleNavigation.Next: + return DataGrid.AccessibilityObject.GetChild(1 + owner.dgTable.GridColumnStyles.Count + owner.RowNumber + 1); + + case AccessibleNavigation.Up: + case AccessibleNavigation.Left: + case AccessibleNavigation.Previous: + return DataGrid.AccessibilityObject.GetChild(1 + owner.dgTable.GridColumnStyles.Count + owner.RowNumber - 1); + + case AccessibleNavigation.FirstChild: + if (GetChildCount() > 0) + { + return GetChild(0); + } + + break; + case AccessibleNavigation.LastChild: + if (GetChildCount() > 0) + { + return GetChild(GetChildCount() - 1); + } + + break; + } + + return null; + } + + public override void Select(AccessibleSelection flags) + { + // Focus the PropertyGridView window + // + if ((flags & AccessibleSelection.TakeFocus) == AccessibleSelection.TakeFocus) + { + DataGrid.Focus(); + } + + // Select the grid entry + // + if ((flags & AccessibleSelection.TakeSelection) == AccessibleSelection.TakeSelection) + { + DataGrid.CurrentRowIndex = owner.RowNumber; + } + } + } + + [ComVisible(true)] + protected class DataGridCellAccessibleObject : AccessibleObject + { + readonly DataGridRow owner; + readonly int column; + + public DataGridCellAccessibleObject(DataGridRow owner, int column) : base() + { + Debug.Assert(owner != null, "DataGridColumnAccessibleObject must have a valid owner DataGridRow"); + this.owner = owner; + this.column = column; + Debug.WriteLineIf(DataGrid.DataGridAcc.TraceVerbose, "Create cell accessible object"); + } + + public override Rectangle Bounds + { + get + { + return DataGrid.RectangleToScreen(DataGrid.GetCellBounds(new DataGridCell(owner.RowNumber, column))); + } + } + + public override string Name + { + get + { + return DataGrid.myGridTable.GridColumnStyles[column].HeaderText; + } + } + + public override AccessibleObject Parent + { + get + { + return owner.AccessibleObject; + } + } + + protected DataGrid DataGrid + { + get + { + return owner.DataGrid; + } + } + + public override string DefaultAction + { + get + { + return SR.AccDGEdit; + } + } + + public override AccessibleRole Role + { + get + { + return AccessibleRole.Cell; + } + } + + public override AccessibleStates State + { + get + { + AccessibleStates state = AccessibleStates.Selectable | AccessibleStates.Focusable; + + // Determine focus + // + if (DataGrid.CurrentCell.RowNumber == owner.RowNumber + && DataGrid.CurrentCell.ColumnNumber == column) + { + if (DataGrid.Focused) + { + state |= AccessibleStates.Focused; + } + + state |= AccessibleStates.Selected; + } + + return state; + } + } + + public override string Value + { + get + { + if (owner is DataGridAddNewRow) + { + return null; + } + else + { + return DataGridRowAccessibleObject.CellToDisplayString(DataGrid, owner.RowNumber, column); + } + } + + set + { + if (!(owner is DataGridAddNewRow)) + { + object realValue = DataGridRowAccessibleObject.DisplayStringToCell(DataGrid, owner.RowNumber, column, value); + DataGrid[owner.RowNumber, column] = realValue; + } + } + } + + public override void DoDefaultAction() + { + Select(AccessibleSelection.TakeFocus | AccessibleSelection.TakeSelection); + } + + /// + /// Returns the currently focused child, if any. + /// Returns this if the object itself is focused. + /// + public override AccessibleObject GetFocused() + { + // Datagrid always returns the cell as the focused thing... so do we! + // + return DataGrid.AccessibilityObject.GetFocused(); + } + + /// + /// Navigate to the next or previous grid entry. + /// + public override AccessibleObject Navigate(AccessibleNavigation navdir) + { + switch (navdir) + { + case AccessibleNavigation.Right: + case AccessibleNavigation.Next: + if (column < owner.AccessibleObject.GetChildCount() - 1) + { + return owner.AccessibleObject.GetChild(column + 1); + } + else + { + AccessibleObject o = DataGrid.AccessibilityObject.GetChild(1 + owner.dgTable.GridColumnStyles.Count + owner.RowNumber + 1); + if (o != null) + { + return o.Navigate(AccessibleNavigation.FirstChild); + } + } + + break; + case AccessibleNavigation.Down: + return DataGrid.AccessibilityObject.GetChild(1 + owner.dgTable.GridColumnStyles.Count + owner.RowNumber + 1).Navigate(AccessibleNavigation.FirstChild); + case AccessibleNavigation.Up: + return DataGrid.AccessibilityObject.GetChild(1 + owner.dgTable.GridColumnStyles.Count + owner.RowNumber - 1).Navigate(AccessibleNavigation.FirstChild); + case AccessibleNavigation.Left: + case AccessibleNavigation.Previous: + if (column > 0) + { + return owner.AccessibleObject.GetChild(column - 1); + } + else + { + AccessibleObject o = DataGrid.AccessibilityObject.GetChild(1 + owner.dgTable.GridColumnStyles.Count + owner.RowNumber - 1); + if (o != null) + { + return o.Navigate(AccessibleNavigation.LastChild); + } + } + + break; + + case AccessibleNavigation.FirstChild: + case AccessibleNavigation.LastChild: + + break; + } + + return null; + } + + public override void Select(AccessibleSelection flags) + { + // Focus the PropertyGridView window + // + if ((flags & AccessibleSelection.TakeFocus) == AccessibleSelection.TakeFocus) + { + DataGrid.Focus(); + } + + // Select the grid entry + // + if ((flags & AccessibleSelection.TakeSelection) == AccessibleSelection.TakeSelection) + { + DataGrid.CurrentCell = new DataGridCell(owner.RowNumber, column); + } + } + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridState.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridState.cs new file mode 100644 index 00000000000..ec1d23a50ca --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridState.cs @@ -0,0 +1,261 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1510, CSIsNull001, CSIsNull002, IDE0301, IDE0031, IDE0066 + +#nullable disable + +using System.Drawing; +using System.Runtime.InteropServices; +using System.Text; + +namespace System.Windows.Forms; + /// + /// Encapsulates the state of a DataGrid that changes when the + /// user navigates back and forth through ADO.NET data relations. + /// + internal sealed class DataGridState : ICloneable + { + // fields + // + public object DataSource; + public string DataMember; + public CurrencyManager ListManager; + public DataGridRow[] DataGridRows = Array.Empty(); + public DataGrid DataGrid; + public int DataGridRowsLength; + public GridColumnStylesCollection GridColumnStyles; + + public int FirstVisibleRow; + public int FirstVisibleCol; + + public int CurrentRow; + public int CurrentCol; + + public DataGridRow LinkingRow; + AccessibleObject parentRowAccessibleObject; + + public DataGridState() + { + } + + public DataGridState(DataGrid dataGrid) + { + PushState(dataGrid); + } + + internal AccessibleObject ParentRowAccessibleObject + { + get + { + if (parentRowAccessibleObject == null) + { + parentRowAccessibleObject = new DataGridStateParentRowAccessibleObject(this); + } + + return parentRowAccessibleObject; + } + } + + // methods + // + + public object Clone() + { + DataGridState dgs = new DataGridState + { + DataGridRows = DataGridRows, + DataSource = DataSource, + DataMember = DataMember, + FirstVisibleRow = FirstVisibleRow, + FirstVisibleCol = FirstVisibleCol, + CurrentRow = CurrentRow, + CurrentCol = CurrentCol, + GridColumnStyles = GridColumnStyles, + ListManager = ListManager, + DataGrid = DataGrid + }; + return dgs; + } + + /// + /// Called by a DataGrid when it wishes to preserve its + /// transient state in the current DataGridState object. + /// + public void PushState(DataGrid dataGrid) + { + DataSource = dataGrid.DataSource; + DataMember = dataGrid.DataMember; + DataGrid = dataGrid; + DataGridRows = dataGrid.DataGridRows; + DataGridRowsLength = dataGrid.DataGridRowsLength; + FirstVisibleRow = dataGrid.firstVisibleRow; + FirstVisibleCol = dataGrid.firstVisibleCol; + CurrentRow = dataGrid.currentRow; + GridColumnStyles = new GridColumnStylesCollection(dataGrid.myGridTable); + + GridColumnStyles.Clear(); + foreach (DataGridColumnStyle style in dataGrid.myGridTable.GridColumnStyles) + { + GridColumnStyles.Add(style); + } + + ListManager = dataGrid.ListManager; + ListManager.ItemChanged += new ItemChangedEventHandler(DataSource_Changed); + ListManager.MetaDataChanged += new EventHandler(DataSource_MetaDataChanged); + CurrentCol = dataGrid.currentCol; + } + + // this is needed so that the parent rows will remove notification from the list + // when the datagridstate is no longer needed; + public void RemoveChangeNotification() + { + ListManager.ItemChanged -= new ItemChangedEventHandler(DataSource_Changed); + ListManager.MetaDataChanged -= new EventHandler(DataSource_MetaDataChanged); + } + + /// + /// Called by a grid when it wishes to match its transient + /// state with the current DataGridState object. + /// + public void PullState(DataGrid dataGrid, bool createColumn) + { + // dataGrid.DataSource = DataSource; + // dataGrid.DataMember = DataMember; + dataGrid.Set_ListManager(DataSource, DataMember, true, createColumn); // true for forcing new listManager, + + /* + if (DataSource.Table.ParentRelations.Count > 0) + dataGrid.PopulateColumns(); + */ + + dataGrid.firstVisibleRow = FirstVisibleRow; + dataGrid.firstVisibleCol = FirstVisibleCol; + dataGrid.currentRow = CurrentRow; + dataGrid.currentCol = CurrentCol; + dataGrid.SetDataGridRows(DataGridRows, DataGridRowsLength); + } + + private void DataSource_Changed(object sender, ItemChangedEventArgs e) + { + if (DataGrid != null && ListManager.Position == e.Index) + { + DataGrid.InvalidateParentRows(); + return; + } + + if (DataGrid != null) + { + DataGrid.ParentRowsDataChanged(); + } + } + + private void DataSource_MetaDataChanged(object sender, EventArgs e) + { + if (DataGrid != null) + { + DataGrid.ParentRowsDataChanged(); + } + } + + [ComVisible(true)] + internal class DataGridStateParentRowAccessibleObject : AccessibleObject + { + readonly DataGridState owner; + + public DataGridStateParentRowAccessibleObject(DataGridState owner) : base() + { + Debug.Assert(owner != null, "DataGridRowAccessibleObject must have a valid owner DataGridRow"); + this.owner = owner; + } + + public override Rectangle Bounds + { + get + { + DataGridParentRows dataGridParentRows = ((DataGridParentRows.DataGridParentRowsAccessibleObject)Parent).Owner; + DataGrid g = owner.LinkingRow.DataGrid; + Rectangle r = dataGridParentRows.GetBoundsForDataGridStateAccesibility(owner); + r.Y += g.ParentRowsBounds.Y; + return g.RectangleToScreen(r); + } + } + + public override string Name + { + get + { + return SR.AccDGParentRow; + } + } + + public override AccessibleObject Parent + { + get + { + return owner.LinkingRow.DataGrid.ParentRowsAccessibleObject; + } + } + + public override AccessibleRole Role + { + get + { + return AccessibleRole.ListItem; + } + } + + public override string Value + { + get + { + StringBuilder sb = new StringBuilder(); + + CurrencyManager source = (CurrencyManager)owner.LinkingRow.DataGrid.BindingContext[owner.DataSource, owner.DataMember]; + + sb.Append(owner.ListManager.GetListName()); + sb.Append(": "); + + bool needComma = false; + foreach (DataGridColumnStyle col in owner.GridColumnStyles) + { + if (needComma) + { + sb.Append(", "); + } + + string colName = col.HeaderText; + string cellValue = col.PropertyDescriptor.Converter.ConvertToString(col.PropertyDescriptor.GetValue(source.Current)); + sb.Append(colName); + sb.Append(": "); + sb.Append(cellValue); + needComma = true; + } + + return sb.ToString(); + } + } + + /// + /// Navigate to the next or previous grid entry. + /// + public override AccessibleObject Navigate(AccessibleNavigation navdir) + { + DataGridParentRows.DataGridParentRowsAccessibleObject parentAcc = (DataGridParentRows.DataGridParentRowsAccessibleObject)Parent; + + switch (navdir) + { + case AccessibleNavigation.Down: + case AccessibleNavigation.Right: + case AccessibleNavigation.Next: + return parentAcc.GetNext(this); + case AccessibleNavigation.Up: + case AccessibleNavigation.Left: + case AccessibleNavigation.Previous: + return parentAcc.GetPrev(this); + } + + return null; + } + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridStrings.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridStrings.cs new file mode 100644 index 00000000000..85ccccf90ad --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridStrings.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable SA1518 + +namespace System.Windows.Forms; + +internal static class DataGridStrings +{ + public static string DataGridCaptionBackButtonToolTip => "Back"; + public static string DataGridCaptionDetailsButtonToolTip => "Details"; + public static string DataGridColumnCollectionGetEnumerator => "Enumeration is not supported for this collection."; + public static string DataGridColumnCollectionMissing => "The specified column does not belong to this collection."; + public static string DataGridColumnListManagerPosition => "The specified row index is not valid for the current list manager position."; + public static string DataGridColumnNoPropertyDescriptor => "The column is not bound to a property descriptor."; + public static string DataGridColumnStyleDuplicateMappingName => "A column with the same mapping name already exists."; + public static string DataGridColumnUnbound => "The column '{0}' is not bound to a data source."; + public static string DataGridColumnWidth => "PreferredColumnWidth must be greater than or equal to zero."; + public static string DataGridDefaultColumnCollectionChanged => "The default column collection cannot be modified."; + public static string DataGridDefaultTableSet => "The property '{0}' cannot be set on the default table style."; + public static string DataGridEmptyColor => "The color specified for '{0}' must not be empty."; + public static string DataGridErrorMessageBoxCaption => "DataGrid"; + public static string DataGridExceptionInPaint => "An exception occurred while painting the DataGrid."; + public static string DataGridBeginInit => "EndInit must be called before this operation can continue."; + public static string DataGridNullText => "(null)"; + public static string DataGridPushedIncorrectValueIntoColumn => "The value pushed into the column is invalid: {0}"; + public static string DataGridRowRowHeight => "PreferredRowHeight must be greater than zero."; + public static string DataGridRowRowNumber => "The row number must be greater than or equal to zero."; + public static string DataGridSetListManager => "The current data source cannot be assigned to this DataGrid."; + public static string DataGridSetSelectIndex => "The current row cannot be selected."; + public static string DataGridSettingCurrentCellNotGood => "The current cell could not be set to the requested location."; + public static string DataGridTableCollectionGetEnumerator => "Enumeration is not supported for this collection."; + public static string DataGridTableCollectionMissingTable => "The specified table style does not belong to this collection."; + public static string DataGridTableStyleCollectionAddedParentedTableStyle => "The table style already belongs to a DataGrid."; + public static string DataGridTableStyleDuplicateMappingName => "A table style with the same mapping name already exists."; + public static string DataGridTableStyleTransparentAlternatingBackColorNotAllowed => "AlternatingBackColor cannot be transparent."; + public static string DataGridTableStyleTransparentBackColorNotAllowed => "BackColor cannot be transparent."; + public static string DataGridTableStyleTransparentHeaderBackColorNotAllowed => "HeaderBackColor cannot be transparent."; + public static string DataGridTableStyleTransparentSelectionBackColorNotAllowed => "SelectionBackColor cannot be transparent."; + public static string DataGridToolTipEmptyIcon => "The tooltip icon bounds must not be empty."; + public static string DataGridTransparentAlternatingBackColorNotAllowed => "AlternatingBackColor cannot be transparent."; + public static string DataGridTransparentBackColorNotAllowed => "BackColor cannot be transparent."; + public static string DataGridTransparentCaptionBackColorNotAllowed => "CaptionBackColor cannot be transparent."; + public static string DataGridTransparentHeaderBackColorNotAllowed => "HeaderBackColor cannot be transparent."; + public static string DataGridTransparentParentRowsBackColorNotAllowed => "ParentRowsBackColor cannot be transparent."; + public static string DataGridTransparentSelectionBackColorNotAllowed => "SelectionBackColor cannot be transparent."; + public static string DataGridUnbound => "The DataGrid is not bound to a data source."; +} \ No newline at end of file diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTableStyle.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTableStyle.cs index 30c1cbd0f1a..f3e3261be8e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTableStyle.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTableStyle.cs @@ -1,394 +1,1794 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; -using System.Drawing; -using System.Drawing.Design; - -namespace System.Windows.Forms; +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, WFDEV006, IDE0036, SA1507, IDE0031, IDE0078, IDE0059, RS0016 #nullable disable -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[ToolboxItem(false)] -[DesignTimeVisible(false)] -public class DataGridTableStyle : Component, IDataGridEditingService -{ - public DataGridTableStyle() : this(isDefaultTableStyle: false) => throw new PlatformNotSupportedException(); - - public DataGridTableStyle(bool isDefaultTableStyle) => throw new PlatformNotSupportedException(); - - public DataGridTableStyle(CurrencyManager listManager) : this() => throw new PlatformNotSupportedException(); - - [DefaultValue(true)] - public bool AllowSorting - { - get => throw null; - set { } - } - - public event EventHandler AllowSortingChanged - { - add { } - remove { } - } - - public Color AlternatingBackColor - { - get => throw null; - set { } - } - - public event EventHandler AlternatingBackColorChanged - { - add { } - remove { } - } - - public void ResetAlternatingBackColor() { } - - protected virtual bool ShouldSerializeAlternatingBackColor() => throw null; - - protected bool ShouldSerializeBackColor() => throw null; - - protected bool ShouldSerializeForeColor() => throw null; - - public Color BackColor - { - get => throw null; - set { } - } - - public event EventHandler BackColorChanged - { - add { } - remove { } - } - - public void ResetBackColor() { } - - public Color ForeColor - { - get => throw null; - set { } - } - - public event EventHandler ForeColorChanged - { - add { } - remove { } - } - - public void ResetForeColor() { } - - public Color GridLineColor - { - get => throw null; - set { } - } - - public event EventHandler GridLineColorChanged - { - add { } - remove { } - } - - protected virtual bool ShouldSerializeGridLineColor() => throw null; - - public void ResetGridLineColor() { } - - [DefaultValue(DataGridLineStyle.Solid)] - public DataGridLineStyle GridLineStyle - { - get => throw null; - set { } - } - - public event EventHandler GridLineStyleChanged - { - add { } - remove { } - } - - public Color HeaderBackColor - { - get => throw null; - set { } - } - - public event EventHandler HeaderBackColorChanged - { - add { } - remove { } - } - - protected virtual bool ShouldSerializeHeaderBackColor() => throw null; - - public void ResetHeaderBackColor() { } - - [Localizable(true)] - [AmbientValue(null)] - public Font HeaderFont - { - get => throw null; - set { } - } - - public event EventHandler HeaderFontChanged - { - add { } - remove { } - } - - public void ResetHeaderFont() { } - - public Color HeaderForeColor - { - get => throw null; - set { } - } - - public event EventHandler HeaderForeColorChanged - { - add { } - remove { } - } - - protected virtual bool ShouldSerializeHeaderForeColor() => throw null; - - public void ResetHeaderForeColor() { } - - public Color LinkColor - { - get => throw null; - set { } - } - - public event EventHandler LinkColorChanged - { - add { } - remove { } - } - - protected virtual bool ShouldSerializeLinkColor() => throw null; - - public void ResetLinkColor() { } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public Color LinkHoverColor - { - get => throw null; - set { } - } - - public event EventHandler LinkHoverColorChanged - { - add { } - remove { } - } - - protected virtual bool ShouldSerializeLinkHoverColor() => throw null; - - public void ResetLinkHoverColor() { } +using DSR = System.Windows.Forms.DataGridStrings; - [DefaultValue(75)] - [Localizable(true)] - [TypeConverter(typeof(DataGridPreferredColumnWidthTypeConverter))] - public int PreferredColumnWidth - { - get => throw null; - set { } - } - - public event EventHandler PreferredColumnWidthChanged - { - add { } - remove { } - } - - [Localizable(true)] - public int PreferredRowHeight - { - get => throw null; - set { } - } - - public event EventHandler PreferredRowHeightChanged - { - add { } - remove { } - } - - protected bool ShouldSerializePreferredRowHeight() => throw null; - - [DefaultValue(true)] - public bool ColumnHeadersVisible - { - get => throw null; - set { } - } - - public event EventHandler ColumnHeadersVisibleChanged - { - add { } - remove { } - } - - [DefaultValue(true)] - public bool RowHeadersVisible - { - get => throw null; - set { } - } - - public event EventHandler RowHeadersVisibleChanged - { - add { } - remove { } - } - - [DefaultValue(35)] - [Localizable(true)] - public int RowHeaderWidth - { - get => throw null; - set { } - } - - public event EventHandler RowHeaderWidthChanged - { - add { } - remove { } - } - - public Color SelectionBackColor - { - get => throw null; - set { } - } - - public event EventHandler SelectionBackColorChanged - { - add { } - remove { } - } - - protected bool ShouldSerializeSelectionBackColor() => throw null; - - public void ResetSelectionBackColor() { } - - [Description("The foreground color for the current data grid row")] - public Color SelectionForeColor - { - get => throw null; - set { } - } - - public event EventHandler SelectionForeColorChanged - { - add { } - remove { } - } - - protected virtual bool ShouldSerializeSelectionForeColor() => throw null; - - public void ResetSelectionForeColor() { } - - public static readonly DataGridTableStyle DefaultTableStyle; - - [Editor($"System.Windows.Forms.Design.DataGridTableStyleMappingNameEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))] - [DefaultValue("")] - public string MappingName - { - get => throw null; - set { } - } - - public event EventHandler MappingNameChanged - { - add { } - remove { } - } - - [Localizable(true)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] - public virtual GridColumnStylesCollection GridColumnStyles => throw null; - [Browsable(false)] - public virtual DataGrid DataGrid - { - get => throw null; - set { } - } - - [DefaultValue(false)] - public virtual bool ReadOnly - { - get => throw null; - set { } - } +using System.Collections; +using System.ComponentModel; +using System.Drawing; - public event EventHandler ReadOnlyChanged +namespace System.Windows.Forms; + /// + /// Represents the table drawn by the control at run time. + /// + [ + ToolboxItem(false), + DesignTimeVisible(false), + //DefaultProperty("GridTableName") + ] + public class DataGridTableStyle : Component, IDataGridEditingService { - add { } - remove { } + // internal for DataGridColumn accessibility... + // + internal DataGrid dataGrid; + + // relationship UI + private int relationshipHeight; + internal const int relationshipSpacing = 1; + private Rectangle relationshipRect = Rectangle.Empty; + private int focusedRelation = -1; + private int focusedTextWidth; + + // will contain a list of relationships that this table has + private readonly ArrayList relationsList = new ArrayList(2); + + // the name of the table + private string mappingName = string.Empty; + private readonly GridColumnStylesCollection gridColumns; + private bool readOnly; + private readonly bool isDefaultTableStyle; + + private static readonly object EventAllowSorting = new object(); + private static readonly object EventGridLineColor = new object(); + private static readonly object EventGridLineStyle = new object(); + private static readonly object EventHeaderBackColor = new object(); + private static readonly object EventHeaderForeColor = new object(); + private static readonly object EventHeaderFont = new object(); + private static readonly object EventLinkColor = new object(); + private static readonly object EventLinkHoverColor = new object(); + private static readonly object EventPreferredColumnWidth = new object(); + private static readonly object EventPreferredRowHeight = new object(); + private static readonly object EventColumnHeadersVisible = new object(); + private static readonly object EventRowHeaderWidth = new object(); + private static readonly object EventSelectionBackColor = new object(); + private static readonly object EventSelectionForeColor = new object(); + private static readonly object EventMappingName = new object(); + private static readonly object EventAlternatingBackColor = new object(); + private static readonly object EventBackColor = new object(); + private static readonly object EventForeColor = new object(); + private static readonly object EventReadOnly = new object(); + private static readonly object EventRowHeadersVisible = new object(); + + // add a bunch of properties, taken from the dataGrid + // + + // default values + // + private const bool defaultAllowSorting = true; + private const DataGridLineStyle defaultGridLineStyle = DataGridLineStyle.Solid; + private const int defaultPreferredColumnWidth = 75; + private const int defaultRowHeaderWidth = 35; + internal static readonly Font defaultFont = Control.DefaultFont; + internal static readonly int defaultFontHeight = defaultFont.Height; + + // the actual place holders for properties + // + private bool allowSorting = defaultAllowSorting; + private SolidBrush alternatingBackBrush = DefaultAlternatingBackBrush; + private SolidBrush backBrush = DefaultBackBrush; + private SolidBrush foreBrush = DefaultForeBrush; + private SolidBrush gridLineBrush = DefaultGridLineBrush; + private DataGridLineStyle gridLineStyle = defaultGridLineStyle; + internal SolidBrush headerBackBrush = DefaultHeaderBackBrush; + internal Font headerFont; // this is ambient property to Font value. + internal SolidBrush headerForeBrush = DefaultHeaderForeBrush; + internal Pen headerForePen = DefaultHeaderForePen; + private SolidBrush linkBrush = DefaultLinkBrush; + internal int preferredColumnWidth = defaultPreferredColumnWidth; + private int preferredRowHeight = defaultFontHeight + 3; + private SolidBrush selectionBackBrush = DefaultSelectionBackBrush; + private SolidBrush selectionForeBrush = DefaultSelectionForeBrush; + private int rowHeaderWidth = defaultRowHeaderWidth; + private bool rowHeadersVisible = true; + private bool columnHeadersVisible = true; + + // the dataGrid would need to know when the ColumnHeaderVisible, RowHeadersVisible, RowHeaderWidth + // and preferredColumnWidth, preferredRowHeight properties are changed in the current dataGridTableStyle + // also: for GridLineStyle, GridLineColor, ForeColor, BackColor, HeaderBackColor, HeaderFont, HeaderForeColor + // LinkColor, LinkHoverColor + // + + [ + SRCategory(nameof(SR.CatBehavior)), + DefaultValue(defaultAllowSorting), + ] + public bool AllowSorting + { + get + { + return allowSorting; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(AllowSorting))); + } + + if (allowSorting != value) + { + allowSorting = value; + OnAllowSortingChanged(EventArgs.Empty); + } + } + } + + /// + /// [To be supplied] + /// + public event EventHandler AllowSortingChanged + { + add => Events.AddHandler(EventAllowSorting, value); + remove => Events.RemoveHandler(EventAllowSorting, value); + } + + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color AlternatingBackColor + { + get + { + return alternatingBackBrush.Color; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(AlternatingBackColor))); + } + + if (System.Windows.Forms.DataGrid.IsTransparentColor(value)) + { + throw new ArgumentException(DSR.DataGridTableStyleTransparentAlternatingBackColorNotAllowed, nameof(value)); + } + + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, nameof(AlternatingBackColor)), nameof(value)); + } + + if (!alternatingBackBrush.Color.Equals(value)) + { + alternatingBackBrush = new SolidBrush(value); + InvalidateInside(); + OnAlternatingBackColorChanged(EventArgs.Empty); + } + } + } + + public event EventHandler AlternatingBackColorChanged + { + add => Events.AddHandler(EventAlternatingBackColor, value); + remove => Events.RemoveHandler(EventAlternatingBackColor, value); + } + + public void ResetAlternatingBackColor() + { + if (ShouldSerializeAlternatingBackColor()) + { + AlternatingBackColor = DefaultAlternatingBackBrush.Color; + InvalidateInside(); + } + } + + protected virtual bool ShouldSerializeAlternatingBackColor() + { + return !AlternatingBackBrush.Equals(DefaultAlternatingBackBrush); + } + + internal SolidBrush AlternatingBackBrush + { + get + { + return alternatingBackBrush; + } + } + + protected bool ShouldSerializeBackColor() + { + return !System.Windows.Forms.DataGridTableStyle.DefaultBackBrush.Equals(backBrush); + } + + protected bool ShouldSerializeForeColor() + { + return !System.Windows.Forms.DataGridTableStyle.DefaultForeBrush.Equals(foreBrush); + } + + internal SolidBrush BackBrush + { + get + { + return backBrush; + } + } + + [ + SRCategory(nameof(SR.CatColors)), + SRDescription(nameof(SR.ControlBackColorDescr)) + ] + public Color BackColor + { + get + { + return backBrush.Color; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(BackColor))); + } + + if (System.Windows.Forms.DataGrid.IsTransparentColor(value)) + { + throw new ArgumentException(DSR.DataGridTableStyleTransparentBackColorNotAllowed, nameof(value)); + } + + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, nameof(BackColor)), nameof(value)); + } + + if (!backBrush.Color.Equals(value)) + { + backBrush = new SolidBrush(value); + InvalidateInside(); + OnBackColorChanged(EventArgs.Empty); + } + } + } + + public event EventHandler BackColorChanged + { + add => Events.AddHandler(EventBackColor, value); + remove => Events.RemoveHandler(EventBackColor, value); + } + + public void ResetBackColor() + { + if (!backBrush.Equals(DefaultBackBrush)) + { + BackColor = DefaultBackBrush.Color; + } + } + + internal int BorderWidth + { + get + { + DataGrid dataGrid = DataGrid; + if (dataGrid is null) + { + return 0; + } + + // if the user set the GridLineStyle property on the dataGrid. + // then use the value of that property + DataGridLineStyle gridStyle; + int gridLineWidth; + if (IsDefault) + { + gridStyle = DataGrid.GridLineStyle; + gridLineWidth = DataGrid.GridLineWidth; + } + else + { + gridStyle = GridLineStyle; + gridLineWidth = GridLineWidth; + } + + if (gridStyle == DataGridLineStyle.None) + { + return 0; + } + + return gridLineWidth; + } + } + + internal static SolidBrush DefaultAlternatingBackBrush + { + get + { + return (SolidBrush)SystemBrushes.Window; + } + } + + internal static SolidBrush DefaultBackBrush + { + get + { + return (SolidBrush)SystemBrushes.Window; + } + } + + internal static SolidBrush DefaultForeBrush + { + get + { + return (SolidBrush)SystemBrushes.WindowText; + } + } + + private static SolidBrush DefaultGridLineBrush + { + get + { + return (SolidBrush)SystemBrushes.Control; + } + } + + private static SolidBrush DefaultHeaderBackBrush + { + get + { + return (SolidBrush)SystemBrushes.Control; + } + } + + private static SolidBrush DefaultHeaderForeBrush + { + get + { + return (SolidBrush)SystemBrushes.ControlText; + } + } + + private static Pen DefaultHeaderForePen + { + get + { + return new Pen(SystemColors.ControlText); + } + } + + private static SolidBrush DefaultLinkBrush + { + get + { + return (SolidBrush)SystemBrushes.HotTrack; + } + } + + private static SolidBrush DefaultSelectionBackBrush + { + get + { + return (SolidBrush)SystemBrushes.ActiveCaption; + } + } + + private static SolidBrush DefaultSelectionForeBrush + { + get + { + return (SolidBrush)SystemBrushes.ActiveCaptionText; + } + } + + internal int FocusedRelation + { + get + { + return focusedRelation; + } + set + { + if (focusedRelation != value) + { + focusedRelation = value; + if (focusedRelation == -1) + { + focusedTextWidth = 0; + } + else + { + Graphics g = DataGrid.CreateGraphicsInternal(); + focusedTextWidth = (int)Math.Ceiling(g.MeasureString(((string)RelationsList[focusedRelation]), DataGrid.LinkFont).Width); + g.Dispose(); + } + } + } + } + + internal int FocusedTextWidth + { + get + { + return focusedTextWidth; + } + } + + [ + SRCategory(nameof(SR.CatColors)), + SRDescription(nameof(SR.ControlForeColorDescr)) + ] + public Color ForeColor + { + get + { + return foreBrush.Color; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(ForeColor))); + } + + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, nameof(ForeColor)), nameof(value)); + } + + if (!foreBrush.Color.Equals(value)) + { + foreBrush = new SolidBrush(value); + InvalidateInside(); + OnForeColorChanged(EventArgs.Empty); + } + } + } + + public event EventHandler ForeColorChanged + { + add => Events.AddHandler(EventForeColor, value); + remove => Events.RemoveHandler(EventForeColor, value); + } + + internal SolidBrush ForeBrush + { + get + { + return foreBrush; + } + } + + public void ResetForeColor() + { + if (!foreBrush.Equals(DefaultForeBrush)) + { + ForeColor = DefaultForeBrush.Color; + } + } + + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color GridLineColor + { + get + { + return gridLineBrush.Color; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(GridLineColor))); + } + + if (gridLineBrush.Color != value) + { + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, nameof(GridLineColor)), nameof(value)); + } + + gridLineBrush = new SolidBrush(value); + OnGridLineColorChanged(EventArgs.Empty); + } + } + } + + public event EventHandler GridLineColorChanged + { + add => Events.AddHandler(EventGridLineColor, value); + remove => Events.RemoveHandler(EventGridLineColor, value); + } + + protected virtual bool ShouldSerializeGridLineColor() + { + return !GridLineBrush.Equals(DefaultGridLineBrush); + } + + public void ResetGridLineColor() + { + if (ShouldSerializeGridLineColor()) + { + GridLineColor = DefaultGridLineBrush.Color; + } + } + + internal SolidBrush GridLineBrush + { + get + { + return gridLineBrush; + } + } + + internal int GridLineWidth + { + get + { + Debug.Assert(GridLineStyle == DataGridLineStyle.Solid || GridLineStyle == DataGridLineStyle.None, "are there any other styles?"); + return GridLineStyle == DataGridLineStyle.Solid ? 1 : 0; + } + } + + [ + SRCategory(nameof(SR.CatAppearance)), + DefaultValue(defaultGridLineStyle), + ] + public DataGridLineStyle GridLineStyle + { + get + { + return gridLineStyle; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(GridLineStyle))); + } + + if (value is < DataGridLineStyle.None or > DataGridLineStyle.Solid) + { + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(DataGridLineStyle)); + } + + if (gridLineStyle != value) + { + gridLineStyle = value; + OnGridLineStyleChanged(EventArgs.Empty); + } + } + } + + public event EventHandler GridLineStyleChanged + { + add => Events.AddHandler(EventGridLineStyle, value); + remove => Events.RemoveHandler(EventGridLineStyle, value); + } + + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color HeaderBackColor + { + get + { + return headerBackBrush.Color; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(HeaderBackColor))); + } + + if (System.Windows.Forms.DataGrid.IsTransparentColor(value)) + { + throw new ArgumentException(DSR.DataGridTableStyleTransparentHeaderBackColorNotAllowed, nameof(value)); + } + + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, nameof(HeaderBackColor)), nameof(value)); + } + + if (!value.Equals(headerBackBrush.Color)) + { + headerBackBrush = new SolidBrush(value); + OnHeaderBackColorChanged(EventArgs.Empty); + } + } + } + + public event EventHandler HeaderBackColorChanged + { + add => Events.AddHandler(EventHeaderBackColor, value); + remove => Events.RemoveHandler(EventHeaderBackColor, value); + } + + internal SolidBrush HeaderBackBrush + { + get + { + return headerBackBrush; + } + } + + protected virtual bool ShouldSerializeHeaderBackColor() + { + return !HeaderBackBrush.Equals(DefaultHeaderBackBrush); + } + + public void ResetHeaderBackColor() + { + if (ShouldSerializeHeaderBackColor()) + { + HeaderBackColor = DefaultHeaderBackBrush.Color; + } + } + + [ + SRCategory(nameof(SR.CatAppearance)), + Localizable(true), + AmbientValue(null), + ] + public Font HeaderFont + { + get + { + return (headerFont ?? (DataGrid is null ? Control.DefaultFont : DataGrid.Font)); + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(HeaderFont))); + } + + if ((value is null && headerFont is not null) || (value is not null && !value.Equals(headerFont))) + { + headerFont = value; + OnHeaderFontChanged(EventArgs.Empty); + } + } + } + + public event EventHandler HeaderFontChanged + { + add => Events.AddHandler(EventHeaderFont, value); + remove => Events.RemoveHandler(EventHeaderFont, value); + } + + private bool ShouldSerializeHeaderFont() + { + return (headerFont is not null); + } + + public void ResetHeaderFont() + { + if (headerFont is not null) + { + headerFont = null; + OnHeaderFontChanged(EventArgs.Empty); + } + } + + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color HeaderForeColor + { + get + { + return headerForePen.Color; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(HeaderForeColor))); + } + + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, nameof(HeaderForeColor)), nameof(value)); + } + + if (!value.Equals(headerForePen.Color)) + { + headerForePen = new Pen(value); + headerForeBrush = new SolidBrush(value); + OnHeaderForeColorChanged(EventArgs.Empty); + } + } + } + + public event EventHandler HeaderForeColorChanged + { + add => Events.AddHandler(EventHeaderForeColor, value); + remove => Events.RemoveHandler(EventHeaderForeColor, value); + } + + protected virtual bool ShouldSerializeHeaderForeColor() + { + return !HeaderForePen.Equals(DefaultHeaderForePen); + } + + public void ResetHeaderForeColor() + { + if (ShouldSerializeHeaderForeColor()) + { + HeaderForeColor = DefaultHeaderForeBrush.Color; + } + } + + internal SolidBrush HeaderForeBrush + { + get + { + return headerForeBrush; + } + } + + internal Pen HeaderForePen + { + get + { + return headerForePen; + } + } + + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color LinkColor + { + get + { + return linkBrush.Color; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(LinkColor))); + } + + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, nameof(LinkColor)), nameof(value)); + } + + if (!linkBrush.Color.Equals(value)) + { + linkBrush = new SolidBrush(value); + OnLinkColorChanged(EventArgs.Empty); + } + } + } + + public event EventHandler LinkColorChanged + { + add => Events.AddHandler(EventLinkColor, value); + remove => Events.RemoveHandler(EventLinkColor, value); + } + + protected virtual bool ShouldSerializeLinkColor() + { + return !LinkBrush.Equals(DefaultLinkBrush); + } + + public void ResetLinkColor() + { + if (ShouldSerializeLinkColor()) + { + LinkColor = DefaultLinkBrush.Color; + } + } + + internal Brush LinkBrush + { + get + { + return linkBrush; + } + } + + [ + SRCategory(nameof(SR.CatColors)), + Browsable(false), + EditorBrowsable(EditorBrowsableState.Never) + ] + public Color LinkHoverColor + { + get + { + return LinkColor; + } + set + { + } + } + + public event EventHandler LinkHoverColorChanged + { + add => Events.AddHandler(EventLinkHoverColor, value); + remove => Events.RemoveHandler(EventLinkHoverColor, value); + } + + protected virtual bool ShouldSerializeLinkHoverColor() + { + return false; + } + + internal Rectangle RelationshipRect + { + get + { + if (relationshipRect.IsEmpty) + { + ComputeRelationshipRect(); + } + + return relationshipRect; + } + } + + private Rectangle ComputeRelationshipRect() + { + if (relationshipRect.IsEmpty && DataGrid.AllowNavigation) + { + Debug.WriteLineIf(CompModSwitches.DGRelationShpRowLayout.TraceVerbose, "GetRelationshipRect grinding away"); + Graphics g = DataGrid.CreateGraphicsInternal(); + relationshipRect = new Rectangle + { + X = 0 //indentWidth; + }; + // relationshipRect.Y = base.Height - BorderWidth; + + // Determine the width of the widest relationship name + int longestRelationship = 0; + for (int r = 0; r < RelationsList.Count; ++r) + { + int rwidth = (int)Math.Ceiling(g.MeasureString(((string)RelationsList[r]), DataGrid.LinkFont).Width) +; + if (rwidth > longestRelationship) + { + longestRelationship = rwidth; + } + } + + g.Dispose(); + + relationshipRect.Width = longestRelationship + 5; + relationshipRect.Width += 2; // relationshipRect border; + relationshipRect.Height = BorderWidth + relationshipHeight * RelationsList.Count; + relationshipRect.Height += 2; // relationship border + if (RelationsList.Count > 0) + { + relationshipRect.Height += 2 * relationshipSpacing; + } + } + + return relationshipRect; + } + + internal void ResetRelationsUI() + { + relationshipRect = Rectangle.Empty; + focusedRelation = -1; + relationshipHeight = dataGrid.LinkFontHeight + relationshipSpacing; + } + + internal int RelationshipHeight + { + get + { + return relationshipHeight; + } + } + + public void ResetLinkHoverColor() + { + } + + [ + DefaultValue(defaultPreferredColumnWidth), + SRCategory(nameof(SR.CatLayout)), + Localizable(true), + TypeConverter(typeof(DataGridPreferredColumnWidthTypeConverter)) + ] + public int PreferredColumnWidth + { + get + { + return preferredColumnWidth; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(PreferredColumnWidth))); + } + + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, DSR.DataGridColumnWidth); + } + + if (preferredColumnWidth != value) + { + preferredColumnWidth = value; + OnPreferredColumnWidthChanged(EventArgs.Empty); + } + } + } + + public event EventHandler PreferredColumnWidthChanged + { + add => Events.AddHandler(EventPreferredColumnWidth, value); + remove => Events.RemoveHandler(EventPreferredColumnWidth, value); + } + + [ + SRCategory(nameof(SR.CatLayout)), + Localizable(true), + ] + public int PreferredRowHeight + { + get + { + return preferredRowHeight; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(PreferredRowHeight))); + } + + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, DSR.DataGridRowRowHeight); + } + + if (preferredRowHeight != value) + { + preferredRowHeight = value; + OnPreferredRowHeightChanged(EventArgs.Empty); + } + } + } + + public event EventHandler PreferredRowHeightChanged + { + add => Events.AddHandler(EventPreferredRowHeight, value); + remove => Events.RemoveHandler(EventPreferredRowHeight, value); + } + + private void ResetPreferredRowHeight() + { + PreferredRowHeight = defaultFontHeight + 3; + } + + protected bool ShouldSerializePreferredRowHeight() + { + return preferredRowHeight != defaultFontHeight + 3; + } + + [ + SRCategory(nameof(SR.CatDisplay)), + DefaultValue(true), + ] + public bool ColumnHeadersVisible + { + get + { + return columnHeadersVisible; + } + set + { + if (columnHeadersVisible != value) + { + columnHeadersVisible = value; + OnColumnHeadersVisibleChanged(EventArgs.Empty); + } + } + } + + public event EventHandler ColumnHeadersVisibleChanged + { + add => Events.AddHandler(EventColumnHeadersVisible, value); + remove => Events.RemoveHandler(EventColumnHeadersVisible, value); + } + + [ + SRCategory(nameof(SR.CatDisplay)), + DefaultValue(true), + ] + public bool RowHeadersVisible + { + get + { + return rowHeadersVisible; + } + set + { + if (rowHeadersVisible != value) + { + rowHeadersVisible = value; + OnRowHeadersVisibleChanged(EventArgs.Empty); + } + } + } + + public event EventHandler RowHeadersVisibleChanged + { + add => Events.AddHandler(EventRowHeadersVisible, value); + remove => Events.RemoveHandler(EventRowHeadersVisible, value); + } + + [ + SRCategory(nameof(SR.CatLayout)), + DefaultValue(defaultRowHeaderWidth), + Localizable(true), + ] + public int RowHeaderWidth + { + get + { + return rowHeaderWidth; + } + set + { + if (DataGrid is not null) + { + value = Math.Max(DataGrid.MinimumRowHeaderWidth(), value); + } + + if (rowHeaderWidth != value) + { + rowHeaderWidth = value; + OnRowHeaderWidthChanged(EventArgs.Empty); + } + } + } + + public event EventHandler RowHeaderWidthChanged + { + add => Events.AddHandler(EventRowHeaderWidth, value); + remove => Events.RemoveHandler(EventRowHeaderWidth, value); + } + + [ + SRCategory(nameof(SR.CatColors)), + ] + public Color SelectionBackColor + { + get + { + return selectionBackBrush.Color; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(SelectionBackColor))); + } + + if (System.Windows.Forms.DataGrid.IsTransparentColor(value)) + { + throw new ArgumentException(DSR.DataGridTableStyleTransparentSelectionBackColorNotAllowed, nameof(value)); + } + + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, nameof(SelectionBackColor)), nameof(value)); + } + + if (!value.Equals(selectionBackBrush.Color)) + { + selectionBackBrush = new SolidBrush(value); + InvalidateInside(); + OnSelectionBackColorChanged(EventArgs.Empty); + } + } + } + + public event EventHandler SelectionBackColorChanged + { + add => Events.AddHandler(EventSelectionBackColor, value); + remove => Events.RemoveHandler(EventSelectionBackColor, value); + } + + internal SolidBrush SelectionBackBrush + { + get + { + return selectionBackBrush; + } + } + + internal SolidBrush SelectionForeBrush + { + get + { + return selectionForeBrush; + } + } + + protected bool ShouldSerializeSelectionBackColor() + { + return !DefaultSelectionBackBrush.Equals(selectionBackBrush); + } + + public void ResetSelectionBackColor() + { + if (ShouldSerializeSelectionBackColor()) + { + SelectionBackColor = DefaultSelectionBackBrush.Color; + } + } + + [ + Description("The foreground color for the current data grid row"), + SRCategory(nameof(SR.CatColors)), + ] + public Color SelectionForeColor + { + get + { + return selectionForeBrush.Color; + } + set + { + if (isDefaultTableStyle) + { + throw new ArgumentException(string.Format(DSR.DataGridDefaultTableSet, nameof(SelectionForeColor))); + } + + if (value.IsEmpty) + { + throw new ArgumentException(string.Format(DSR.DataGridEmptyColor, nameof(SelectionForeColor)), nameof(value)); + } + + if (!value.Equals(selectionForeBrush.Color)) + { + selectionForeBrush = new SolidBrush(value); + InvalidateInside(); + OnSelectionForeColorChanged(EventArgs.Empty); + } + } + } + + public event EventHandler SelectionForeColorChanged + { + add => Events.AddHandler(EventSelectionForeColor, value); + remove => Events.RemoveHandler(EventSelectionForeColor, value); + } + + protected virtual bool ShouldSerializeSelectionForeColor() + { + return !SelectionForeBrush.Equals(DefaultSelectionForeBrush); + } + + public void ResetSelectionForeColor() + { + if (ShouldSerializeSelectionForeColor()) + { + SelectionForeColor = DefaultSelectionForeBrush.Color; + } + } + + // will need this function from the dataGrid + // + private void InvalidateInside() + { + if (DataGrid is not null) + { + DataGrid.InvalidateInside(); + } + } + + public static readonly DataGridTableStyle DefaultTableStyle = new DataGridTableStyle(true); + + /// + /// Initializes a new instance of the class. + /// + public DataGridTableStyle(bool isDefaultTableStyle) + { + gridColumns = new GridColumnStylesCollection(this, isDefaultTableStyle); + gridColumns.CollectionChanged += new CollectionChangeEventHandler(OnColumnCollectionChanged); + this.isDefaultTableStyle = isDefaultTableStyle; + } + + public DataGridTableStyle() : this(false) + { + } + + /// + /// Initializes a new instance of the class with the specified + /// . + /// + public DataGridTableStyle(CurrencyManager listManager) : this() + { + Debug.Assert(listManager is not null, "the DataGridTabel cannot use a null listManager"); + mappingName = listManager.GetListName(); + // set up the Relations and the columns + SetGridColumnStylesCollection(listManager); + } + + internal void SetRelationsList(CurrencyManager listManager) + { + PropertyDescriptorCollection propCollection = listManager.GetItemProperties(); + Debug.Assert(!IsDefault, "the grid can set the relations only on a table that was manually added by the user"); + int propCount = propCollection.Count; + if (relationsList.Count > 0) + { + relationsList.Clear(); + } + + for (int i = 0; i < propCount; i++) + { + PropertyDescriptor prop = propCollection[i]; + Debug.Assert(prop is not null, "prop is null: how that happened?"); + if (PropertyDescriptorIsARelation(prop)) + { + // relation + relationsList.Add(prop.Name); + } + } + } + + internal void SetGridColumnStylesCollection(CurrencyManager listManager) + { + // when we are setting the gridColumnStyles, do not handle any gridColumnCollectionChanged events + gridColumns.CollectionChanged -= new CollectionChangeEventHandler(OnColumnCollectionChanged); + + PropertyDescriptorCollection propCollection = listManager.GetItemProperties(); + + // we need to clear the relations list + if (relationsList.Count > 0) + { + relationsList.Clear(); + } + + Debug.Assert(propCollection is not null, "propCollection is null: how that happened?"); + int propCount = propCollection.Count; + for (int i = 0; i < propCount; i++) + { + PropertyDescriptor prop = propCollection[i]; + Debug.Assert(prop is not null, "prop is null: how that happened?"); + // do not take into account the properties that are browsable. + if (!prop.IsBrowsable) + { + continue; + } + + if (PropertyDescriptorIsARelation(prop)) + { + // relation + relationsList.Add(prop.Name); + } + else + { + // column + DataGridColumnStyle col = CreateGridColumn(prop, isDefaultTableStyle); + if (isDefaultTableStyle) + { + gridColumns.AddDefaultColumn(col); + } + else + { + col.MappingName = prop.Name; + col.HeaderText = prop.Name; + gridColumns.Add(col); + } + } + } + + // now we are able to handle the collectionChangeEvents + gridColumns.CollectionChanged += new CollectionChangeEventHandler(OnColumnCollectionChanged); + } + + private static bool PropertyDescriptorIsARelation(PropertyDescriptor prop) + { + return typeof(IList).IsAssignableFrom(prop.PropertyType) && !typeof(Array).IsAssignableFrom(prop.PropertyType); + } + + internal protected virtual DataGridColumnStyle CreateGridColumn(PropertyDescriptor prop) + { + return CreateGridColumn(prop, false); + } + + internal protected virtual DataGridColumnStyle CreateGridColumn(PropertyDescriptor prop, bool isDefault) + { + ArgumentNullException.ThrowIfNull(prop); + + DataGridColumnStyle ret = null; + Type dataType = prop.PropertyType; + + if (dataType.Equals(typeof(bool))) + { + ret = new DataGridBoolColumn(prop, isDefault); + } + else if (dataType.Equals(typeof(string))) + { + ret = new DataGridTextBoxColumn(prop, isDefault); + } + else if (dataType.Equals(typeof(DateTime))) + { + ret = new DataGridTextBoxColumn(prop, "d", isDefault); + } + else if (dataType.Equals(typeof(short)) || + dataType.Equals(typeof(int)) || + dataType.Equals(typeof(long)) || + dataType.Equals(typeof(ushort)) || + dataType.Equals(typeof(uint)) || + dataType.Equals(typeof(ulong)) || + dataType.Equals(typeof(decimal)) || + dataType.Equals(typeof(double)) || + dataType.Equals(typeof(float)) || + dataType.Equals(typeof(byte)) || + dataType.Equals(typeof(sbyte))) + { + ret = new DataGridTextBoxColumn(prop, "G", isDefault); + } + else + { + ret = new DataGridTextBoxColumn(prop, isDefault); + } + + return ret; + } + + internal void ResetRelationsList() + { + if (isDefaultTableStyle) + { + relationsList.Clear(); + } + } + + // =------------------------------------------------------------------ + // = Properties + // =------------------------------------------------------------------ + + /// + /// Gets the name of this grid table. + /// + [Editor("System.Windows.Forms.Design.DataGridTableStyleMappingNameEditor, " + Assemblies.SystemDesign, typeof(Drawing.Design.UITypeEditor)), DefaultValue("")] + public string MappingName + { + get + { + return mappingName; + } + set + { + if (value is null) + { + value = string.Empty; + } + + if (value.Equals(mappingName)) + { + return; + } + + string originalMappingName = MappingName; + mappingName = value; + + // this could throw + try + { + if (DataGrid is not null) + { + DataGrid.TableStyles.CheckForMappingNameDuplicates(this); + } + } + catch + { + mappingName = originalMappingName; + throw; + } + + OnMappingNameChanged(EventArgs.Empty); + } + } + + public event EventHandler MappingNameChanged + { + add => Events.AddHandler(EventMappingName, value); + remove => Events.RemoveHandler(EventMappingName, value); + } + + /// + /// Gets the + /// list of relation objects for the grid table. + /// + internal ArrayList RelationsList + { + get + { + return relationsList; + } + } + + /// + /// Gets the collection of columns drawn for this table. + /// + [ + Localizable(true), + DesignerSerializationVisibility(DesignerSerializationVisibility.Content) + ] + public virtual GridColumnStylesCollection GridColumnStyles + { + get + { + return gridColumns; + } + } + + /// + /// Gets or sets the + /// control displaying the table. + /// + internal void SetInternalDataGrid(DataGrid dG, bool force) + { + if (dataGrid is not null && dataGrid.Equals(dG) && !force) + { + return; + } + else + { + dataGrid = dG; + if (dG is not null && dG.Initializing) + { + return; + } + + int nCols = gridColumns.Count; + for (int i = 0; i < nCols; i++) + { + gridColumns[i].SetDataGridInternalInColumn(dG); + } + } + } + + /// + /// Gets or sets the control for the drawn table. + /// + [Browsable(false)] + public virtual DataGrid DataGrid + { + get + { + return dataGrid; + } + set + { + SetInternalDataGrid(value, true); + } + } + + /// + /// Gets or sets a value indicating whether columns can be + /// edited. + /// + [DefaultValue(false)] + public virtual bool ReadOnly + { + get + { + return readOnly; + } + set + { + if (readOnly != value) + { + readOnly = value; + OnReadOnlyChanged(EventArgs.Empty); + } + } + } + + public event EventHandler ReadOnlyChanged + { + add => Events.AddHandler(EventReadOnly, value); + remove => Events.RemoveHandler(EventReadOnly, value); + } + + // =------------------------------------------------------------------ + // = Methods + // =------------------------------------------------------------------ + + /// + /// Requests an edit operation. + /// + public bool BeginEdit(DataGridColumnStyle gridColumn, int rowNumber) + { + DataGrid grid = DataGrid; + if (grid is null) + { + return false; + } + else + { + return grid.BeginEdit(gridColumn, rowNumber); + } + } + + /// + /// Requests an end to an edit + /// operation. + /// + public bool EndEdit(DataGridColumnStyle gridColumn, int rowNumber, bool shouldAbort) + { + DataGrid grid = DataGrid; + if (grid is null) + { + return false; + } + else + { + return grid.EndEdit(gridColumn, rowNumber, shouldAbort); + } + } + + internal void InvalidateColumn(DataGridColumnStyle column) + { + int index = GridColumnStyles.IndexOf(column); + if (index >= 0 && DataGrid is not null) + { + DataGrid.InvalidateColumn(index); + } + } + + private void OnColumnCollectionChanged(object sender, CollectionChangeEventArgs e) + { + gridColumns.CollectionChanged -= new CollectionChangeEventHandler(OnColumnCollectionChanged); + + try + { + DataGrid grid = DataGrid; + DataGridColumnStyle col = e.Element as DataGridColumnStyle; + if (e.Action == CollectionChangeAction.Add) + { + if (col is not null) + { + col.SetDataGridInternalInColumn(grid); + } + } + else if (e.Action == CollectionChangeAction.Remove) + { + if (col is not null) + { + col.SetDataGridInternalInColumn(null); + } + } + else + { + // refresh + Debug.Assert(e.Action == CollectionChangeAction.Refresh, "there are only Add, Remove and Refresh in the CollectionChangeAction"); + // if we get a column in this collectionChangeEventArgs it means + // that the propertyDescriptor in that column changed. + if (e.Element is not null) + { + for (int i = 0; i < gridColumns.Count; i++) + { + gridColumns[i].SetDataGridInternalInColumn(null); + } + } + } + + if (grid is not null) + { + grid.OnColumnCollectionChanged(this, e); + } + } + finally + { + gridColumns.CollectionChanged += new CollectionChangeEventHandler(OnColumnCollectionChanged); + } + } + +#if false + /// + /// The DataColumnCollection class actually wires up this + /// event handler to the PropertyChanged events of + /// a DataGridTable's columns. + /// + internal void OnColumnChanged(object sender, PropertyChangedEvent event) { + if (event.PropertyName.Equals("Visible")) + GenerateVisibleColumnsCache(); + } +#endif + protected virtual void OnReadOnlyChanged(EventArgs e) + { + if (Events[EventReadOnly] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnMappingNameChanged(EventArgs e) + { + if (Events[EventMappingName] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnAlternatingBackColorChanged(EventArgs e) + { + if (Events[EventAlternatingBackColor] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnForeColorChanged(EventArgs e) + { + if (Events[EventForeColor] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnBackColorChanged(EventArgs e) + { + if (Events[EventBackColor] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnAllowSortingChanged(EventArgs e) + { + if (Events[EventAllowSorting] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnGridLineColorChanged(EventArgs e) + { + if (Events[EventGridLineColor] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnGridLineStyleChanged(EventArgs e) + { + if (Events[EventGridLineStyle] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnHeaderBackColorChanged(EventArgs e) + { + if (Events[EventHeaderBackColor] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnHeaderFontChanged(EventArgs e) + { + if (Events[EventHeaderFont] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnHeaderForeColorChanged(EventArgs e) + { + if (Events[EventHeaderForeColor] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnLinkColorChanged(EventArgs e) + { + if (Events[EventLinkColor] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnLinkHoverColorChanged(EventArgs e) + { + if (Events[EventLinkHoverColor] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnPreferredRowHeightChanged(EventArgs e) + { + if (Events[EventPreferredRowHeight] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnPreferredColumnWidthChanged(EventArgs e) + { + if (Events[EventPreferredColumnWidth] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnColumnHeadersVisibleChanged(EventArgs e) + { + if (Events[EventColumnHeadersVisible] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnRowHeadersVisibleChanged(EventArgs e) + { + if (Events[EventRowHeadersVisible] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnRowHeaderWidthChanged(EventArgs e) + { + if (Events[EventRowHeaderWidth] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnSelectionForeColorChanged(EventArgs e) + { + if (Events[EventSelectionForeColor] is EventHandler eh) + { + eh(this, e); + } + } + + protected virtual void OnSelectionBackColorChanged(EventArgs e) + { + if (Events[EventSelectionBackColor] is EventHandler eh) + { + eh(this, e); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + GridColumnStylesCollection cols = GridColumnStyles; + if (cols is not null) + { + for (int i = 0; i < cols.Count; i++) + { + cols[i].Dispose(); + } + } + } + + base.Dispose(disposing); + } + + internal bool IsDefault + { + get + { + return isDefaultTableStyle; + } + } } - - public bool BeginEdit(DataGridColumnStyle gridColumn, int rowNumber) => throw null; - - public bool EndEdit(DataGridColumnStyle gridColumn, int rowNumber, bool shouldAbort) => throw null; - - protected internal virtual DataGridColumnStyle CreateGridColumn(PropertyDescriptor prop, bool isDefault) => throw null; - - protected internal virtual DataGridColumnStyle CreateGridColumn(PropertyDescriptor prop) => throw null; - - protected virtual void OnReadOnlyChanged(EventArgs e) { } - - protected virtual void OnMappingNameChanged(EventArgs e) { } - - protected virtual void OnAlternatingBackColorChanged(EventArgs e) { } - - protected virtual void OnForeColorChanged(EventArgs e) { } - - protected virtual void OnBackColorChanged(EventArgs e) { } - - protected virtual void OnAllowSortingChanged(EventArgs e) { } - - protected virtual void OnGridLineColorChanged(EventArgs e) { } - - protected virtual void OnGridLineStyleChanged(EventArgs e) { } - - protected virtual void OnHeaderBackColorChanged(EventArgs e) { } - - protected virtual void OnHeaderFontChanged(EventArgs e) { } - - protected virtual void OnHeaderForeColorChanged(EventArgs e) { } - - protected virtual void OnLinkColorChanged(EventArgs e) { } - - protected virtual void OnLinkHoverColorChanged(EventArgs e) { } - - protected virtual void OnPreferredRowHeightChanged(EventArgs e) { } - - protected virtual void OnPreferredColumnWidthChanged(EventArgs e) { } - - protected virtual void OnColumnHeadersVisibleChanged(EventArgs e) { } - - protected virtual void OnRowHeadersVisibleChanged(EventArgs e) { } - - protected virtual void OnRowHeaderWidthChanged(EventArgs e) { } - - protected virtual void OnSelectionForeColorChanged(EventArgs e) { } - - protected virtual void OnSelectionBackColorChanged(EventArgs e) { } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTextBox.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTextBox.cs index a798de6ed36..c05cfbd2388 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTextBox.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTextBox.cs @@ -1,37 +1,301 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, RS0016, IDE0078 + +#nullable disable + using System.ComponentModel; using System.Runtime.InteropServices; namespace System.Windows.Forms; + /// + /// Represents a control that is hosted in a + /// . + /// + [ + ComVisible(true), + ClassInterface(ClassInterfaceType.AutoDispatch), + ToolboxItem(false), + DesignTimeVisible(false) + ] + public class DataGridTextBox : TextBox + { + private bool isInEditOrNavigateMode = true; -#nullable disable + // only needed to signal the dataGrid that an edit + // takes place + private DataGrid dataGrid; -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[ComVisible(true)] -[ClassInterface(ClassInterfaceType.AutoDispatch)] -[ToolboxItem(false)] -[DesignTimeVisible(false)] -[DefaultProperty("GridEditName")] -public class DataGridTextBox : TextBox -{ - public DataGridTextBox() => throw new PlatformNotSupportedException(); - - public void SetDataGrid(DataGrid parentGrid) { } - - public bool IsInEditOrNavigateMode - { - get => throw null; - set { } + public DataGridTextBox() : base() + { + TabStop = false; + } + + /// + /// Sets the to which this control belongs. + /// + public void SetDataGrid(DataGrid parentGrid) + { + dataGrid = parentGrid; + } + + protected override void WndProc(ref Message m) + { + // but what if we get a CtrlV? + // what about deleting from the menu? + if (m.MsgInternal == PInvokeCore.WM_PASTE || m.MsgInternal == PInvokeCore.WM_CUT || m.MsgInternal == PInvokeCore.WM_CLEAR) + { + IsInEditOrNavigateMode = false; + dataGrid.ColumnStartedEditing(Bounds); + } + + base.WndProc(ref m); + } + + protected override void OnMouseWheel(MouseEventArgs e) + { + dataGrid.TextBoxOnMouseWheel(e); + } + + protected override void OnKeyPress(KeyPressEventArgs e) + { + base.OnKeyPress(e); + + // Shift-Space should not cause the grid to + // be put in edit mode + if (e.KeyChar == ' ' && (Control.ModifierKeys & Keys.Shift) == Keys.Shift) + { + return; + } + + // if the edit box is in ReadOnly mode, then do not tell the DataGrid about the + // edit + if (ReadOnly) + { + return; + } + + // Ctrl-* should not put the grid in edit mode + if ((Control.ModifierKeys & Keys.Control) == Keys.Control && ((Control.ModifierKeys & Keys.Alt) == 0)) + { + return; + } + + IsInEditOrNavigateMode = false; + + // let the DataGrid know about the edit + dataGrid.ColumnStartedEditing(Bounds); + } + + protected internal override bool ProcessKeyMessage(ref Message m) + { + Keys key = (Keys)(int)m.WParamInternal; + Keys modifierKeys = ModifierKeys; + + if ((key | modifierKeys) == Keys.Enter || (key | modifierKeys) == Keys.Escape || ((key | modifierKeys) == (Keys.Enter | Keys.Control)) + ) + { + // enter and escape keys are sent directly to the DataGrid + // for those keys, eat the WM_CHAR part of the KeyMessage + // + if (m.MsgInternal == PInvokeCore.WM_CHAR) + { + return true; + } + + return ProcessKeyPreview(ref m); + } + + if (m.MsgInternal == PInvokeCore.WM_CHAR) + { + if (key == Keys.LineFeed) // eat the LineFeed we get when the user presses Ctrl-Enter in a gridTextBox + { + return true; + } + + return ProcessKeyEventArgs(ref m); + } + + // now the edit control will be always on top of the grid + // we only want to process the WM_KEYUP message ( the same way the grid was doing when the grid was getting all + // the keys ) + if (m.MsgInternal == PInvokeCore.WM_KEYUP) + { + return true; + } + + Keys keyData = key & Keys.KeyCode; + + switch (keyData) + { + case Keys.Right: + // here is the deal with Keys.Right: + // if the end of the selection is at the end of the string + // send this character to the dataGrid + // else, process the KeyEvent + // + if (SelectionStart + SelectionLength == Text.Length) + { + return ProcessKeyPreview(ref m); + } + + return ProcessKeyEventArgs(ref m); + case Keys.Left: + // if the end of the selection is at the begining of the string + // or if the entire text is selected and we did not start editing + // send this character to the dataGrid + // else, process the KeyEvent + // + if (SelectionStart + SelectionLength == 0 || + (IsInEditOrNavigateMode && SelectionLength == Text.Length)) + { + return ProcessKeyPreview(ref m); + } + + return ProcessKeyEventArgs(ref m); + case Keys.Down: + // if the end of the selection is on the last line of the text then + // send this character to the dataGrid + // else, process the KeyEvent + // + int end = SelectionStart + SelectionLength; + if (Text.IndexOf("\r\n", end, StringComparison.Ordinal) == -1) + { + return ProcessKeyPreview(ref m); + } + + return ProcessKeyEventArgs(ref m); + case Keys.Up: + // if the end of the selection is on the first line of the text then + // send this character to the dataGrid + // else, process the KeyEvent + // + if (Text.IndexOf("\r\n", StringComparison.Ordinal) < 0 || SelectionStart + SelectionLength < Text.IndexOf("\r\n", StringComparison.Ordinal)) + { + return ProcessKeyPreview(ref m); + } + + return ProcessKeyEventArgs(ref m); + case Keys.Home: + case Keys.End: + if (SelectionLength == Text.Length) + { + return ProcessKeyPreview(ref m); + } + else + { + return ProcessKeyEventArgs(ref m); + } + + case Keys.Prior: + case Keys.Next: + case Keys.Oemplus: + case Keys.Add: + case Keys.OemMinus: + case Keys.Subtract: + if (IsInEditOrNavigateMode) + { + // this will ultimately call parent's ProcessKeyPreview + // in our case, DataGrid's ProcessKeyPreview + return ProcessKeyPreview(ref m); + } + else + { + return ProcessKeyEventArgs(ref m); + } + + case Keys.Space: + if (IsInEditOrNavigateMode && (Control.ModifierKeys & Keys.Shift) == Keys.Shift) + { + // when we get a SHIFT-SPACEBAR message, disregard the WM_CHAR part of the message + if (m.MsgInternal == PInvokeCore.WM_CHAR) + { + return true; + } + + // if the user pressed the SHIFT key at the same time with + // the space key, send the key message to the DataGrid + return ProcessKeyPreview(ref m); + } + + return ProcessKeyEventArgs(ref m); + case Keys.A: + if (IsInEditOrNavigateMode && (Control.ModifierKeys & Keys.Control) == Keys.Control) + { + // when we get a Control-A message, disregard the WM_CHAR part of the message + if (m.MsgInternal == PInvokeCore.WM_CHAR) + { + return true; + } + + // if the user pressed the Control key at the same time with + // the space key, send the key message to the DataGrid + return ProcessKeyPreview(ref m); + } + + return ProcessKeyEventArgs(ref m); + case Keys.F2: + IsInEditOrNavigateMode = false; + // do not select all the text, but + // position the caret at the end of the text + SelectionStart = Text.Length; + return true; + case Keys.Delete: + if (IsInEditOrNavigateMode) + { + // pass the delete to the parent, in our case, the DataGrid + // if the dataGrid used the key, then we aren't gonne + // use it anymore, else we are + if (ProcessKeyPreview(ref m)) + { + return true; + } + else + { + // the edit control will use the + // delete key: we are in Edit mode now: + IsInEditOrNavigateMode = false; + dataGrid.ColumnStartedEditing(Bounds); + + return ProcessKeyEventArgs(ref m); + } + } + else + { + return ProcessKeyEventArgs(ref m); + } + + case Keys.Tab: + // the TextBox gets the Control-Tab messages, + // not the parent + if ((ModifierKeys & Keys.Control) == Keys.Control) + { + return ProcessKeyPreview(ref m); + } + else + { + return ProcessKeyEventArgs(ref m); + } + + default: + return ProcessKeyEventArgs(ref m); + } + } + + public bool IsInEditOrNavigateMode + { + get + { + return isInEditOrNavigateMode; + } + set + { + isInEditOrNavigateMode = value; + if (value) + { + SelectAll(); + } + } + } } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTextBoxColumn.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTextBoxColumn.cs index 6a2c81afd5d..1c597c174cc 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTextBoxColumn.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridTextBoxColumn.cs @@ -1,108 +1,644 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1310, CA1510, CSIsNull001, CSIsNull002, RS0016, IDE0031, IDE0300, IDE0052, CA1822 + +#nullable disable + using System.ComponentModel; using System.Drawing; -using System.Drawing.Design; +using System.Globalization; namespace System.Windows.Forms; + /// + /// Hosts a System.Windows.Forms.TextBox control in a cell of a System.Windows.Forms.DataGridColumnStyle for editing strings. + /// + public class DataGridTextBoxColumn : DataGridColumnStyle + { + // ui State + private readonly int xMargin = 2; + private readonly int yMargin = 1; + // private int fontHandle = 0; + private string format; + private TypeConverter typeConverter; + private IFormatProvider formatInfo; + private Reflection.MethodInfo parseMethod; -#nullable disable + // hosted control + private readonly DataGridTextBox edit; -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -public class DataGridTextBoxColumn : DataGridColumnStyle -{ - public DataGridTextBoxColumn() => throw new PlatformNotSupportedException(); + // editing state + private string oldValue; + private int editRow = -1; - public DataGridTextBoxColumn(PropertyDescriptor prop) => throw new PlatformNotSupportedException(); + /// + /// Initializes a new instance of the System.Windows.Forms.DataGridTextBoxColumn + /// class. + /// + public DataGridTextBoxColumn() : this(null, null) + { + } - public DataGridTextBoxColumn(PropertyDescriptor prop, string format) => throw new PlatformNotSupportedException(); + /// + /// Initializes a new instance of a System.Windows.Forms.DataGridTextBoxColumn with + /// a specified System.Data.DataColumn. + /// + public DataGridTextBoxColumn(PropertyDescriptor prop) + : this(prop, null, false) + { + } - public DataGridTextBoxColumn(PropertyDescriptor prop, string format, bool isDefault) => throw new PlatformNotSupportedException(); + /// + /// Initializes a new instance of a System.Windows.Forms.DataGridTextBoxColumn. with + /// the specified System.Data.DataColumn and System.Windows.Forms.ComponentModel.Format. + /// + public DataGridTextBoxColumn(PropertyDescriptor prop, string format) : this(prop, format, false) { } - public DataGridTextBoxColumn(PropertyDescriptor prop, bool isDefault) => throw new PlatformNotSupportedException(); + public DataGridTextBoxColumn(PropertyDescriptor prop, string format, bool isDefault) : base(prop, isDefault) + { + edit = new DataGridTextBox + { + BorderStyle = BorderStyle.None, + Multiline = true, + AcceptsReturn = true, + Visible = false + }; + Format = format; + } - [Browsable(false)] - public virtual TextBox TextBox => throw null; + public DataGridTextBoxColumn(PropertyDescriptor prop, bool isDefault) : this(prop, null, isDefault) { } - [DefaultValue(null)] - [Editor($"System.Windows.Forms.Design.DataGridColumnStyleFormatEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))] - public string Format - { - get => throw null; - set { } - } + // =------------------------------------------------------------------ + // = Properties + // =------------------------------------------------------------------ - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Advanced)] - public IFormatProvider FormatInfo - { - get => throw null; - set { } - } + /// + /// Gets the hosted System.Windows.Forms.TextBox control. + /// + [Browsable(false)] + public virtual TextBox TextBox + { + get + { + return edit; + } + } - [DefaultValue(null)] - public override PropertyDescriptor PropertyDescriptor - { - set { } - } + internal override bool KeyPress(int rowNum, Keys keyData) + { + if (edit.IsInEditOrNavigateMode) + { + return base.KeyPress(rowNum, keyData); + } + + // if the edit box is editing, then + // pass this keystroke to the edit box + // + return false; + } + + /// + /// Adds a System.Windows.Forms.TextBox control to the System.Windows.Forms.DataGrid control's System.Windows.Forms.Control.ControlCollection + /// . + /// + protected override void SetDataGridInColumn(DataGrid value) + { + base.SetDataGridInColumn(value); + if (edit.ParentInternal != null) + { + edit.ParentInternal.Controls.Remove(edit); + } + + if (value != null) + { + value.Controls.Add(edit); + } + + // we have to tell the edit control about its dataGrid + edit.SetDataGrid(value); + } + + /* CUT as part of the new DataGridTableStyleSheet thing + public override Font Font { + set { + base.Font = value; + Font f = base.Font; + edit.Font = f; + // if (f != null) { + // fontHandle = f.Handle; + // } + } + } + */ + + /// + /// Gets or sets the System.Windows.Forms.ComponentModel.Format for the System.Windows.Forms.DataGridTextBoxColumn + /// . + /// + [ + SRDescription(nameof(SR.FormatControlFormatDescr)), + DefaultValue(null) + ] + public override PropertyDescriptor PropertyDescriptor + { + set + { + base.PropertyDescriptor = value; + if (PropertyDescriptor != null) + { + if (PropertyDescriptor.PropertyType != typeof(object)) + { + typeConverter = TypeDescriptor.GetConverter(PropertyDescriptor.PropertyType); + parseMethod = PropertyDescriptor.PropertyType.GetMethod("Parse", new Type[] { typeof(string), typeof(IFormatProvider) }); + } + } + } + } + + // add the corresponding value Editor: rip one from the valueEditor for the DisplayMember in the + // format object + [DefaultValue(null), Editor("System.Windows.Forms.Design.DataGridColumnStyleFormatEditor, " + Assemblies.SystemDesign, typeof(Drawing.Design.UITypeEditor))] + public string Format + { + get + { + return format; + } + set + { + if (value == null) + { + value = string.Empty; + } + + if (format == null || !format.Equals(value)) + { + format = value; + + // if the associated typeConverter cannot convert from string, + // then we can't modify the column value. hence, make it readOnly + // + if (format.Length == 0) + { + if (typeConverter != null && !typeConverter.CanConvertFrom(typeof(string))) + { + ReadOnly = true; + } + } + + Invalidate(); + } + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)] + public IFormatProvider FormatInfo + { + get + { + return formatInfo; + } + set + { + if (formatInfo == null || !formatInfo.Equals(value)) + { + formatInfo = value; + } + } + } + + public override bool ReadOnly + { + get + { + return base.ReadOnly; + } + set + { + // if the gridColumn is can't convert the string to + // the backGround propertyDescriptor, then make the column ReadOnly + if (!value && (format == null || format.Length == 0)) + { + if (typeConverter != null && !typeConverter.CanConvertFrom(typeof(string))) + { + return; + } + } + + base.ReadOnly = value; + } + } + + // =------------------------------------------------------------------ + // = Methods + // =------------------------------------------------------------------ + + private void DebugOut(string s) + { + Debug.WriteLineIf(CompModSwitches.DGEditColumnEditing.TraceVerbose, "DGEditColumnEditing: " + s); + } + + // will hide the edit control + /// + /// Informs the column the focus is being conceded. + /// + protected internal override void ConcedeFocus() + { + edit.Bounds = Rectangle.Empty; + // edit.Visible = false; + // HideEditBox(); + } + + /// + /// Hides the System.Windows.Forms.TextBox + /// control and moves the focus to the System.Windows.Forms.DataGrid + /// control. + /// + protected void HideEditBox() + { + bool wasFocused = edit.Focused; + edit.Visible = false; + + // it seems that edit.Visible = false will take away the focus from + // the edit control. And this means that we will not give the focus to the grid + // If all the columns would have an edit control this would not be bad + // ( or if the grid is the only control on the form ), + // but when we have a DataGridBoolColumn then the focus will be taken away + // by the next control in the form. + // + // if (edit.Focused && this.DataGridTableStyle.DataGrid.CanFocus) { + + // when the user deletes the current ( ie active ) column from the + // grid, the grid should still call EndEdit ( so that the changes that the user made + // before deleting the column will go to the backEnd) + // however, in that situation, we are left w/ the editColumn which is not parented. + // the grid will call Edit to reset the EditColumn + if (wasFocused && DataGridTableStyle != null && DataGridTableStyle.DataGrid != null && DataGridTableStyle.DataGrid.CanFocus) + { + DataGridTableStyle.DataGrid.Focus(); + Debug.Assert(!edit.Focused, "the edit control just conceeded focus to the dataGrid"); + } + } + + protected internal override void UpdateUI(CurrencyManager source, int rowNum, string displayText) + { + edit.Text = GetText(GetColumnValueAtRow(source, rowNum)); + if (!edit.ReadOnly && displayText != null) + { + edit.Text = displayText; + } + } + + /// + /// Ends an edit operation on the System.Windows.Forms.DataGridColumnStyle + /// . + /// + protected void EndEdit() + { + edit.IsInEditOrNavigateMode = true; + DebugOut("Ending Edit"); + Invalidate(); + } - protected void HideEditBox() { } + /// + /// Returns the optimum width and + /// height of the cell in a specified row relative + /// to the specified value. + /// + protected internal override Size GetPreferredSize(Graphics g, object value) + { + Size extents = Size.Ceiling(g.MeasureString(GetText(value), DataGridTableStyle.DataGrid.Font)); + extents.Width += xMargin * 2 + DataGridTableStyle.GridLineWidth; + extents.Height += yMargin; + return extents; + } - protected void EndEdit() { } + /// + /// Gets the height of a cell in a System.Windows.Forms.DataGridColumnStyle + /// . + /// + protected internal override int GetMinimumHeight() + { + // why + 3? cause we have to give some way to the edit box. + return FontHeight + yMargin + 3; + } - protected void PaintText( - Graphics g, - Rectangle bounds, - string text, - bool alignToRight) - { } + /// + /// Gets the height to be used in for automatically resizing columns. + /// + protected internal override int GetPreferredHeight(Graphics g, object value) + { + int newLineIndex = 0; + int newLines = 0; + string valueString = GetText(value); + while (newLineIndex != -1 && newLineIndex < valueString.Length) + { + newLineIndex = valueString.IndexOf("\r\n", newLineIndex + 1); + newLines++; + } - protected void PaintText( - Graphics g, - Rectangle textBounds, - string text, - Brush backBrush, - Brush foreBrush, - bool alignToRight) - { } + return FontHeight * newLines + yMargin; + } - protected internal override Size GetPreferredSize(Graphics g, object value) => throw null; + /// + /// Initiates a request to interrupt an edit procedure. + /// + protected internal override void Abort(int rowNum) + { + RollBack(); + HideEditBox(); + EndEdit(); + } - protected internal override int GetMinimumHeight() => throw null; + // used for Alt0 functionality + /// + /// Enters a in the column. + /// + protected internal override void EnterNullValue() + { + if (ReadOnly) + { + return; + } - protected internal override int GetPreferredHeight(Graphics g, object value) => throw null; + // if the edit box is not visible, then + // do not put the edit text in it + if (!edit.Visible) + { + return; + } - protected internal override void Abort(int rowNum) { } + // if we are editing, then we should be able to enter alt-0 in a cell. + // + if (!edit.IsInEditOrNavigateMode) + { + return; + } - protected internal override bool Commit(CurrencyManager dataSource, int rowNum) => throw null; + edit.Text = NullText; + // edit.Visible = true; + edit.IsInEditOrNavigateMode = false; + // tell the dataGrid that there is an edit: + if (DataGridTableStyle != null && DataGridTableStyle.DataGrid != null) + { + DataGridTableStyle.DataGrid.ColumnStartedEditing(edit.Bounds); + } + } - protected internal override void Edit( - CurrencyManager source, - int rowNum, - Rectangle bounds, - bool readOnly, - string displayText, - bool cellIsVisible) - { } + /// + /// Inititates a request to complete an editing procedure. + /// + protected internal override bool Commit(CurrencyManager dataSource, int rowNum) + { + // always hide the edit box + // HideEditBox(); + edit.Bounds = Rectangle.Empty; - protected internal override void Paint( - Graphics g, - Rectangle bounds, - CurrencyManager source, - int rowNum, - bool alignToRight) - { } + if (edit.IsInEditOrNavigateMode) + { + return true; + } - protected internal override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum) - { } -} + try + { + object value = edit.Text; + if (NullText.Equals(value)) + { + value = Convert.DBNull; + edit.Text = NullText; + } + else if (format != null && format.Length != 0 && parseMethod != null && FormatInfo != null) + { + // use reflection to get the Parse method on the + // type of the propertyDescriptor. + value = (object)parseMethod.Invoke(null, new object[] { edit.Text, FormatInfo }); + if (value is IFormattable) + { + edit.Text = ((IFormattable)value).ToString(format, formatInfo); + } + else + { + edit.Text = value.ToString(); + } + } + else if (typeConverter != null && typeConverter.CanConvertFrom(typeof(string))) + { + value = typeConverter.ConvertFromString(edit.Text); + edit.Text = typeConverter.ConvertToString(value); + } + + SetColumnValueAtRow(dataSource, rowNum, value); + } + catch + { + // MessageBox.Show("There was an error caught setting field \"" + // + this.PropertyDescriptor.Name + "\" to the value \"" + edit.Text + "\"\n" + // + "The value is being rolled back to the original.\n" + // + "The error was a '" + e.Message + "' " + e.StackTrace + // , "Error commiting changes...", MessageBox.IconError); + // Debug.WriteLine(e.GetType().Name); + RollBack(); + return false; + } + + DebugOut("OnCommit completed without Exception."); + EndEdit(); + return true; + } + + /// + /// Prepares a cell for editing. + /// + protected internal override void Edit(CurrencyManager source, + int rowNum, + Rectangle bounds, + bool readOnly, + string displayText, + bool cellIsVisible) + { + DebugOut("Begining Edit, rowNum :" + rowNum.ToString(CultureInfo.InvariantCulture)); + + Rectangle originalBounds = bounds; + + edit.ReadOnly = readOnly || ReadOnly || DataGridTableStyle.ReadOnly; + + edit.Text = GetText(GetColumnValueAtRow(source, rowNum)); + if (!edit.ReadOnly && displayText != null) + { + // tell the grid that we are changing stuff + DataGridTableStyle.DataGrid.ColumnStartedEditing(bounds); + // tell the edit control that the user changed it + edit.IsInEditOrNavigateMode = false; + edit.Text = displayText; + } + + if (cellIsVisible) + { + bounds.Offset(xMargin, 2 * yMargin); + bounds.Width -= xMargin; + bounds.Height -= 2 * yMargin; + DebugOut("edit bounds: " + bounds.ToString()); + edit.Bounds = bounds; + + edit.Visible = true; + + edit.TextAlign = Alignment; + } + else + { + edit.Bounds = Rectangle.Empty; + // edit.Bounds = originalBounds; + // edit.Visible = false; + } + + edit.RightToLeft = DataGridTableStyle.DataGrid.RightToLeft; + + edit.Focus(); + + editRow = rowNum; + + if (!edit.ReadOnly) + { + oldValue = edit.Text; + } + + // select the text even if the text box is read only + // because the navigation code in the DataGridTextBox::ProcessKeyMessage + // uses the SelectedText property + if (displayText == null) + { + edit.SelectAll(); + } + else + { + int end = edit.Text.Length; + edit.Select(end, 0); + } + + if (edit.Visible) + { + DataGridTableStyle.DataGrid.Invalidate(originalBounds); + } + } + + internal override string GetDisplayText(object value) + { + return GetText(value); + } + + private string GetText(object value) + { + if (value is DBNull) + { + return NullText; + } + else if (format != null && format.Length != 0 && (value is IFormattable)) + { + try + { + return ((IFormattable)value).ToString(format, formatInfo); + } + catch + { + // + } + } + else + { + // use the typeConverter: + if (typeConverter != null && typeConverter.CanConvertTo(typeof(string))) + { + return (string)typeConverter.ConvertTo(value, typeof(string)); + } + } + + return (value != null ? value.ToString() : ""); + } + + /// + /// Paints the a System.Windows.Forms.DataGridColumnStyle with the specified System.Drawing.Graphics, + /// System.Drawing.Rectangle, DataView.Rectangle, and row number. + /// + protected internal override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum) + { + Paint(g, bounds, source, rowNum, false); + } + + /// + /// Paints a System.Windows.Forms.DataGridColumnStyle with the specified System.Drawing.Graphics, System.Drawing.Rectangle, DataView, row number, and alignment. + /// + protected internal override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, bool alignToRight) + { + string text = GetText(GetColumnValueAtRow(source, rowNum)); + PaintText(g, bounds, text, alignToRight); + } + + /// + /// Paints a System.Windows.Forms.DataGridColumnStyle with the specified System.Drawing.Graphics, + /// System.Drawing.Rectangle, DataView.Rectangle, row number, background color, + /// and foreground color.. + /// + protected internal override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, + Brush backBrush, Brush foreBrush, bool alignToRight) + { + string text = GetText(GetColumnValueAtRow(source, rowNum)); + PaintText(g, bounds, text, backBrush, foreBrush, alignToRight); + } + + /// + /// Draws the text and + /// rectangle at the given location with the specified alignment. + /// + protected void PaintText(Graphics g, Rectangle bounds, string text, bool alignToRight) + { + PaintText(g, bounds, text, DataGridTableStyle.BackBrush, DataGridTableStyle.ForeBrush, alignToRight); + } + + /// + /// Draws the text and rectangle at the specified location with the + /// specified colors and alignment. + /// + protected void PaintText(Graphics g, Rectangle textBounds, string text, Brush backBrush, Brush foreBrush, bool alignToRight) + { + /* + if (edit.Visible) + g.BackColor = BackColor; + */ + + Rectangle rect = textBounds; + + StringFormat format = new StringFormat(); + if (alignToRight) + { + format.FormatFlags |= StringFormatFlags.DirectionRightToLeft; + } + + format.Alignment = Alignment == HorizontalAlignment.Left ? StringAlignment.Near : Alignment == HorizontalAlignment.Center ? StringAlignment.Center : StringAlignment.Far; + + // do not wrap the text + // + format.FormatFlags |= StringFormatFlags.NoWrap; + + g.FillRectangle(backBrush, rect); + // by design, painting leaves a little padding around the rectangle. + // so do not deflate the rectangle. + rect.Offset(0, 2 * yMargin); + rect.Height -= 2 * yMargin; + g.DrawString(text, DataGridTableStyle.DataGrid.Font, foreBrush, rect, format); + format.Dispose(); + } + + private void RollBack() + { + Debug.Assert(!edit.IsInEditOrNavigateMode, "Must be editing to rollback changes..."); + edit.Text = oldValue; + } + + protected internal override void ReleaseHostedControl() + { + if (edit.ParentInternal != null) + { + edit.ParentInternal.Controls.Remove(edit); + } + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridToolTip.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridToolTip.cs new file mode 100644 index 00000000000..a50fcb60a31 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/DataGridToolTip.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400 + +#nullable disable + +using System.Drawing; +using System.Runtime.InteropServices; +using DSR = System.Windows.Forms.DataGridStrings; + +namespace System.Windows.Forms; + // this class is basically a NativeWindow that does toolTipping + // should be one for the entire grid + internal class DataGridToolTip : MarshalByRefObject + { + // the toolTip control + private NativeWindow tipWindow; + + // the dataGrid which contains this toolTip + private readonly DataGrid dataGrid; + + // CONSTRUCTOR + public DataGridToolTip(DataGrid dataGrid) + { + ArgumentNullException.ThrowIfNull(dataGrid); + Debug.Assert(dataGrid is not null, "can't attach a tool tip to a null grid"); + this.dataGrid = dataGrid; + } + + // will ensure that the toolTip window was created + public void CreateToolTipHandle() + { + if (tipWindow is null || tipWindow.Handle == IntPtr.Zero) + { + PInvoke.InitCommonControlsEx(new INITCOMMONCONTROLSEX + { + dwSize = (uint)Marshal.SizeOf(), + dwICC = INITCOMMONCONTROLSEX_ICC.ICC_TAB_CLASSES + }); + + var cparams = new CreateParams + { + Parent = dataGrid.Handle, + ClassName = PInvoke.TOOLTIPS_CLASS, + Style = (int)PInvoke.TTS_ALWAYSTIP, + }; + + tipWindow = new NativeWindow(); + tipWindow.CreateHandle(cparams); + + PInvokeCore.SendMessage(tipWindow, PInvoke.TTM_SETMAXTIPWIDTH, 0, SystemInformation.MaxWindowTrackSize.Width); + PInvoke.SetWindowPos( + tipWindow, + HWND.HWND_NOTOPMOST, + 0, 0, 0, 0, + SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + PInvokeCore.SendMessage(tipWindow, PInvoke.TTM_SETDELAYTIME, (nuint)PInvoke.TTDT_INITIAL, 0); + } + } + + public void AddToolTip(string toolTipString, IntPtr toolTipId, Rectangle iconBounds) + { + Debug.Assert(tipWindow is not null && tipWindow.Handle != IntPtr.Zero, "the tipWindow was not initialized, bailing out"); + if (iconBounds.IsEmpty) + { + throw new ArgumentNullException(nameof(iconBounds), DSR.DataGridToolTipEmptyIcon); + } + + ArgumentNullException.ThrowIfNull(toolTipString); + + var info = new ToolInfoWrapper(dataGrid, toolTipId, TOOLTIP_FLAGS.TTF_SUBCLASS, toolTipString, iconBounds); + info.SendMessage(tipWindow, PInvoke.TTM_ADDTOOLW); + } + + public void RemoveToolTip(IntPtr toolTipId) + { + var info = new ToolInfoWrapper(dataGrid, toolTipId); + info.SendMessage(tipWindow, PInvoke.TTM_DELTOOLW); + } + + // will destroy the tipWindow + public void Destroy() + { + Debug.Assert(tipWindow is not null, "how can one destroy a null window"); + tipWindow.DestroyHandle(); + tipWindow = null; + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridColumnStylesCollection.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridColumnStylesCollection.cs index d9d03901302..f0fddf6eb45 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridColumnStylesCollection.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridColumnStylesCollection.cs @@ -1,95 +1,566 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1510, CSIsNull001, CSIsNull002, IDE0031 + +#nullable disable + using System.Collections; using System.ComponentModel; using System.Drawing.Design; +using System.Globalization; +using DSR = System.Windows.Forms.DataGridStrings; namespace System.Windows.Forms; - -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[Editor($"System.Windows.Forms.Design.DataGridColumnCollectionEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))] -[ListBindable(false)] -public class GridColumnStylesCollection : BaseCollection, IList -{ - private GridColumnStylesCollection() { } - - int IList.Add(object value) => throw null; - - void IList.Clear() { } - - bool IList.Contains(object value) => throw null; - - int IList.IndexOf(object value) => throw null; - - void IList.Insert(int index, object value) { } - - void IList.Remove(object value) { } - - void IList.RemoveAt(int index) { } - - bool IList.IsFixedSize => throw null; - - bool IList.IsReadOnly => throw null; - - object IList.this[int index] - { - get => throw null; - set { } - } - - void ICollection.CopyTo(Array array, int index) { } - - int ICollection.Count => throw null; - - bool ICollection.IsSynchronized => throw null; - - object ICollection.SyncRoot => throw null; - - IEnumerator IEnumerable.GetEnumerator() => throw null; - - public DataGridColumnStyle this[int index] => throw null; - - public DataGridColumnStyle this[string columnName] => throw null; - - public DataGridColumnStyle this[PropertyDescriptor propertyDesciptor] => throw null; - - public virtual int Add(DataGridColumnStyle column) => throw null; - - public void AddRange(DataGridColumnStyle[] columns) { } - - public event CollectionChangeEventHandler CollectionChanged + /// + /// Represents a collection of System.Windows.Forms.DataGridColumnStyle objects in the + /// control. + /// + [ + Editor("System.Windows.Forms.Design.DataGridColumnCollectionEditor, " + Assemblies.SystemDesign, typeof(UITypeEditor)), + ListBindable(false) + ] + public class GridColumnStylesCollection : BaseCollection, IList { - add { } - remove { } + CollectionChangeEventHandler onCollectionChanged; + readonly ArrayList items = new ArrayList(); + readonly DataGridTableStyle owner; + private readonly bool isDefault; + + // we have to implement IList for the Collection editor to work + // + int IList.Add(object value) + { + return Add((DataGridColumnStyle)value); + } + + void IList.Clear() + { + Clear(); + } + + bool IList.Contains(object value) + { + return items.Contains(value); + } + + int IList.IndexOf(object value) + { + return items.IndexOf(value); + } + + void IList.Insert(int index, object value) + { + throw new NotSupportedException(); + } + + void IList.Remove(object value) + { + Remove((DataGridColumnStyle)value); + } + + void IList.RemoveAt(int index) + { + RemoveAt(index); + } + + bool IList.IsFixedSize + { + get { return false; } + } + + bool IList.IsReadOnly + { + get { return false; } + } + + object IList.this[int index] + { + get { return items[index]; } + set { throw new NotSupportedException(); } + } + + void ICollection.CopyTo(Array array, int index) + { + items.CopyTo(array, index); + } + + int ICollection.Count + { + get { return items.Count; } + } + + bool ICollection.IsSynchronized + { + get { return false; } + } + + object ICollection.SyncRoot + { + get { return this; } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } + + internal GridColumnStylesCollection(DataGridTableStyle table) + { + owner = table; + } + + internal GridColumnStylesCollection(DataGridTableStyle table, bool isDefault) : this(table) + { + this.isDefault = isDefault; + } + + /// + /// Gets the list of items in the collection. + /// + protected override ArrayList List + { + get + { + return items; + } + } + + /* implemented in BaseCollection + /// + /// Gets the number of System.Windows.Forms.DataGridColumnStyle objects in the collection. + /// + /// + /// The number of System.Windows.Forms.DataGridColumnStyle objects in the System.Windows.Forms.GridColumnsStyleCollection . + /// + /// + /// The following example uses the + /// property to determine how many System.Windows.Forms.DataGridColumnStyle objects are in a System.Windows.Forms.GridColumnsStyleCollection, and uses that number to iterate through the + /// collection. + /// + /// Private Sub PrintGridColumns() + /// Dim colsCount As Integer + /// colsCount = DataGrid1.GridColumns.Count + /// Dim i As Integer + /// For i = 0 to colsCount - 1 + /// Debug.Print DataGrid1.GridColumns(i).GetType.ToString + /// Next i + /// End Sub + /// + /// + /// + /// + public override int Count { + get { + return items.Count; + } + } + */ + + /// + /// Gets the System.Windows.Forms.DataGridColumnStyle at a specified index. + /// + public DataGridColumnStyle this[int index] + { + get + { + return (DataGridColumnStyle)items[index]; + } + } + + /// + /// Gets the System.Windows.Forms.DataGridColumnStyle + /// with the specified name. + /// + public DataGridColumnStyle this[string columnName] + { + get + { + int itemCount = items.Count; + for (int i = 0; i < itemCount; ++i) + { + DataGridColumnStyle column = (DataGridColumnStyle)items[i]; + // NOTE: case-insensitive + if (string.Equals(column.MappingName, columnName, StringComparison.OrdinalIgnoreCase)) + { + return column; + } + } + + return null; + } + } + + internal DataGridColumnStyle MapColumnStyleToPropertyName(string mappingName) + { + int itemCount = items.Count; + for (int i = 0; i < itemCount; ++i) + { + DataGridColumnStyle column = (DataGridColumnStyle)items[i]; + // NOTE: case-insensitive + if (string.Equals(column.MappingName, mappingName, StringComparison.OrdinalIgnoreCase)) + { + return column; + } + } + + return null; + } + + /// + /// Gets the System.Windows.Forms.DataGridColumnStyle associated with the + /// specified . + /// + public DataGridColumnStyle this[PropertyDescriptor propertyDesciptor] + { + get + { + int itemCount = items.Count; + for (int i = 0; i < itemCount; ++i) + { + DataGridColumnStyle column = (DataGridColumnStyle)items[i]; + if (propertyDesciptor.Equals(column.PropertyDescriptor)) + { + return column; + } + } + + return null; + } + } + + internal DataGridTableStyle DataGridTableStyle + { + get + { + return owner; + } + } + + /// + /// Adds a System.Windows.Forms.DataGridColumnStyle to the System.Windows.Forms.GridColumnStylesCollection + /// + internal void CheckForMappingNameDuplicates(DataGridColumnStyle column) + { + if (string.IsNullOrEmpty(column.MappingName)) + { + return; + } + + for (int i = 0; i < items.Count; i++) + { + if (((DataGridColumnStyle)items[i]).MappingName.Equals(column.MappingName) && column != items[i]) + { + throw new ArgumentException(DSR.DataGridColumnStyleDuplicateMappingName, nameof(column)); + } + } + } + + private void ColumnStyleMappingNameChanged(object sender, EventArgs pcea) + { + OnCollectionChanged(new CollectionChangeEventArgs(CollectionChangeAction.Refresh, null)); + } + + private void ColumnStylePropDescChanged(object sender, EventArgs pcea) + { + OnCollectionChanged(new CollectionChangeEventArgs(CollectionChangeAction.Refresh, (DataGridColumnStyle)sender)); + } + + public virtual int Add(DataGridColumnStyle column) + { + if (isDefault) + { + throw new ArgumentException(DSR.DataGridDefaultColumnCollectionChanged); + } + + CheckForMappingNameDuplicates(column); + + column.SetDataGridTableInColumn(owner, true); + column.MappingNameChanged += new EventHandler(ColumnStyleMappingNameChanged); + column.PropertyDescriptorChanged += new EventHandler(ColumnStylePropDescChanged); + + // columns which are not the default should have a default + // width of DataGrid.PreferredColumnWidth + if (DataGridTableStyle != null && column.Width == -1) + { + column._width = DataGridTableStyle.PreferredColumnWidth; + } +#if false + column.AddOnPropertyChanged(owner.OnColumnChanged); +#endif + int index = items.Add(column); + OnCollectionChanged(new CollectionChangeEventArgs(CollectionChangeAction.Add, column)); + return index; + } + + public void AddRange(DataGridColumnStyle[] columns) + { + if (columns == null) + { + throw new ArgumentNullException(nameof(columns)); + } + + for (int i = 0; i < columns.Length; i++) + { + Add(columns[i]); + } + } + + // the dataGrid will need to add default columns to a default + // table when there is no match for the listName in the tableStyle + internal void AddDefaultColumn(DataGridColumnStyle column) + { +#if DEBUG + Debug.Assert(isDefault, "we should be calling this function only for default tables"); + Debug.Assert(column.IsDefault, "we should be a default column"); +#endif // DEBUG + column.SetDataGridTableInColumn(owner, true); + items.Add(column); + } + + internal void ResetDefaultColumnCollection() + { + Debug.Assert(isDefault, "we should be calling this function only for default tables"); + // unparent the edit controls + for (int i = 0; i < Count; i++) + { + this[i].ReleaseHostedControl(); + } + + // get rid of the old list and get a new empty list + items.Clear(); + } + + /// + /// Occurs when a change is made to the System.Windows.Forms.GridColumnStylesCollection. + /// + public event CollectionChangeEventHandler CollectionChanged + { + add => onCollectionChanged += value; + remove => onCollectionChanged -= value; + } + + public void Clear() + { + for (int i = 0; i < Count; i++) + { + this[i].ReleaseHostedControl(); + } + + items.Clear(); + OnCollectionChanged(new CollectionChangeEventArgs(CollectionChangeAction.Refresh, null)); + } + + /// + /// Gets a value indicating whether the System.Windows.Forms.GridColumnStylesCollection contains a System.Windows.Forms.DataGridColumnStyle associated with the + /// specified . + /// + public bool Contains(PropertyDescriptor propertyDescriptor) + { + return this[propertyDescriptor] != null; + } + + /// + /// Gets a value indicating whether the System.Windows.Forms.GridColumnsStyleCollection contains the specified System.Windows.Forms.DataGridColumnStyle. + /// + public bool Contains(DataGridColumnStyle column) + { + int index = items.IndexOf(column); + return index != -1; + } + + /// + /// Gets a value indicating whether the System.Windows.Forms.GridColumnsStyleCollection contains the System.Windows.Forms.DataGridColumnStyle with the specified name. + /// + public bool Contains(string name) + { + IEnumerator e = items.GetEnumerator(); + while (e.MoveNext()) + { + DataGridColumnStyle column = (DataGridColumnStyle)e.Current; + // NOTE: case-insensitive + if (string.Compare(column.MappingName, name, true, CultureInfo.InvariantCulture) == 0) + { + return true; + } + } + + return false; + } + + /* implemented at BaseCollection + /// + /// Gets an enumerator for the System.Windows.Forms.GridColumnsStyleCollection. + /// + /// + /// Gets an enumerator for the System.Windows.Forms.GridColumnsStyleCollection. + /// + /// + /// An + /// that can be used to iterate through the collection. + /// + /// + /// The following example gets an that iterates through the System.Windows.Forms.GridColumnsStyleCollection. and prints the + /// of each + /// associated with the object. + /// + /// Private Sub EnumerateThroughGridColumns() + /// Dim ie As System.Collections.IEnumerator + /// Dim dgCol As DataGridColumn + /// Set ie = DataGrid1.GridColumns.GetEnumerator + /// Do While ie.GetNext = True + /// Set dgCol = ie.GetObject + /// Debug.Print dgCol.DataColumn.Caption + /// Loop + /// End Sub + /// + /// + /// + /// + /// + /// + public override IEnumerator GetEnumerator() { + return items.GetEnumerator(); + } + + /// + /// Gets an enumerator for the System.Windows.Forms.GridColumnsStyleCollection + /// . + /// + /// + /// A value that indicates if the enumerator can remove elements. , if removals are allowed; otherwise, . The default is . + /// + /// + /// An that can be used to iterate through the + /// collection. + /// + /// + /// An attempt was made to remove the System.Windows.Forms.DataGridColumnStyle through the object's method. Use the System.Windows.Forms.GridColumnsStyleCollection object's method instead. + /// + /// + /// Because this implementation doesn't support the removal + /// of System.Windows.Forms.DataGridColumnStyle objects through the + /// class's method, you must use the class's + /// method instead. + /// + /// + /// The following example gets an for that iterates through the System.Windows.Forms.GridColumnsStyleCollection. If a column in the collection is of type , it is deleted. + /// + /// Private Sub RemoveBoolColumns() + /// Dim ie As System.Collections.IEnumerator + /// Dim dgCol As DataGridColumn + /// Set ie = DataGrid1.GridColumns.GetEnumerator(true) + /// Do While ie.GetNext + /// Set dgCol = ie.GetObject + /// + /// If dgCol.ToString = "DataGridBoolColumn" Then + /// DataGrid1.GridColumns.Remove dgCol + /// End If + /// Loop + /// End If + /// + /// + /// + /// + /// + /// + public override IEnumerator GetEnumerator(bool allowRemove) { + if (!allowRemove) + return GetEnumerator(); + else + throw new NotSupportedException(DSR.DataGridColumnCollectionGetEnumerator); + } + */ + + /// + /// Gets the index of a specified System.Windows.Forms.DataGridColumnStyle. + /// + public int IndexOf(DataGridColumnStyle element) + { + int itemCount = items.Count; + for (int i = 0; i < itemCount; ++i) + { + DataGridColumnStyle column = (DataGridColumnStyle)items[i]; + if (element == column) + { + return i; + } + } + + return -1; + } + + /// + /// Raises the System.Windows.Forms.GridColumnsCollection.CollectionChanged event. + /// + protected void OnCollectionChanged(CollectionChangeEventArgs e) + { + onCollectionChanged?.Invoke(this, e); + + DataGrid grid = owner.DataGrid; + if (grid != null) + { + grid.checkHierarchy = true; + } + } + + /// + /// Removes the specified System.Windows.Forms.DataGridColumnStyle from the System.Windows.Forms.GridColumnsStyleCollection. + /// + public void Remove(DataGridColumnStyle column) + { + if (isDefault) + { + throw new ArgumentException(DSR.DataGridDefaultColumnCollectionChanged); + } + + int columnIndex = -1; + int itemsCount = items.Count; + for (int i = 0; i < itemsCount; ++i) + { + if (items[i] == column) + { + columnIndex = i; + break; + } + } + + if (columnIndex == -1) + { + throw new InvalidOperationException(DSR.DataGridColumnCollectionMissing); + } + else + { + RemoveAt(columnIndex); + } + } + + /// + /// Removes the System.Windows.Forms.DataGridColumnStyle with the specified index from the System.Windows.Forms.GridColumnsStyleCollection. + /// + public void RemoveAt(int index) + { + if (isDefault) + { + throw new ArgumentException(DSR.DataGridDefaultColumnCollectionChanged); + } + + DataGridColumnStyle toRemove = (DataGridColumnStyle)items[index]; + toRemove.SetDataGridTableInColumn(null, true); + toRemove.MappingNameChanged -= new EventHandler(ColumnStyleMappingNameChanged); + toRemove.PropertyDescriptorChanged -= new EventHandler(ColumnStylePropDescChanged); +#if false + toRemove.RemoveOnPropertyChange(owner.OnColumnChanged); +#endif + items.RemoveAt(index); + OnCollectionChanged(new CollectionChangeEventArgs(CollectionChangeAction.Remove, toRemove)); + } + + public void ResetPropertyDescriptors() + { + for (int i = 0; i < Count; i++) + { + this[i].PropertyDescriptor = null; + } + } } - - public void Clear() { } - - public bool Contains(PropertyDescriptor propertyDescriptor) => throw null; - - public bool Contains(DataGridColumnStyle column) => throw null; - - public bool Contains(string name) => throw null; - - public int IndexOf(DataGridColumnStyle element) => throw null; - - protected void OnCollectionChanged(CollectionChangeEventArgs e) { } - - public void Remove(DataGridColumnStyle column) { } - - public void RemoveAt(int index) { } - - public void ResetPropertyDescriptors() { } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridTableStylesCollection.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridTableStylesCollection.cs index 7b1f7bf9c92..33dade8a20d 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridTableStylesCollection.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridTableStylesCollection.cs @@ -1,85 +1,342 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable IDE0040, IDE1006, SA1005, SA1206, SA1400, CA1510, CSIsNull001, CSIsNull002, IDE0031 + +#nullable disable + using System.Collections; using System.ComponentModel; +using System.Globalization; +using DSR = System.Windows.Forms.DataGridStrings; namespace System.Windows.Forms; + /// + /// Represents a collection of objects in the + /// control. + /// + [ListBindable(false)] + public class GridTableStylesCollection : BaseCollection, IList + { + CollectionChangeEventHandler onCollectionChanged; + readonly ArrayList items = new ArrayList(); + readonly DataGrid owner; -#nullable disable + int IList.Add(object value) + { + return Add((DataGridTableStyle)value); + } -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.DataGridMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[ListBindable(false)] -public class GridTableStylesCollection : BaseCollection, IList -{ - private GridTableStylesCollection() { } + void IList.Clear() + { + Clear(); + } - int IList.Add(object value) => throw null; + bool IList.Contains(object value) + { + return items.Contains(value); + } - void IList.Clear() { } + int IList.IndexOf(object value) + { + return items.IndexOf(value); + } - bool IList.Contains(object value) => throw null; + void IList.Insert(int index, object value) + { + throw new NotSupportedException(); + } - int IList.IndexOf(object value) => throw null; + void IList.Remove(object value) + { + Remove((DataGridTableStyle)value); + } - void IList.Insert(int index, object value) { } + void IList.RemoveAt(int index) + { + RemoveAt(index); + } - void IList.Remove(object value) { } + bool IList.IsFixedSize + { + get { return false; } + } - void IList.RemoveAt(int index) { } + bool IList.IsReadOnly + { + get { return false; } + } - bool IList.IsFixedSize => throw null; + object IList.this[int index] + { + get { return items[index]; } + set { throw new NotSupportedException(); } + } - bool IList.IsReadOnly => throw null; + void ICollection.CopyTo(Array array, int index) + { + items.CopyTo(array, index); + } - object IList.this[int index] - { - get => throw null; - set { } - } + int ICollection.Count + { + get { return items.Count; } + } - void ICollection.CopyTo(Array array, int index) { } + bool ICollection.IsSynchronized + { + get { return false; } + } - int ICollection.Count => throw null; + object ICollection.SyncRoot + { + get { return this; } + } - bool ICollection.IsSynchronized => throw null; + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } - object ICollection.SyncRoot => throw null; + internal GridTableStylesCollection(DataGrid grid) + { + owner = grid; + } - IEnumerator IEnumerable.GetEnumerator() => throw null; + protected override ArrayList List + { + get + { + return items; + } + } - public DataGridTableStyle this[int index] => throw null; + /* implemented in BaseCollection + /// + /// Retrieves the number of GridTables in the collection. + /// + /// + /// The number of GridTables in the collection. + /// + public override int Count { + get { + return items.Count; + } + } + */ - public DataGridTableStyle this[string tableName] => throw null; + /// + /// Retrieves the DataGridTable with the specified index. + /// + public DataGridTableStyle this[int index] + { + get + { + return (DataGridTableStyle)items[index]; + } + } - public virtual int Add(DataGridTableStyle table) => throw null; + /// + /// Retrieves the DataGridTable with the name provided. + /// + public DataGridTableStyle this[string tableName] + { + get + { + if (tableName == null) + { + throw new ArgumentNullException(nameof(tableName)); + } - public virtual void AddRange(DataGridTableStyle[] tables) { } + int itemCount = items.Count; + for (int i = 0; i < itemCount; ++i) + { + DataGridTableStyle table = (DataGridTableStyle)items[i]; + // NOTE: case-insensitive + if (string.Equals(table.MappingName, tableName, StringComparison.OrdinalIgnoreCase)) + { + return table; + } + } - public event CollectionChangeEventHandler CollectionChanged - { - add { } - remove { } - } + return null; + } + } + + internal void CheckForMappingNameDuplicates(DataGridTableStyle table) + { + if (string.IsNullOrEmpty(table.MappingName)) + { + return; + } + + for (int i = 0; i < items.Count; i++) + { + if (((DataGridTableStyle)items[i]).MappingName.Equals(table.MappingName) && table != items[i]) + { + throw new ArgumentException(DSR.DataGridTableStyleDuplicateMappingName, nameof(table)); + } + } + } + + /// + /// Adds a to this collection. + /// + public virtual int Add(DataGridTableStyle table) + { + // set the rowHeaderWidth on the newly added table to at least the minimum value + // on its owner + if (owner != null && owner.MinimumRowHeaderWidth() > table.RowHeaderWidth) + { + table.RowHeaderWidth = owner.MinimumRowHeaderWidth(); + } + + if (table.DataGrid != owner && table.DataGrid != null) + { + throw new ArgumentException(DSR.DataGridTableStyleCollectionAddedParentedTableStyle, nameof(table)); + } + + table.DataGrid = owner; + CheckForMappingNameDuplicates(table); + table.MappingNameChanged += new EventHandler(TableStyleMappingNameChanged); + int index = items.Add(table); + OnCollectionChanged(new CollectionChangeEventArgs(CollectionChangeAction.Add, table)); - public void Clear() { } + return index; + } - public bool Contains(DataGridTableStyle table) => throw null; + private void TableStyleMappingNameChanged(object sender, EventArgs pcea) + { + OnCollectionChanged(new CollectionChangeEventArgs(CollectionChangeAction.Refresh, null)); + } - public bool Contains(string name) => throw null; + public virtual void AddRange(DataGridTableStyle[] tables) + { + if (tables == null) + { + throw new ArgumentNullException(nameof(tables)); + } - protected void OnCollectionChanged(CollectionChangeEventArgs e) { } + foreach (DataGridTableStyle table in tables) + { + table.DataGrid = owner; + table.MappingNameChanged += new EventHandler(TableStyleMappingNameChanged); + items.Add(table); + } - public void Remove(DataGridTableStyle table) { } + OnCollectionChanged(new CollectionChangeEventArgs(CollectionChangeAction.Refresh, null)); + } - public void RemoveAt(int index) { } -} + public event CollectionChangeEventHandler CollectionChanged + { + add => onCollectionChanged += value; + remove => onCollectionChanged -= value; + } + + public void Clear() + { + for (int i = 0; i < items.Count; i++) + { + DataGridTableStyle element = (DataGridTableStyle)items[i]; + element.MappingNameChanged -= new EventHandler(TableStyleMappingNameChanged); + } + + items.Clear(); + OnCollectionChanged(new CollectionChangeEventArgs(CollectionChangeAction.Refresh, null)); + } + + /// + /// Checks to see if a DataGridTableStyle is contained in this collection. + /// + public bool Contains(DataGridTableStyle table) + { + int index = items.IndexOf(table); + return index != -1; + } + + /// + /// Checks to see if a with the given name + /// is contained in this collection. + /// + public bool Contains(string name) + { + int itemCount = items.Count; + for (int i = 0; i < itemCount; ++i) + { + DataGridTableStyle table = (DataGridTableStyle)items[i]; + // NOTE: case-insensitive + if (string.Compare(table.MappingName, name, true, CultureInfo.InvariantCulture) == 0) + { + return true; + } + } + + return false; + } + + /* + public override IEnumerator GetEnumerator() { + return items.GetEnumerator(); + } + + public override IEnumerator GetEnumerator(bool allowRemove) { + if (!allowRemove) + return GetEnumerator(); + else + throw new NotSupportedException(DSR.DataGridTableCollectionGetEnumerator); + } + */ + + protected void OnCollectionChanged(CollectionChangeEventArgs e) + { + onCollectionChanged?.Invoke(this, e); + + DataGrid grid = owner; + if (grid != null) + { + /* FOR DEMO: Microsoft: TableStylesCollection::OnCollectionChanged: set the datagridtble + DataView dataView = ((DataView) grid.DataSource); + if (dataView != null) { + DataTable dataTable = dataView.Table; + if (dataTable != null) { + if (Contains(dataTable)) { + grid.SetDataGridTable(this[dataTable]); + } + } + } + */ + grid.checkHierarchy = true; + } + } + + public void Remove(DataGridTableStyle table) + { + int tableIndex = -1; + int itemsCount = items.Count; + for (int i = 0; i < itemsCount; ++i) + { + if (items[i] == table) + { + tableIndex = i; + break; + } + } + + if (tableIndex == -1) + { + throw new ArgumentException(DSR.DataGridTableCollectionMissingTable, nameof(table)); + } + else + { + RemoveAt(tableIndex); + } + } + + public void RemoveAt(int index) + { + DataGridTableStyle element = (DataGridTableStyle)items[index]; + element.MappingNameChanged -= new EventHandler(TableStyleMappingNameChanged); + items.RemoveAt(index); + OnCollectionChanged(new CollectionChangeEventArgs(CollectionChangeAction.Remove, element)); + } + } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridTablesFactory.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridTablesFactory.cs index d1001b33f63..5dc869734a9 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridTablesFactory.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/GridTablesFactory.cs @@ -17,13 +17,14 @@ namespace System.Windows.Forms; UrlFormat = Obsoletions.SharedUrlFormat)] [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] -public sealed class GridTablesFactory +public static class GridTablesFactory { - private GridTablesFactory() { } - public static DataGridTableStyle[] CreateGridTables( DataGridTableStyle gridTable, object dataSource, string dataMember, - BindingContext bindingManager) => throw new PlatformNotSupportedException(); + BindingContext bindingManager) + { + return [gridTable]; + } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/LegacyDataGridInteropCompat.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/LegacyDataGridInteropCompat.cs new file mode 100644 index 00000000000..46867211a50 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/LegacyDataGridInteropCompat.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Windows.Forms; + +internal static class LegacyDataGridInteropCompat +{ + internal static partial class User32 + { + [DllImport(Libraries.User32, ExactSpelling = true)] + private static extern BOOL ScrollWindow(HWND hWnd, int nXAmount, int nYAmount, ref RECT rectScrollRegion, ref RECT rectClip); + + public static BOOL ScrollWindow(IHandle hWnd, int nXAmount, int nYAmount, ref RECT rectScrollRegion, ref RECT rectClip) + { + BOOL result = ScrollWindow(hWnd.Handle, nXAmount, nYAmount, ref rectScrollRegion, ref rectClip); + GC.KeepAlive(hWnd); + + return result; + } + } +} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/Triangle.TriangleDirection.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/Triangle.TriangleDirection.cs new file mode 100644 index 00000000000..94b4431b73f --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/Triangle.TriangleDirection.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable SA1518 + +namespace System.Windows.Forms; + +internal static partial class Triangle +{ + internal enum TriangleDirection + { + Up, + Down, + Left, + Right + } +} \ No newline at end of file diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/Triangle.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/Triangle.cs new file mode 100644 index 00000000000..e7c5ec34984 --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/DataGrid/Triangle.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable SA1518 + +using System.Drawing; + +namespace System.Windows.Forms; + +internal static partial class Triangle +{ + private const double TriHeightRatio = 2.5; + private const double TriWidthRatio = 0.8; + + public static void Paint( + Graphics g, + Rectangle bounds, + TriangleDirection dir, + Brush backBr, + Pen backPen1, + Pen backPen2, + Pen backPen3, + bool opaque) + { + Point[] points = BuildTrianglePoints(dir, bounds); + + if (opaque) + { + g.FillPolygon(backBr, points); + } + + g.DrawLine(backPen1, points[0], points[1]); + g.DrawLine(backPen2, points[1], points[2]); + g.DrawLine(backPen3, points[2], points[0]); + } + + private static Point[] BuildTrianglePoints(TriangleDirection dir, Rectangle bounds) + { + Point[] points = new Point[3]; + + int upDownWidth = (int)(bounds.Width * TriWidthRatio); + if (upDownWidth % 2 == 1) + { + upDownWidth++; + } + + int upDownHeight = (int)Math.Ceiling((upDownWidth / 2D) * TriHeightRatio); + + int leftRightWidth = (int)(bounds.Height * TriWidthRatio); + if (leftRightWidth % 2 == 0) + { + leftRightWidth++; + } + + int leftRightHeight = (int)Math.Ceiling((leftRightWidth / 2D) * TriHeightRatio); + + switch (dir) + { + case TriangleDirection.Up: + points[0] = new Point(0, upDownHeight); + points[1] = new Point(upDownWidth, upDownHeight); + points[2] = new Point(upDownWidth / 2, 0); + break; + case TriangleDirection.Down: + points[0] = new Point(0, 0); + points[1] = new Point(upDownWidth, 0); + points[2] = new Point(upDownWidth / 2, upDownHeight); + break; + case TriangleDirection.Left: + points[0] = new Point(leftRightWidth, 0); + points[1] = new Point(leftRightWidth, leftRightHeight); + points[2] = new Point(0, leftRightHeight / 2); + break; + case TriangleDirection.Right: + points[0] = new Point(0, 0); + points[1] = new Point(0, leftRightHeight); + points[2] = new Point(leftRightWidth, leftRightHeight / 2); + break; + default: + Debug.Fail("Unexpected triangle direction."); + break; + } + + switch (dir) + { + case TriangleDirection.Up: + case TriangleDirection.Down: + OffsetPoints( + points, + bounds.X + (bounds.Width - upDownHeight) / 2, + bounds.Y + (bounds.Height - upDownWidth) / 2); + break; + case TriangleDirection.Left: + case TriangleDirection.Right: + OffsetPoints( + points, + bounds.X + (bounds.Width - leftRightWidth) / 2, + bounds.Y + (bounds.Height - leftRightHeight) / 2); + break; + } + + return points; + } + + private static void OffsetPoints(Point[] points, int xOffset, int yOffset) + { + for (int i = 0; i < points.Length; i++) + { + points[i].X += xOffset; + points[i].Y += yOffset; + } + } +} \ No newline at end of file diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/MainMenu/MainMenu.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/MainMenu/MainMenu.cs index 5098969a376..20b6985054b 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/MainMenu/MainMenu.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/MainMenu/MainMenu.cs @@ -1,48 +1,196 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable IDE1006, CS8625, CS8618, CS8601, RS0036, RS0016 + using System.ComponentModel; +using UnsafeNativeMethods = System.Windows.Forms.LegacyMenuUnsafeNativeMethods; -namespace System.Windows.Forms; - -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.MainMenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[ToolboxItemFilter("System.Windows.Forms.MainMenu")] -public class MainMenu : Menu +namespace System.Windows.Forms { - public MainMenu() : base(items: default) => throw new PlatformNotSupportedException(); + /// + /// Represents a menu structure for a form. + /// + [ToolboxItemFilter("System.Windows.Forms.MainMenu")] + public class MainMenu : Menu + { + internal Form form; + internal Form ownerForm; // this is the form that created this menu, and is the only form allowed to dispose it. + private RightToLeft rightToLeft = RightToLeft.Inherit; + private EventHandler onCollapse; - public MainMenu(IContainer container) : this() => throw new PlatformNotSupportedException(); + /// + /// Creates a new MainMenu control. + /// + public MainMenu() + : base(null) + { + } - public MainMenu(MenuItem[] items) : base(items: items) => throw new PlatformNotSupportedException(); + /// + /// Initializes a new instance of the class with the specified container. + /// + public MainMenu(IContainer container) : this() + { + ArgumentNullException.ThrowIfNull(container); - public event EventHandler Collapse - { - add { } - remove { } - } + container.Add(this); + } - [Localizable(true)] - [AmbientValue(RightToLeft.Inherit)] - public virtual RightToLeft RightToLeft - { - get => throw null; - set { } - } + /// + /// Creates a new MainMenu control with the given items to start + /// with. + /// + public MainMenu(MenuItem[] items) + : base(items) + { + } + + public event EventHandler Collapse + { + add => onCollapse += value; + remove => onCollapse -= value; + } + + /// + /// This is used for international applications where the language + /// is written from RightToLeft. When this property is true, + /// text alignment and reading order will be from right to left. + /// + // Add an AmbientValue attribute so that the Reset context menu becomes available in the Property Grid. + [ + Localizable(true), + AmbientValue(RightToLeft.Inherit) + ] + public virtual RightToLeft RightToLeft + { + get + { + if (rightToLeft == RightToLeft.Inherit) + { + if (form is not null) + { + return form.RightToLeft; + } + else + { + return RightToLeft.Inherit; + } + } + else + { + return rightToLeft; + } + } + set + { + // valid values are 0x0 to 0x2 + if (value is < RightToLeft.No or > RightToLeft.Inherit) + { + throw new InvalidEnumArgumentException(nameof(RightToLeft), (int)value, typeof(RightToLeft)); + } + + if (rightToLeft != value) + { + rightToLeft = value; + UpdateRtl((value == RightToLeft.Yes)); + } + } + } + + internal override bool RenderIsRightToLeft + { + get + { + return (RightToLeft == RightToLeft.Yes && (form is null || !form.IsMirrored)); + } + } - public virtual MainMenu CloneMenu() => throw null; + /// + /// Creates a new MainMenu object which is a dupliate of this one. + /// + public virtual MainMenu CloneMenu() + { + MainMenu newMenu = new MainMenu(); + newMenu.CloneMenu(this); + return newMenu; + } - public Form GetForm() => throw null; + protected override IntPtr CreateMenuHandle() + { + return UnsafeNativeMethods.CreateMenu(); + } - protected internal virtual void OnCollapse(EventArgs e) { } + /// + /// Clears out this MainMenu object and discards all of it's resources. + /// If the menu is parented in a form, it is disconnected from that as + /// well. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (form is not null && (ownerForm is null || form == ownerForm)) + { + form.Menu = null; + } + } + + base.Dispose(disposing); + } + + /// + /// Indicates which form in which we are currently residing [if any] + /// + public Form GetForm() + { + return form; + } + + internal Form GetFormUnsafe() + { + return form; + } + + internal override void ItemsChanged(int change) + { + base.ItemsChanged(change); + form?.MenuChanged(change, this); + } + + internal virtual void ItemsChanged(int change, Menu menu) + { + form?.MenuChanged(change, menu); + } + + /// + /// Fires the collapse event + /// + protected internal virtual void OnCollapse(EventArgs e) + { + onCollapse?.Invoke(this, e); + } + + /// + /// Returns true if the RightToLeft should be persisted in code gen. + /// + internal virtual bool ShouldSerializeRightToLeft() + { + if (RightToLeft == RightToLeft.Inherit) + { + return false; + } + + return true; + } + + /// + /// Returns a string representation for this control. + /// + public override string ToString() + { + // Removing GetForm information + return base.ToString(); + } + } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBar.StatusBarPanelCollection.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBar.StatusBarPanelCollection.cs deleted file mode 100644 index 6238b478caf..00000000000 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBar.StatusBarPanelCollection.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; -using System.ComponentModel; - -namespace System.Windows.Forms; - -#nullable disable - -public partial class StatusBar -{ - /// - /// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.StatusBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] - [Browsable(false)] - [ListBindable(false)] - public class StatusBarPanelCollection : IList - { - public StatusBarPanelCollection(StatusBar owner) => throw new PlatformNotSupportedException(); - - public virtual StatusBarPanel this[int index] - { - get => throw null; - set { } - } - - object IList.this[int index] - { - get => throw null; - set { } - } - - public virtual StatusBarPanel this[string key] => throw null; - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public int Count => throw null; - - object ICollection.SyncRoot => throw null; - - bool ICollection.IsSynchronized => throw null; - - bool IList.IsFixedSize => throw null; - - public bool IsReadOnly => throw null; - - public virtual StatusBarPanel Add(string text) => throw null; - - public virtual int Add(StatusBarPanel value) => throw null; - - int IList.Add(object value) => throw null; - - public virtual void AddRange(StatusBarPanel[] panels) { } - - public bool Contains(StatusBarPanel panel) => throw null; - - bool IList.Contains(object panel) => throw null; - - public virtual bool ContainsKey(string key) => throw null; - - public int IndexOf(StatusBarPanel panel) => throw null; - - int IList.IndexOf(object panel) => throw null; - - public virtual int IndexOfKey(string key) => throw null; - - public virtual void Insert(int index, StatusBarPanel value) { } - - void IList.Insert(int index, object value) { } - - public virtual void Clear() { } - - public virtual void Remove(StatusBarPanel value) { } - - void IList.Remove(object value) { } - - public virtual void RemoveAt(int index) { } - - public virtual void RemoveByKey(string key) { } - - void ICollection.CopyTo(Array dest, int index) { } - - public IEnumerator GetEnumerator() => throw null; - } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBar.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBar.cs index dac60ecd9bb..656a38a79cc 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBar.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBar.cs @@ -1,23 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable RS0016, CA1822 + +using System.Collections; using System.ComponentModel; +using System.Drawing; +using System.Globalization; using System.Runtime.InteropServices; +using System.Windows.Forms.VisualStyles; namespace System.Windows.Forms; #nullable disable -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.StatusBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] [ComVisible(true)] [ClassInterface(ClassInterfaceType.AutoDispatch)] [DefaultEvent(nameof(PanelClick))] @@ -25,157 +21,1339 @@ namespace System.Windows.Forms; [Designer($"System.Windows.Forms.Design.StatusBarDesigner, {Assemblies.SystemDesign}")] public partial class StatusBar : Control { - // Adding this constructor to suppress creation of a default one. - public StatusBar() => throw new PlatformNotSupportedException(); + private const int SimpleIndex = 0xFF; + private const string BadStatusBarPanelMessage = "Value must be a StatusBarPanel."; + + private static readonly object s_panelClickEvent = new(); + private static readonly object s_drawItemEvent = new(); + + private static VisualStyleRenderer s_renderer; + + private readonly List _panels = []; + private Point _lastClick; + private StatusBarPanelCollection _panelsCollection; + private ControlToolTip _tooltips; + private int _panelsRealized; + private int _sizeGripWidth; + private bool _layoutDirty; + private bool _showPanels; + private bool _sizeGrip = true; + private string _simpleText; + + public StatusBar() + { + SetStyle(ControlStyles.UserPaint | ControlStyles.Selectable, false); + + Dock = DockStyle.Bottom; + TabStop = false; + } + + private static VisualStyleRenderer VisualStyleRenderer + { + get + { + if (!VisualStyleRenderer.IsSupported) + { + s_renderer = null; + + return null; + } + + s_renderer ??= new VisualStyleRenderer(VisualStyleElement.ToolBar.Button.Normal); + + return s_renderer; + } + } + + private int SizeGripWidth + { + get + { + if (_sizeGripWidth != 0) + { + return _sizeGripWidth; + } + + if (Application.RenderWithVisualStyles && VisualStyleRenderer is not null) + { + using Graphics graphics = Graphics.FromHwndInternal(Handle); + VisualStyleRenderer renderer = VisualStyleRenderer; + + renderer.SetParameters(VisualStyleElement.Status.GripperPane.Normal); + Size paneSize = renderer.GetPartSize(graphics, ThemeSizeType.True); + + renderer.SetParameters(VisualStyleElement.Status.Gripper.Normal); + Size gripSize = renderer.GetPartSize(graphics, ThemeSizeType.True); + + _sizeGripWidth = Math.Max(paneSize.Width + gripSize.Width, 16); + } + else + { + _sizeGripWidth = 16; + } + + return _sizeGripWidth; + } + } [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] public override Color BackColor { - get => throw null; + get => SystemColors.Control; set { } } [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] - public override Drawing.Image BackgroundImage + public new event EventHandler BackColorChanged { - get => throw null; - set { } + add => base.BackColorChanged += value; + remove => base.BackColorChanged -= value; + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override Image BackgroundImage + { + get => base.BackgroundImage; + set => base.BackgroundImage = value; + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public new event EventHandler BackgroundImageChanged + { + add => base.BackgroundImageChanged += value; + remove => base.BackgroundImageChanged -= value; } [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] public override ImageLayout BackgroundImageLayout { - get => throw null; - set { } + get => base.BackgroundImageLayout; + set => base.BackgroundImageLayout = value; } - [Localizable(true)] - [DefaultValue(DockStyle.Bottom)] - public override DockStyle Dock + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public new event EventHandler BackgroundImageLayoutChanged { - get => throw null; - set { } + add => base.BackgroundImageLayoutChanged += value; + remove => base.BackgroundImageLayoutChanged -= value; + } + + protected override CreateParams CreateParams + { + get + { + CreateParams cp = base.CreateParams; + cp.ClassName = LegacyStatusBarInterop.StatusClassName; + cp.Style |= LegacyStatusBarInterop.CcsNoParentAlign | LegacyStatusBarInterop.CcsNoResize; + + if (_sizeGrip) + { + cp.Style |= LegacyStatusBarInterop.SbarsSizeGrip; + } + else + { + cp.Style &= ~LegacyStatusBarInterop.SbarsSizeGrip; + } + + return cp; + } } + protected override ImeMode DefaultImeMode => ImeMode.Disable; + + protected override Size DefaultSize => new(100, 22); + [EditorBrowsable(EditorBrowsableState.Never)] protected override bool DoubleBuffered { - get => throw null; - set { } + get => base.DoubleBuffered; + set => base.DoubleBuffered = value; } [Localizable(true)] - public override Drawing.Font Font + [DefaultValue(DockStyle.Bottom)] + public override DockStyle Dock { - get => throw null; - set { } + get => base.Dock; + set => base.Dock = value; + } + + [Localizable(true)] + public override Font Font + { + get => base.Font; + set + { + base.Font = value; + SetPanelContentsWidths(newPanels: false); + } } [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] public override Color ForeColor { - get => throw null; - set { } + get => base.ForeColor; + set => base.ForeColor = value; + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public new event EventHandler ForeColorChanged + { + add => base.ForeColorChanged += value; + remove => base.ForeColorChanged -= value; } [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] public new ImeMode ImeMode { - get => throw null; - set { } + get => base.ImeMode; + set => base.ImeMode = value; + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public new event EventHandler ImeModeChanged + { + add => base.ImeModeChanged += value; + remove => base.ImeModeChanged -= value; } [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [Localizable(true)] + [SRCategory(nameof(SR.CatAppearance))] [MergableProperty(false)] - public StatusBarPanelCollection Panels => throw null; + public StatusBarPanelCollection Panels => _panelsCollection ??= new StatusBarPanelCollection(this); + + [Localizable(true)] + public override string Text + { + get => _simpleText ?? string.Empty; + set + { + value ??= string.Empty; + + if (string.Equals(Text, value, StringComparison.Ordinal)) + { + return; + } + + _simpleText = value.Length == 0 ? null : value; + SetSimpleText(_simpleText); + + OnTextChanged(EventArgs.Empty); + } + } + [SRCategory(nameof(SR.CatBehavior))] [DefaultValue(false)] public bool ShowPanels { - get => throw null; - set { } + get => _showPanels; + set + { + if (_showPanels == value) + { + return; + } + + _showPanels = value; + _layoutDirty = true; + + if (!IsHandleCreated) + { + return; + } + + LegacyStatusBarInterop.SendMessageIntPtr( + Handle, + LegacyStatusBarInterop.SbSimple, + (IntPtr)(value ? 0 : 1), + IntPtr.Zero); + + if (value) + { + PerformLayout(); + RealizePanels(); + } + else if (_tooltips is not null) + { + foreach (StatusBarPanel panel in _panels) + { + _tooltips.SetTool(panel, null); + } + } + + SetSimpleText(_simpleText); + } } + [SRCategory(nameof(SR.CatAppearance))] [DefaultValue(true)] public bool SizingGrip { - get => throw null; - set { } + get => _sizeGrip; + set + { + if (_sizeGrip == value) + { + return; + } + + _sizeGrip = value; + _sizeGripWidth = 0; + RecreateHandle(); + } } [DefaultValue(false)] - public new bool TabStop { get => throw null; set { } } + public new bool TabStop + { + get => base.TabStop; + set => base.TabStop = value; + } - [Localizable(true)] - public override string Text { get => throw null; set { } } + [SRCategory(nameof(SR.CatBehavior))] + public event StatusBarDrawItemEventHandler DrawItem + { + add => Events.AddHandler(s_drawItemEvent, value); + remove => Events.RemoveHandler(s_drawItemEvent, value); + } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler BackColorChanged + [SRCategory(nameof(SR.CatMouse))] + public event StatusBarPanelClickEventHandler PanelClick { - add { } - remove { } + add => Events.AddHandler(s_panelClickEvent, value); + remove => Events.RemoveHandler(s_panelClickEvent, value); } [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler BackgroundImageChanged + public new event PaintEventHandler Paint { - add { } - remove { } + add => base.Paint += value; + remove => base.Paint -= value; } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler BackgroundImageLayoutChanged + internal bool ArePanelsRealized() => _showPanels && IsHandleCreated; + + internal void DirtyLayout() => _layoutDirty = true; + + internal void ForcePanelUpdate() { - add { } - remove { } + if (!ArePanelsRealized()) + { + return; + } + + _layoutDirty = true; + SetPanelContentsWidths(newPanels: true); + PerformLayout(); + RealizePanels(); } - public event StatusBarDrawItemEventHandler DrawItem + internal void RealizePanels() { - add { } - remove { } + int oldCount = _panelsRealized; + _panelsRealized = 0; + + if (_panels.Count == 0) + { + LegacyStatusBarInterop.SendMessageString(Handle, LegacyStatusBarInterop.SbSetText, IntPtr.Zero, string.Empty); + } + + int index = 0; + for (; index < _panels.Count; index++) + { + try + { + _panels[index].Realize(); + _panelsRealized++; + } + catch + { + } + } + + for (; index < oldCount; index++) + { + LegacyStatusBarInterop.SendMessageString(Handle, LegacyStatusBarInterop.SbSetText, IntPtr.Zero, null); + } } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler ForeColorChanged + internal void RemoveAllPanelsWithoutUpdate() { - add { } - remove { } + foreach (StatusBarPanel panel in _panels) + { + panel.ParentInternal = null; + } + + _panels.Clear(); + + if (_showPanels) + { + ApplyPanelWidths(); + ForcePanelUpdate(); + } } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler ImeModeChanged + internal void SetPanelContentsWidths(bool newPanels) { - add { } - remove { } + bool changed = false; + + foreach (StatusBarPanel panel in _panels) + { + if (panel.AutoSize != StatusBarPanelAutoSize.Contents) + { + continue; + } + + int newWidth = panel.GetContentsWidth(newPanels); + if (panel.Width == newWidth) + { + continue; + } + + panel.Width = newWidth; + changed = true; + } + + if (!changed) + { + return; + } + + DirtyLayout(); + PerformLayout(); } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event PaintEventHandler Paint + internal void UpdateTooltip(StatusBarPanel panel) { - add { } - remove { } + if (_tooltips is null) + { + if (IsHandleCreated && !DesignMode) + { + _tooltips = new ControlToolTip(this); + } + else + { + return; + } + } + + if (panel.Parent == this && panel.ToolTipText.Length > 0) + { + int border = SystemInformation.Border3DSize.Width; + ControlToolTip.Tool tool = _tooltips.GetTool(panel) ?? new ControlToolTip.Tool(); + tool.Text = panel.ToolTipText; + tool.Bounds = new Rectangle(panel.Right - panel.Width + border, 0, panel.Width - border, Height); + _tooltips.SetTool(panel, tool); + + return; + } + + _tooltips.SetTool(panel, null); } - public event StatusBarPanelClickEventHandler PanelClick + internal void UpdatePanelIndex() + { + for (int i = 0; i < _panels.Count; i++) + { + _panels[i].Index = i; + } + } + + protected override unsafe void CreateHandle() + { + if (!RecreatingHandle) + { + using ThemingScope scope = new(Application.UseVisualStyles); + PInvoke.InitCommonControlsEx(new INITCOMMONCONTROLSEX + { + dwSize = (uint)sizeof(INITCOMMONCONTROLSEX), + dwICC = INITCOMMONCONTROLSEX_ICC.ICC_BAR_CLASSES + }); + } + + base.CreateHandle(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && _panelsCollection is not null) + { + StatusBarPanel[] panelCopy = [.. _panels]; + _panelsCollection.Clear(); + + foreach (StatusBarPanel panel in panelCopy) + { + panel.Dispose(); + } + } + + base.Dispose(disposing); + } + + protected virtual void OnDrawItem(StatusBarDrawItemEventArgs sbdievent) + => (Events[s_drawItemEvent] as StatusBarDrawItemEventHandler)?.Invoke(this, sbdievent); + + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + + if (!DesignMode) + { + _tooltips = new ControlToolTip(this); + } + + if (!_showPanels) + { + LegacyStatusBarInterop.SendMessageIntPtr( + Handle, + LegacyStatusBarInterop.SbSimple, + (IntPtr)1, + IntPtr.Zero); + SetSimpleText(_simpleText); + + return; + } + + ForcePanelUpdate(); + } + + protected override void OnHandleDestroyed(EventArgs e) + { + base.OnHandleDestroyed(e); + + _tooltips?.Dispose(); + _tooltips = null; + } + + protected override void OnLayout(LayoutEventArgs levent) + { + if (_showPanels) + { + LayoutPanels(); + + if (IsHandleCreated && _panelsRealized != _panels.Count) + { + RealizePanels(); + } + } + + base.OnLayout(levent); + } + + protected override void OnMouseDown(MouseEventArgs e) + { + _lastClick = e.Location; + + base.OnMouseDown(e); + } + + protected virtual void OnPanelClick(StatusBarPanelClickEventArgs e) + => (Events[s_panelClickEvent] as StatusBarPanelClickEventHandler)?.Invoke(this, e); + + protected override void OnResize(EventArgs e) + { + Invalidate(); + + base.OnResize(e); + } + + public override string ToString() + { + string text = base.ToString(); + text += ", Panels.Count: " + Panels.Count.ToString(CultureInfo.CurrentCulture); + + if (Panels.Count > 0) + { + text += ", Panels[0]: " + Panels[0]; + } + + return text; + } + + protected override unsafe void WndProc(ref Message m) + { + switch (m.MsgInternal) + { + case PInvokeCore.WM_NCHITTEST: + WmNCHitTest(ref m); + break; + case MessageId.WM_REFLECT_DRAWITEM: + WmDrawItem(ref m); + break; + case PInvokeCore.WM_NOTIFY: + case MessageId.WM_REFLECT_NOTIFY: + NMHDR* note = (NMHDR*)(nint)m.LParamInternal; + if (note->code is PInvoke.NM_CLICK or PInvoke.NM_RCLICK or PInvoke.NM_DBLCLK or PInvoke.NM_RDBLCLK) + { + WmNotifyNmClick(note->code); + } + else + { + base.WndProc(ref m); + } + + break; + default: + base.WndProc(ref m); + break; + } + } + + private void ApplyPanelWidths() + { + if (!IsHandleCreated) + { + return; + } + + if (_panels.Count == 0) + { + int[] offsets = [Size.Width - (_sizeGrip ? SizeGripWidth : 0)]; + LegacyStatusBarInterop.SendMessageParts(Handle, LegacyStatusBarInterop.SbSetParts, (IntPtr)1, offsets); + LegacyStatusBarInterop.SendMessageIntPtr(Handle, LegacyStatusBarInterop.SbSetIcon, IntPtr.Zero, IntPtr.Zero); + + return; + } + + int[] offsets2 = new int[_panels.Count]; + int currentOffset = 0; + for (int i = 0; i < _panels.Count; i++) + { + StatusBarPanel panel = _panels[i]; + currentOffset += panel.Width; + offsets2[i] = currentOffset; + panel.Right = currentOffset; + } + + LegacyStatusBarInterop.SendMessageParts(Handle, LegacyStatusBarInterop.SbSetParts, (IntPtr)offsets2.Length, offsets2); + + foreach (StatusBarPanel panel in _panels) + { + UpdateTooltip(panel); + } + + _layoutDirty = false; + } + + private void LayoutPanels() + { + int fixedWidth = 0; + List springPanels = []; + bool changed = false; + + foreach (StatusBarPanel panel in _panels) + { + if (panel.AutoSize == StatusBarPanelAutoSize.Spring) + { + springPanels.Add(panel); + } + else + { + fixedWidth += panel.Width; + } + } + + if (springPanels.Count > 0) + { + int springPanelsLeft = springPanels.Count; + int leftoverWidth = Bounds.Width - fixedWidth; + if (_sizeGrip) + { + leftoverWidth -= SizeGripWidth; + } + + int lastLeftoverWidth = int.MinValue; + + while (springPanelsLeft > 0) + { + int widthOfSpringPanel = leftoverWidth / springPanelsLeft; + if (leftoverWidth == lastLeftoverWidth) + { + break; + } + + lastLeftoverWidth = leftoverWidth; + + for (int i = 0; i < springPanels.Count; i++) + { + StatusBarPanel panel = springPanels[i]; + if (panel is null) + { + continue; + } + + if (widthOfSpringPanel < panel.MinWidth) + { + if (panel.Width != panel.MinWidth) + { + changed = true; + } + + panel.Width = panel.MinWidth; + springPanels[i] = null; + springPanelsLeft--; + leftoverWidth -= panel.MinWidth; + + continue; + } + + if (panel.Width != widthOfSpringPanel) + { + changed = true; + } + + panel.Width = widthOfSpringPanel; + } + } + } + + if (changed || _layoutDirty) + { + ApplyPanelWidths(); + } + } + + private void SetSimpleText(string simpleText) { - add { } - remove { } + if (_showPanels || !IsHandleCreated) + { + return; + } + + int wParam = SimpleIndex | LegacyStatusBarInterop.SbtNoBorders; + if (RightToLeft == RightToLeft.Yes) + { + wParam |= LegacyStatusBarInterop.SbtRtlReading; + } + + LegacyStatusBarInterop.SendMessageString(Handle, LegacyStatusBarInterop.SbSetText, (IntPtr)wParam, simpleText); } - protected virtual void OnPanelClick(StatusBarPanelClickEventArgs e) { } + private void WmDrawItem(ref Message m) + { + unsafe + { + DRAWITEMSTRUCT* dis = (DRAWITEMSTRUCT*)(nint)m.LParamInternal; + if (dis->itemID >= (uint)_panels.Count) + { + Debug.Fail("OwnerDraw item out of range"); + + return; + } + + StatusBarPanel panel = _panels[(int)dis->itemID]; + + using Graphics graphics = Graphics.FromHdcInternal((IntPtr)dis->hDC); + Rectangle bounds = Rectangle.FromLTRB(dis->rcItem.left, dis->rcItem.top, dis->rcItem.right, dis->rcItem.bottom); + OnDrawItem(new StatusBarDrawItemEventArgs(graphics, Font, bounds, (int)dis->itemID, DrawItemState.None, panel, ForeColor, BackColor)); + m.ResultInternal = (LRESULT)1; + } + } + + private void WmNCHitTest(ref Message m) + { + int x = PARAM.SignedLOWORD(m.LParamInternal); + Rectangle bounds = Bounds; + bool callSuper = true; + + if (x > bounds.X + bounds.Width - SizeGripWidth) + { + Control parent = ParentInternal; + if (parent is Form form) + { + FormBorderStyle borderStyle = form.FormBorderStyle; + if (borderStyle is not FormBorderStyle.Sizable and not FormBorderStyle.SizableToolWindow) + { + callSuper = false; + } + + if (!form.TopLevel || Dock != DockStyle.Bottom) + { + callSuper = false; + } + + if (callSuper) + { + foreach (Control child in parent.Controls) + { + if (child != this && child.Dock == DockStyle.Bottom && child.Top > Top) + { + callSuper = false; + + break; + } + } + } + } + else + { + callSuper = false; + } + } + + if (callSuper) + { + base.WndProc(ref m); + + return; + } + + m.ResultInternal = (LRESULT)(nint)PInvoke.HTCLIENT; + } + + private void WmNotifyNmClick(uint code) + { + if (!_showPanels) + { + return; + } + + int currentOffset = 0; + int index = -1; + for (int i = 0; i < _panels.Count; i++) + { + StatusBarPanel panel = _panels[i]; + currentOffset += panel.Width; + if (_lastClick.X < currentOffset) + { + index = i; + + break; + } + } + + if (index == -1) + { + return; + } + + MouseButtons button = code switch + { + PInvoke.NM_RCLICK or PInvoke.NM_RDBLCLK => MouseButtons.Right, + _ => MouseButtons.Left + }; + int clicks = code switch + { + PInvoke.NM_DBLCLK or PInvoke.NM_RDBLCLK => 2, + _ => 1 + }; + + OnPanelClick(new StatusBarPanelClickEventArgs(_panels[index], button, clicks, _lastClick.X, _lastClick.Y)); + } + + [ListBindable(false)] + public class StatusBarPanelCollection : IList + { + private readonly StatusBar _owner; + private int _lastAccessedIndex = -1; + + public StatusBarPanelCollection(StatusBar owner) + { + ArgumentNullException.ThrowIfNull(owner); + + _owner = owner; + } + + public virtual StatusBarPanel this[int index] + { + get => _owner._panels[index]; + set + { + ArgumentNullException.ThrowIfNull(value); + + _owner._layoutDirty = true; + + if (value.Parent is not null) + { + throw new ArgumentException(SR.ObjectHasParent, nameof(value)); + } + + if (index < 0 || index >= _owner._panels.Count) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + StatusBarPanel oldPanel = _owner._panels[index]; + oldPanel.ParentInternal = null; + value.ParentInternal = _owner; + if (value.AutoSize == StatusBarPanelAutoSize.Contents) + { + value.Width = value.GetContentsWidth(newPanel: true); + } + + _owner._panels[index] = value; + value.Index = index; + + if (_owner.ArePanelsRealized()) + { + _owner.PerformLayout(); + value.Realize(); + } + } + } + + object IList.this[int index] + { + get => this[index]; + set + { + if (value is not StatusBarPanel panel) + { + throw new ArgumentException(BadStatusBarPanelMessage, nameof(value)); + } + + this[index] = panel; + } + } + + public virtual StatusBarPanel this[string key] + { + get + { + if (string.IsNullOrEmpty(key)) + { + return null; + } + + int index = IndexOfKey(key); + + return IsValidIndex(index) ? this[index] : null; + } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public int Count => _owner._panels.Count; + + public bool IsReadOnly => false; + + object ICollection.SyncRoot => this; + + bool ICollection.IsSynchronized => false; + + bool IList.IsFixedSize => false; + + public virtual StatusBarPanel Add(string text) + { + StatusBarPanel panel = new() + { + Text = text + }; + + Add(panel); + + return panel; + } + + public virtual int Add(StatusBarPanel value) + { + int index = _owner._panels.Count; + Insert(index, value); + + return index; + } + + int IList.Add(object value) + { + if (value is not StatusBarPanel panel) + { + throw new ArgumentException(BadStatusBarPanelMessage, nameof(value)); + } + + return Add(panel); + } + + public virtual void AddRange(StatusBarPanel[] panels) + { + ArgumentNullException.ThrowIfNull(panels); + + foreach (StatusBarPanel panel in panels) + { + Add(panel); + } + } + + public virtual void Clear() + { + _owner.RemoveAllPanelsWithoutUpdate(); + _owner.PerformLayout(); + } + + public bool Contains(StatusBarPanel panel) => IndexOf(panel) != -1; + + bool IList.Contains(object panel) => panel is StatusBarPanel statusBarPanel && Contains(statusBarPanel); + + public virtual bool ContainsKey(string key) => IsValidIndex(IndexOfKey(key)); + + void ICollection.CopyTo(Array dest, int index) => ((ICollection)_owner._panels).CopyTo(dest, index); + + public IEnumerator GetEnumerator() => _owner._panels.GetEnumerator(); + + public int IndexOf(StatusBarPanel panel) + { + for (int i = 0; i < Count; i++) + { + if (this[i] == panel) + { + return i; + } + } + + return -1; + } + + int IList.IndexOf(object panel) => panel is StatusBarPanel statusBarPanel ? IndexOf(statusBarPanel) : -1; + + public virtual int IndexOfKey(string key) + { + if (string.IsNullOrEmpty(key)) + { + return -1; + } + + if (IsValidIndex(_lastAccessedIndex) + && WindowsFormsUtils.SafeCompareStrings(this[_lastAccessedIndex].Name, key, ignoreCase: true)) + { + return _lastAccessedIndex; + } + + for (int i = 0; i < Count; i++) + { + if (WindowsFormsUtils.SafeCompareStrings(this[i].Name, key, ignoreCase: true)) + { + _lastAccessedIndex = i; + + return i; + } + } + + _lastAccessedIndex = -1; + + return -1; + } + + public virtual void Insert(int index, StatusBarPanel value) + { + ArgumentNullException.ThrowIfNull(value); + + _owner._layoutDirty = true; + if (value.Parent != _owner && value.Parent is not null) + { + throw new ArgumentException(SR.ObjectHasParent, nameof(value)); + } + + if (index < 0 || index > _owner._panels.Count) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + value.ParentInternal = _owner; + if (value.AutoSize == StatusBarPanelAutoSize.Contents) + { + value.Width = value.GetContentsWidth(newPanel: true); + } + + _owner._panels.Insert(index, value); + _owner.UpdatePanelIndex(); + _owner.ForcePanelUpdate(); + } + + void IList.Insert(int index, object value) + { + if (value is not StatusBarPanel panel) + { + throw new ArgumentException(BadStatusBarPanelMessage, nameof(value)); + } + + Insert(index, panel); + } + + public virtual void Remove(StatusBarPanel value) + { + ArgumentNullException.ThrowIfNull(value); + + if (value.Parent != _owner) + { + return; + } + + RemoveAt(value.Index); + } + + void IList.Remove(object value) + { + if (value is StatusBarPanel panel) + { + Remove(panel); + } + } + + public virtual void RemoveAt(int index) + { + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + StatusBarPanel panel = _owner._panels[index]; + _owner._panels.RemoveAt(index); + panel.ParentInternal = null; + _owner.UpdateTooltip(panel); + _owner.UpdatePanelIndex(); + _owner.ForcePanelUpdate(); + } + + public virtual void RemoveByKey(string key) + { + int index = IndexOfKey(key); + if (IsValidIndex(index)) + { + RemoveAt(index); + } + } + + private bool IsValidIndex(int index) => index >= 0 && index < Count; + } + + private sealed class ControlToolTip : IHandle, IDisposable + { + public sealed class Tool + { + public Rectangle Bounds = Rectangle.Empty; + public string Text; + internal nint _id = -1; + } + + private readonly Dictionary _tools = []; + private readonly Control _parent; + private readonly ToolTipNativeWindow _window; + private int _nextId; + + public ControlToolTip(Control parent) + { + _parent = parent; + _window = new ToolTipNativeWindow(this); + } + + public HWND Handle + { + get + { + if (_window.Handle == IntPtr.Zero) + { + CreateHandle(); + } + + return (HWND)_window.Handle; + } + } + + private bool IsHandleCreated => _window.Handle != IntPtr.Zero; + + private CreateParams CreateParams => new() + { + Parent = IntPtr.Zero, + ClassName = PInvoke.TOOLTIPS_CLASS, + Style = (int)PInvoke.TTS_ALWAYSTIP, + ExStyle = 0, + Caption = null + }; + + public void Dispose() + { + if (IsHandleCreated) + { + _window.DestroyHandle(); + _tools.Clear(); + } + + GC.SuppressFinalize(this); + } + + public Tool GetTool(object key) => _tools.TryGetValue(key, out Tool tool) ? tool : null; + + public void SetTool(object key, Tool tool) + { + bool remove = _tools.TryGetValue(key, out Tool existingTool); + bool add = tool is not null; + bool update = add && remove && tool._id == existingTool._id; + + if (update) + { + UpdateTool(tool); + } + else + { + if (remove) + { + RemoveTool(existingTool); + } + + if (add) + { + AddTool(tool); + } + } + + if (tool is not null) + { + _tools[key] = tool; + } + else + { + _tools.Remove(key); + } + } + + private void AddTool(Tool tool) + { + if (tool is null || string.IsNullOrEmpty(tool.Text)) + { + return; + } + + ToolInfoWrapper info = GetToolInfo(tool); + if (info.SendMessage(this, PInvoke.TTM_ADDTOOLW) == IntPtr.Zero) + { + throw new InvalidOperationException(SR.ToolTipAddFailed); + } + } + + private void AssignId(Tool tool) => tool._id = _nextId++; + + private unsafe void CreateHandle() + { + if (IsHandleCreated) + { + return; + } + + PInvoke.InitCommonControlsEx(new INITCOMMONCONTROLSEX + { + dwSize = (uint)sizeof(INITCOMMONCONTROLSEX), + dwICC = INITCOMMONCONTROLSEX_ICC.ICC_TAB_CLASSES + }); + _window.CreateHandle(CreateParams); + PInvoke.SetWindowPos( + (HWND)_window.Handle, + HWND.HWND_TOPMOST, + 0, + 0, + 0, + 0, + SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + PInvokeCore.SendMessage(this, PInvoke.TTM_SETMAXTIPWIDTH, 0, SystemInformation.MaxWindowTrackSize.Width); + } + + private ToolInfoWrapper GetMinToolInfo(Tool tool) + { + if (tool._id < 0) + { + AssignId(tool); + } + + return new ToolInfoWrapper(_parent, id: tool._id); + } + + private ToolInfoWrapper GetToolInfo(Tool tool) + { + ToolInfoWrapper toolInfo = GetMinToolInfo(tool); + toolInfo.Info.uFlags |= TOOLTIP_FLAGS.TTF_TRANSPARENT | TOOLTIP_FLAGS.TTF_SUBCLASS; + if (_parent.RightToLeft == RightToLeft.Yes && !_parent.IsMirrored) + { + toolInfo.Info.uFlags |= TOOLTIP_FLAGS.TTF_RTLREADING; + } + + toolInfo.Text = tool.Text; + toolInfo.Info.rect = tool.Bounds; + + return toolInfo; + } + + private void RemoveTool(Tool tool) + { + if (tool is null || string.IsNullOrEmpty(tool.Text) || tool._id < 0) + { + return; + } + + GetMinToolInfo(tool).SendMessage(this, PInvoke.TTM_DELTOOLW); + } + + private void UpdateTool(Tool tool) + { + if (tool is null || string.IsNullOrEmpty(tool.Text) || tool._id < 0) + { + return; + } + + GetToolInfo(tool).SendMessage(this, PInvoke.TTM_SETTOOLINFOW); + } + + private void WndProc(ref Message msg) + { + if (msg.MsgInternal == PInvokeCore.WM_SETFOCUS) + { + return; + } + + _window.DefWndProc(ref msg); + } + + private sealed class ToolTipNativeWindow : NativeWindow + { + private readonly ControlToolTip _control; + + internal ToolTipNativeWindow(ControlToolTip control) + { + _control = control; + } + + protected override void WndProc(ref Message m) => _control.WndProc(ref m); + } + } +} + +internal static class LegacyStatusBarInterop +{ + internal const string StatusClassName = "msctls_statusbar32"; + + internal const int CcsNoResize = 0x0004; + internal const int CcsNoParentAlign = 0x0008; + internal const int SbarsSizeGrip = 0x0100; + + internal const int SbtNoBorders = 0x0100; + internal const int SbtPopout = 0x0200; + internal const int SbtRtlReading = 0x0400; + internal const int SbtOwnerDraw = 0x1000; + + internal const int SbSetText = (int)PInvokeCore.WM_USER + 1; + internal const int SbSetParts = (int)PInvokeCore.WM_USER + 4; + internal const int SbGetRect = (int)PInvokeCore.WM_USER + 10; + internal const int SbSimple = (int)PInvokeCore.WM_USER + 9; + internal const int SbSetIcon = (int)PInvokeCore.WM_USER + 15; + + [DllImport(Libraries.User32, EntryPoint = "SendMessageW", CharSet = CharSet.Unicode)] + internal static extern nint SendMessageString(IntPtr hWnd, int msg, IntPtr wParam, string lParam); + + [DllImport(Libraries.User32, EntryPoint = "SendMessageW")] + internal static extern nint SendMessageIntPtr(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + + [DllImport(Libraries.User32, EntryPoint = "SendMessageW")] + internal static extern nint SendMessageParts(IntPtr hWnd, int msg, IntPtr wParam, [In] int[] lParam); - protected virtual void OnDrawItem(StatusBarDrawItemEventArgs sbdievent) { } + [DllImport(Libraries.User32, EntryPoint = "SendMessageW")] + internal static extern nint SendMessageRect(IntPtr hWnd, int msg, IntPtr wParam, ref RECT lParam); } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarDrawItemEventArgs.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarDrawItemEventArgs.cs index b31a1193567..a6ef95274f1 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarDrawItemEventArgs.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarDrawItemEventArgs.cs @@ -1,37 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; +#pragma warning disable RS0036 + using System.Drawing; namespace System.Windows.Forms; -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.StatusBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] public class StatusBarDrawItemEventArgs : DrawItemEventArgs { - public StatusBarDrawItemEventArgs( - Graphics g, - Font font, - Rectangle r, - int itemId, - DrawItemState itemState, - StatusBarPanel panel) : base( - graphics: g, - font: font, - rect: r, - index: itemId, - state: itemState) => throw new PlatformNotSupportedException(); + public StatusBarDrawItemEventArgs(Graphics g, Font font, Rectangle r, int itemId, DrawItemState itemState, StatusBarPanel panel) + : base(g, font, r, itemId, itemState) + { + Panel = panel; + } public StatusBarDrawItemEventArgs( Graphics g, @@ -41,14 +23,11 @@ public StatusBarDrawItemEventArgs( DrawItemState itemState, StatusBarPanel panel, Color foreColor, - Color backColor) : base( - graphics: g, - font: font, - rect: r, - index: itemId, - state: itemState, - foreColor: foreColor, - backColor: backColor) => throw new PlatformNotSupportedException(); + Color backColor) + : base(g, font, r, itemId, itemState, foreColor, backColor) + { + Panel = panel; + } - public StatusBarPanel Panel => throw null; + public StatusBarPanel Panel { get; } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarDrawItemEventHandler.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarDrawItemEventHandler.cs index cbf19aa4e41..67f55f0ae2e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarDrawItemEventHandler.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarDrawItemEventHandler.cs @@ -1,20 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; +#pragma warning disable RS0036 namespace System.Windows.Forms; -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.StatusBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] public delegate void StatusBarDrawItemEventHandler(object sender, StatusBarDrawItemEventArgs sbdevent); diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanel.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanel.cs index 1e44a1d1c7e..21413f27a9a 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanel.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanel.cs @@ -1,124 +1,481 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable RS0016, IDE0031 + using System.ComponentModel; using System.Drawing; +using System.Runtime.InteropServices; namespace System.Windows.Forms; #nullable disable -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.StatusBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] [ToolboxItem(false)] [DesignTimeVisible(false)] [DefaultProperty(nameof(Text))] public class StatusBarPanel : Component, ISupportInitialize { - // Added public constructor to suppress creation of the default one. - public StatusBarPanel() => throw new PlatformNotSupportedException(); + private const int DefaultWidth = 100; + private const int DefaultMinWidth = 10; + private const int PanelTextInset = 3; + private const int PanelGap = 2; + + private HorizontalAlignment _alignment = HorizontalAlignment.Left; + private Icon _icon; + private StatusBar _parent; + private StatusBarPanelAutoSize _autoSize = StatusBarPanelAutoSize.None; + private StatusBarPanelBorderStyle _borderStyle = StatusBarPanelBorderStyle.Sunken; + private StatusBarPanelStyle _style = StatusBarPanelStyle.Text; + private bool _initializing; + private int _index; + private int _minWidth = DefaultMinWidth; + private int _right; + private int _width = DefaultWidth; + private object _userData; + private string _name = string.Empty; + private string _text = string.Empty; + private string _toolTipText = string.Empty; + [SRCategory(nameof(SR.CatAppearance))] [DefaultValue(HorizontalAlignment.Left)] [Localizable(true)] public HorizontalAlignment Alignment { - get => throw null; - set { } + get => _alignment; + set + { + if (!Enum.IsDefined(value)) + { + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(HorizontalAlignment)); + } + + if (_alignment == value) + { + return; + } + + _alignment = value; + Realize(); + } } + [SRCategory(nameof(SR.CatAppearance))] [DefaultValue(StatusBarPanelAutoSize.None)] [RefreshProperties(RefreshProperties.All)] public StatusBarPanelAutoSize AutoSize { - get => throw null; - set { } + get => _autoSize; + set + { + if (!Enum.IsDefined(value)) + { + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(StatusBarPanelAutoSize)); + } + + if (_autoSize == value) + { + return; + } + + _autoSize = value; + UpdateSize(); + } } + [SRCategory(nameof(SR.CatAppearance))] [DefaultValue(StatusBarPanelBorderStyle.Sunken)] - [Runtime.InteropServices.DispId(-504)] + [DispId(-504)] public StatusBarPanelBorderStyle BorderStyle { - get => throw null; - set { } + get => _borderStyle; + set + { + if (!Enum.IsDefined(value)) + { + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(StatusBarPanelBorderStyle)); + } + + if (_borderStyle == value) + { + return; + } + + _borderStyle = value; + Realize(); + + if (Created) + { + _parent.Invalidate(); + } + } } + internal bool Created => _parent is not null && _parent.ArePanelsRealized(); + + [SRCategory(nameof(SR.CatAppearance))] [DefaultValue(null)] [Localizable(true)] public Icon Icon { - get => throw null; - set { } + get => _icon; + set + { + _icon = value is not null + && (value.Height > SystemInformation.SmallIconSize.Height || value.Width > SystemInformation.SmallIconSize.Width) + ? new Icon(value, SystemInformation.SmallIconSize) + : value; + + if (Created) + { + LegacyStatusBarInterop.SendMessageIntPtr( + _parent.Handle, + LegacyStatusBarInterop.SbSetIcon, + (IntPtr)GetIndex(), + _icon is null ? IntPtr.Zero : _icon.Handle); + } + + UpdateSize(); + + if (Created) + { + _parent.Invalidate(); + } + } } - [DefaultValue(10)] + internal int Index + { + get => _index; + set => _index = value; + } + + [SRCategory(nameof(SR.CatBehavior))] + [DefaultValue(DefaultMinWidth)] [Localizable(true)] [RefreshProperties(RefreshProperties.All)] public int MinWidth { - get => throw null; - set { } + get => _minWidth; + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + + if (_minWidth == value) + { + return; + } + + _minWidth = value; + UpdateSize(); + if (_minWidth > Width) + { + Width = value; + } + } } + [SRCategory(nameof(SR.CatAppearance))] [Localizable(true)] public string Name { - get => throw null; - set { } + get => WindowsFormsUtils.GetComponentName(this, _name); + set + { + _name = value ?? string.Empty; + if (Site is not null) + { + Site.Name = _name; + } + } } [Browsable(false)] - public StatusBar Parent => throw null; + public StatusBar Parent => _parent; + internal StatusBar ParentInternal + { + set => _parent = value; + } + + internal int Right + { + get => _right; + set => _right = value; + } + + [SRCategory(nameof(SR.CatAppearance))] [DefaultValue(StatusBarPanelStyle.Text)] public StatusBarPanelStyle Style { - get => throw null; - set { } + get => _style; + set + { + if (!Enum.IsDefined(value)) + { + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(StatusBarPanelStyle)); + } + + if (_style == value) + { + return; + } + + _style = value; + Realize(); + + if (Created) + { + _parent.Invalidate(); + } + } } + [SRCategory(nameof(SR.CatData))] [Localizable(false)] [Bindable(true)] + [SRDescription(nameof(SR.ControlTagDescr))] [DefaultValue(null)] [TypeConverter(typeof(StringConverter))] public object Tag { - get => throw null; - set { } + get => _userData; + set => _userData = value; } + [SRCategory(nameof(SR.CatAppearance))] [Localizable(true)] [DefaultValue("")] public string Text { - get => throw null; - set { } + get => _text ?? string.Empty; + set + { + value ??= string.Empty; + + if (string.Equals(Text, value, StringComparison.Ordinal)) + { + return; + } + + _text = value.Length == 0 ? null : value; + Realize(); + UpdateSize(); + } } + [SRCategory(nameof(SR.CatAppearance))] [Localizable(true)] [DefaultValue("")] public string ToolTipText { - get => throw null; - set { } + get => _toolTipText ?? string.Empty; + set + { + value ??= string.Empty; + + if (string.Equals(ToolTipText, value, StringComparison.Ordinal)) + { + return; + } + + _toolTipText = value.Length == 0 ? null : value; + + if (Created) + { + _parent.UpdateTooltip(this); + } + } } [Localizable(true)] - [DefaultValue(100)] + [SRCategory(nameof(SR.CatAppearance))] + [DefaultValue(DefaultWidth)] public int Width { - get => throw null; - set { } + get => _width; + set + { + if (!_initializing && value < _minWidth) + { + throw new ArgumentOutOfRangeException(nameof(Width), SR.WidthGreaterThanMinWidth); + } + + _width = value; + UpdateSize(); + } + } + + public void BeginInit() => _initializing = true; + + protected override void Dispose(bool disposing) + { + if (disposing && _parent is not null) + { + int index = GetIndex(); + if (index != -1) + { + _parent.Panels.RemoveAt(index); + } + } + + base.Dispose(disposing); + } + + public void EndInit() + { + _initializing = false; + + if (Width < MinWidth) + { + Width = MinWidth; + } + } + + internal int GetContentsWidth(bool newPanel) + { + string text = newPanel ? _text ?? string.Empty : Text; + + using Graphics graphics = _parent.CreateGraphicsInternal(); + Size size = Size.Ceiling(graphics.MeasureString(text, _parent.Font)); + if (_icon is not null) + { + size.Width += _icon.Size.Width + 5; + } + + int width = size.Width + SystemInformation.BorderSize.Width * 2 + PanelTextInset * 2 + PanelGap; + + return Math.Max(width, _minWidth); + } + + internal void Realize() + { + if (!Created) + { + return; + } + + string text = _text ?? string.Empty; + HorizontalAlignment alignment = _alignment; + if (_parent.RightToLeft == RightToLeft.Yes) + { + alignment = alignment switch + { + HorizontalAlignment.Left => HorizontalAlignment.Right, + HorizontalAlignment.Right => HorizontalAlignment.Left, + _ => alignment + }; + } + + string sendText = alignment switch + { + HorizontalAlignment.Center => "\t" + text, + HorizontalAlignment.Right => "\t\t" + text, + _ => text + }; + + int border = _borderStyle switch + { + StatusBarPanelBorderStyle.None => LegacyStatusBarInterop.SbtNoBorders, + StatusBarPanelBorderStyle.Raised => LegacyStatusBarInterop.SbtPopout, + _ => 0 + }; + + if (_style == StatusBarPanelStyle.OwnerDraw) + { + border |= LegacyStatusBarInterop.SbtOwnerDraw; + } + + int wParam = GetIndex() | border; + if (_parent.RightToLeft == RightToLeft.Yes) + { + wParam |= LegacyStatusBarInterop.SbtRtlReading; + } + + int result = (int)LegacyStatusBarInterop.SendMessageString( + _parent.Handle, + LegacyStatusBarInterop.SbSetText, + (IntPtr)wParam, + sendText); + + if (result == 0) + { + throw new InvalidOperationException(SR.UnableToSetPanelText); + } + + if (_icon is not null && _style != StatusBarPanelStyle.OwnerDraw) + { + LegacyStatusBarInterop.SendMessageIntPtr( + _parent.Handle, + LegacyStatusBarInterop.SbSetIcon, + (IntPtr)GetIndex(), + _icon.Handle); + } + else + { + LegacyStatusBarInterop.SendMessageIntPtr( + _parent.Handle, + LegacyStatusBarInterop.SbSetIcon, + (IntPtr)GetIndex(), + IntPtr.Zero); + } + + if (_style != StatusBarPanelStyle.OwnerDraw) + { + return; + } + + RECT rect = default; + result = (int)LegacyStatusBarInterop.SendMessageRect( + _parent.Handle, + LegacyStatusBarInterop.SbGetRect, + (IntPtr)GetIndex(), + ref rect); + + if (result != 0) + { + _parent.Invalidate(Rectangle.FromLTRB(rect.left, rect.top, rect.right, rect.bottom)); + } + } + + public override string ToString() => "StatusBarPanel: {" + Text + '}'; + + private void ApplyContentSizing() + { + if (_autoSize != StatusBarPanelAutoSize.Contents || _parent is null) + { + return; + } + + int newWidth = GetContentsWidth(newPanel: false); + if (newWidth == Width) + { + return; + } + + Width = newWidth; + if (Created) + { + _parent.DirtyLayout(); + _parent.PerformLayout(); + } } - public void BeginInit() { } + private int GetIndex() => _index; - public void EndInit() { } + private void UpdateSize() + { + if (_autoSize == StatusBarPanelAutoSize.Contents) + { + ApplyContentSizing(); + + return; + } + + if (Created) + { + _parent.DirtyLayout(); + _parent.PerformLayout(); + } + } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelAutoSize.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelAutoSize.cs index b78874cffd5..2c5bbc03a3b 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelAutoSize.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelAutoSize.cs @@ -1,23 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; - namespace System.Windows.Forms; /// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. +/// Specifies how a panel on a status bar changes when the status bar resizes. /// -[Obsolete( - Obsoletions.StatusBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] public enum StatusBarPanelAutoSize { + /// + /// The panel does not change its size when the status bar resizes. + /// None = 1, + + /// + /// The panel shares the available status bar space with other spring panels. + /// Spring = 2, + + /// + /// The width of the panel is determined by its contents. + /// Contents = 3, } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelBorderStyle.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelBorderStyle.cs index d110afee09c..edc01d4150a 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelBorderStyle.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelBorderStyle.cs @@ -1,23 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; - namespace System.Windows.Forms; /// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. +/// Specifies the border style of a panel on the . /// -[Obsolete( - Obsoletions.StatusBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] public enum StatusBarPanelBorderStyle { + /// + /// No border. + /// None = 1, + + /// + /// A raised border. + /// Raised = 2, + + /// + /// A sunken border. + /// Sunken = 3, } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelClickEventArgs.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelClickEventArgs.cs index 7d88956e421..733a51f55da 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelClickEventArgs.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelClickEventArgs.cs @@ -1,30 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; +#pragma warning disable RS0036 namespace System.Windows.Forms; -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.StatusBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] public class StatusBarPanelClickEventArgs : MouseEventArgs { - public StatusBarPanelClickEventArgs( - StatusBarPanel statusBarPanel, - MouseButtons button, - int clicks, - int x, - int y) : base(button: button, clicks: clicks, x: x, y: y, delta: 0) => throw new PlatformNotSupportedException(); + public StatusBarPanelClickEventArgs(StatusBarPanel statusBarPanel, MouseButtons button, int clicks, int x, int y) + : base(button, clicks, x, y, 0) + { + StatusBarPanel = statusBarPanel; + } - public StatusBarPanel StatusBarPanel => throw null; + public StatusBarPanel StatusBarPanel { get; } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelClickEventHandler.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelClickEventHandler.cs index 91dca75d577..339fda96350 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelClickEventHandler.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelClickEventHandler.cs @@ -1,20 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; +#pragma warning disable RS0036 namespace System.Windows.Forms; -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.StatusBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] public delegate void StatusBarPanelClickEventHandler(object sender, StatusBarPanelClickEventArgs e); diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelStyle.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelStyle.cs index c524ccbcc4d..6a8a959c2ad 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelStyle.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/StatusBar/StatusBarPanelStyle.cs @@ -1,22 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; - namespace System.Windows.Forms; /// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. +/// Specifies whether a panel on a status bar is owner drawn or system drawn. /// -[Obsolete( - Obsoletions.StatusBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] public enum StatusBarPanelStyle { + /// + /// The panel is drawn by the system. + /// Text = 1, + + /// + /// The panel is drawn by the owner. + /// OwnerDraw = 2, } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/LegacyToolBarInteropCompat.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/LegacyToolBarInteropCompat.cs new file mode 100644 index 00000000000..91e8d38fa5d --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/LegacyToolBarInteropCompat.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE1006, SA1518 + +using System.Runtime.InteropServices; + +namespace System.Windows.Forms; + +internal static class LegacyToolBarSR +{ + public const string ToolBarBadToolBarButton = "Value must be a ToolBarButton."; + public const string ToolBarButtonInvalidDropDownMenuType = "DropDownMenu must be a ContextMenu."; +} + +internal static class LegacyToolBarNativeMethods +{ + public const string WC_TOOLBAR = "ToolbarWindow32"; + public const int TPM_LEFTALIGN = 0x0000; + public const int TPM_LEFTBUTTON = 0x0000; + public const int TPM_VERTICAL = 0x0040; + public const int CCS_NORESIZE = 0x0004; + public const int CCS_NOPARENTALIGN = 0x0008; + public const int CCS_NODIVIDER = 0x0040; + public const int TBSTYLE_TOOLTIPS = 0x0100; + public const int TBSTYLE_WRAPABLE = 0x0200; + public const int TBSTYLE_FLAT = 0x0800; + public const int TBSTYLE_LIST = 0x1000; + public const int TBSTYLE_BUTTON = 0x0000; + public const int TBSTYLE_SEP = 0x0001; + public const int TBSTYLE_CHECK = 0x0002; + public const int TBSTYLE_DROPDOWN = 0x0008; + public const int TBSTYLE_EX_DRAWDDARROWS = 0x00000001; + public const int TBSTATE_CHECKED = 0x01; + public const int TBSTATE_PRESSED = 0x02; + public const int TBSTATE_ENABLED = 0x04; + public const int TBSTATE_HIDDEN = 0x08; + public const int TBSTATE_INDETERMINATE = 0x10; + public const uint TBIF_IMAGE = 0x00000001; + public const uint TBIF_TEXT = 0x00000002; + public const uint TBIF_STATE = 0x00000004; + public const uint TBIF_STYLE = 0x00000008; + public const uint TBIF_COMMAND = 0x00000020; + public const uint TBIF_SIZE = 0x00000040; + public const uint TBN_QUERYINSERT = unchecked((uint)-706); + public const uint TBN_DROPDOWN = unchecked((uint)-710); + public const uint TBN_HOTITEMCHANGE = unchecked((uint)-713); + + [Flags] + public enum HICF + { + HICF_OTHER = 0x00000000, + HICF_MOUSE = 0x00000001, + HICF_ARROWKEYS = 0x00000002, + HICF_ACCELERATOR = 0x00000004, + HICF_DUPACCEL = 0x00000008, + HICF_ENTERING = 0x00000010, + HICF_LEAVING = 0x00000020, + HICF_RESELECT = 0x00000040, + HICF_LMOUSE = 0x00000080, + HICF_TOGGLEDROPDOWN = 0x00000100, + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMTOOLBAR + { + public NMHDR hdr; + public int iItem; + public TBBUTTON tbButton; + public int cchText; + public IntPtr pszText; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMTBHOTITEM + { + public NMHDR hdr; + public int idOld; + public int idNew; + public HICF dwFlags; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TBBUTTON + { + public int iBitmap; + public int idCommand; + public byte fsState; + public byte fsStyle; + public byte bReserved0; + public byte bReserved1; + public IntPtr dwData; + public IntPtr iString; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class TOOLTIPTEXT + { + public NMHDR hdr; + public string? lpszText; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)] + public string? szText = null; + + public IntPtr hinst; + public int uFlags; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct TBBUTTONINFO + { + public int cbSize; + public int dwMask; + public int idCommand; + public int iImage; + public byte fsState; + public byte fsStyle; + public short cx; + public IntPtr lParam; + public IntPtr pszText; + public int cchText; + } + + public static class Util + { + public static int MAKELONG(int low, int high) => (high << 16) | (low & 0xffff); + + public static IntPtr MAKELPARAM(int low, int high) => (IntPtr)MAKELONG(low, high); + + public static int HIWORD(int n) => (n >> 16) & 0xffff; + + public static int LOWORD(int n) => n & 0xffff; + + public static int LOWORD(nint n) => PARAM.LOWORD(n); + } +} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBar.ToolBarButtonCollection.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBar.ToolBarButtonCollection.cs deleted file mode 100644 index 38c2ebaf7f5..00000000000 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBar.ToolBarButtonCollection.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; -using System.ComponentModel; - -namespace System.Windows.Forms; - -#nullable disable - -public partial class ToolBar -{ - /// - /// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.ToolBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] - [Browsable(false)] - public class ToolBarButtonCollection : IList - { - public ToolBarButtonCollection(ToolBar owner) => throw new PlatformNotSupportedException(); - - public virtual ToolBarButton this[int index] - { - get => throw null; - set { } - } - - object IList.this[int index] - { - get => throw null; - set { } - } - - public virtual ToolBarButton this[string key] => throw null; - - [Browsable(false)] - public int Count => throw null; - - object ICollection.SyncRoot => throw null; - - bool ICollection.IsSynchronized => throw null; - - bool IList.IsFixedSize => throw null; - - public bool IsReadOnly => throw null; - - public int Add(ToolBarButton button) => throw null; - - public int Add(string text) => throw null; - - int IList.Add(object button) => throw null; - - public void AddRange(ToolBarButton[] buttons) { } - - public void Clear() { } - - public bool Contains(ToolBarButton button) => throw null; - - bool IList.Contains(object button) => throw null; - - public virtual bool ContainsKey(string key) => throw null; - - void ICollection.CopyTo(Array dest, int index) { } - - public int IndexOf(ToolBarButton button) => throw null; - - int IList.IndexOf(object button) => throw null; - - public virtual int IndexOfKey(string key) => throw null; - - public void Insert(int index, ToolBarButton button) { } - - void IList.Insert(int index, object button) { } - - public void RemoveAt(int index) { } - - public virtual void RemoveByKey(string key) { } - - public void Remove(ToolBarButton button) { } - - void IList.Remove(object button) { } - - public IEnumerator GetEnumerator() => throw null; - } -} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBar.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBar.cs index a034a9733bf..3c5a2387e12 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBar.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBar.cs @@ -1,283 +1,2228 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +#pragma warning disable CS8632, CSIsNull001, CSIsNull002, IDE1006, IDE0002, CA1510, WFDEV001, CA1805, CA2020, RS0016, SA1005, SA1129, SA1131, SA1206, SA1505, SA1508, SA1518, IDE0031, IDE0036, IDE0059, IDE0073, IDE0078, IDE0270 + +using System.Collections; using System.ComponentModel; using System.Drawing; +using System.Globalization; using System.Runtime.InteropServices; - -namespace System.Windows.Forms; +using ToolBarNativeMethods = System.Windows.Forms.LegacyToolBarNativeMethods; #nullable disable -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.ToolBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[ComVisible(true)] -[ClassInterface(ClassInterfaceType.AutoDispatch)] -[DefaultEvent(nameof(ButtonClick))] -[Designer($"System.Windows.Forms.Design.ToolBarDesigner, {Assemblies.SystemDesign}")] -[DefaultProperty(nameof(Buttons))] -public partial class ToolBar : Control +namespace System.Windows.Forms { - // Suppress creation of the default constructor by the compiler. This class should not be constructed. - public ToolBar() => throw new PlatformNotSupportedException(); - - [DefaultValue(ToolBarAppearance.Normal)] - [Localizable(true)] - public ToolBarAppearance Appearance + /// + /// Represents a Windows toolbar. + /// + [ + ComVisible(true), + ClassInterface(ClassInterfaceType.AutoDispatch), + DefaultEvent(nameof(ButtonClick)), + Designer($"System.Windows.Forms.Design.ToolBarDesigner, {Assemblies.SystemDesign}"), + DefaultProperty(nameof(Buttons)) + ] + public class ToolBar : Control { - get => throw null; - set { } - } + private readonly ToolBarButtonCollection buttonsCollection; - [DefaultValue(true)] - [Localizable(true)] - [Browsable(true)] - [EditorBrowsable(EditorBrowsableState.Always)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] - public override bool AutoSize - { - get => throw null; - set { } - } + /// + /// The size of a button in the ToolBar + /// + internal Size buttonSize = System.Drawing.Size.Empty; - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override Color BackColor - { - get => throw null; - set { } - } + /// + /// This is used by our autoSizing support. + /// + private int requestedSize; + /// + /// This represents the width of the drop down arrow we have if the + /// DropDownArrows property is true. this value is used by the ToolBarButton + /// objects to compute their size + /// + internal const int DDARROW_WIDTH = 15; - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override Image BackgroundImage - { - get => throw null; - set { } - } + /// + /// Indicates what our appearance will be. This will either be normal + /// or flat. + /// + private ToolBarAppearance appearance = ToolBarAppearance.Normal; - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override ImageLayout BackgroundImageLayout - { - get => throw null; - set { } - } + /// + /// Indicates whether or not we have a border + /// + private BorderStyle borderStyle = System.Windows.Forms.BorderStyle.None; - [DefaultValue(BorderStyle.None)] - [DispId(-504)] - public BorderStyle BorderStyle - { - get => throw null; - set { } - } + /// + /// The array of buttons we're working with. + /// + private ToolBarButton[] buttons; - [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] - [Localizable(true)] - [MergableProperty(false)] - public ToolBarButtonCollection Buttons => throw null; + /// + /// The number of buttons we're working with + /// + private int buttonCount = 0; - [RefreshProperties(RefreshProperties.All)] - [Localizable(true)] - public Size ButtonSize - { - get => throw null; - set { } - } + /// + /// Indicates if text captions should go underneath images in buttons or + /// to the right of them + /// + private ToolBarTextAlign textAlign = ToolBarTextAlign.Underneath; - [DefaultValue(true)] - public bool Divider - { - get => throw null; - set { } - } + /// + /// The ImageList object that contains the main images for our control. + /// + private ImageList imageList = null; - [Localizable(true)] - [DefaultValue(DockStyle.Top)] - public override DockStyle Dock - { - get => throw null; - set { } - } + /// + /// The maximum width of buttons currently being displayed. This is needed + /// by our autoSizing code. If this value is -1, it needs to be recomputed. + /// + private int maxWidth = -1; + private int hotItem = -1; - [EditorBrowsable(EditorBrowsableState.Never)] - protected override bool DoubleBuffered - { - get => throw null; - set { } - } + // Track the current scale factor so we can scale our buttons + private float currentScaleDX = 1.0F; + private float currentScaleDY = 1.0F; - [DefaultValue(false)] - [Localizable(true)] - public bool DropDownArrows - { - get => throw null; - set { } - } + private const int TOOLBARSTATE_wrappable = 0x00000001; + private const int TOOLBARSTATE_dropDownArrows = 0x00000002; + private const int TOOLBARSTATE_divider = 0x00000004; + private const int TOOLBARSTATE_showToolTips = 0x00000008; + private const int TOOLBARSTATE_autoSize = 0x00000010; - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override Color ForeColor - { - get => throw null; - set { } - } + // PERF: take all the bools and put them into a state variable + private Collections.Specialized.BitVector32 toolBarState; // see TOOLBARSTATE_ consts above - [DefaultValue(null)] - public ImageList ImageList - { - get => throw null; - set { } - } + // event handlers + // + private ToolBarButtonClickEventHandler onButtonClick = null; + private ToolBarButtonClickEventHandler onButtonDropDown = null; - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Advanced)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public Size ImageSize => throw null; + /// + /// Initializes a new instance of the class. + /// + public ToolBar() + : base() + { + // Set this BEFORE calling any other methods so that these defaults will be propagated + toolBarState = new Collections.Specialized.BitVector32(TOOLBARSTATE_autoSize | + TOOLBARSTATE_showToolTips | + TOOLBARSTATE_divider | + TOOLBARSTATE_dropDownArrows | + TOOLBARSTATE_wrappable); - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new ImeMode ImeMode { get => throw null; set { } } + SetStyle(ControlStyles.UserPaint, false); + SetStyle(ControlStyles.FixedHeight, AutoSize); + SetStyle(ControlStyles.FixedWidth, false); + TabStop = false; + Dock = DockStyle.Top; + buttonsCollection = new ToolBarButtonCollection(this); + } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override RightToLeft RightToLeft { get => throw null; set { } } + /// + /// Gets or sets the appearance of the toolbar + /// control and its buttons. + /// + [ + SRCategory(nameof(SR.CatBehavior)), + DefaultValue(ToolBarAppearance.Normal), + Localizable(true), + ] + public ToolBarAppearance Appearance + { + get + { + return appearance; + } - [DefaultValue(false)] - [Localizable(true)] - public bool ShowToolTips - { - get => throw null; - set { } - } + set + { + //valid values are 0x0 to 0x1 + SourceGenerated.EnumValidator.Validate(value); - [DefaultValue(false)] - public new bool TabStop - { - get => throw null; set { } - } + if (value != appearance) + { + appearance = value; + RecreateHandle(); + } + } + } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - [Bindable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public override string Text - { - get => throw null; set { } - } + /// + /// Indicates whether the toolbar + /// adjusts its size automatically based on the size of the buttons and the + /// dock style. + /// + [ + SRCategory(nameof(SR.CatBehavior)), + DefaultValue(true), + Localizable(true), + Browsable(true), + EditorBrowsable(EditorBrowsableState.Always), + DesignerSerializationVisibility(DesignerSerializationVisibility.Visible) + ] + public override bool AutoSize + { + get + { + return toolBarState[TOOLBARSTATE_autoSize]; + } - [DefaultValue(ToolBarTextAlign.Underneath)] - [Localizable(true)] - public ToolBarTextAlign TextAlign - { - get => throw null; - set { } - } + set + { + // Note that we intentionally do not call base. Toolbars size themselves by + // overriding SetBoundsCore (old RTM code). We let CommonProperties.GetAutoSize + // continue to return false to keep our LayoutEngines from messing with TextBoxes. + // This is done for backwards compatibility since the new AutoSize behavior differs. + if (AutoSize != value) + { + toolBarState[TOOLBARSTATE_autoSize] = value; + if (Dock == DockStyle.Left || Dock == DockStyle.Right) + { + SetStyle(ControlStyles.FixedWidth, AutoSize); + SetStyle(ControlStyles.FixedHeight, false); + } + else + { + SetStyle(ControlStyles.FixedHeight, AutoSize); + SetStyle(ControlStyles.FixedWidth, false); + } - [DefaultValue(true)] - [Localizable(true)] - public bool Wrappable - { - get => throw null; - set { } - } + AdjustSize(Dock); + OnAutoSizeChanged(EventArgs.Empty); + } + } + } - [Browsable(true)] - [EditorBrowsable(EditorBrowsableState.Always)] - public new event EventHandler AutoSizeChanged - { - add { } - remove { } - } + [SRCategory(nameof(SR.CatPropertyChanged)), SRDescription(nameof(SR.ControlOnAutoSizeChangedDescr))] + [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)] + new public event EventHandler AutoSizeChanged + { + add => base.AutoSizeChanged += value; + remove => base.AutoSizeChanged -= value; + } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler BackColorChanged - { - add { } - remove { } - } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public override Color BackColor + { + get + { + return base.BackColor; + } + set + { + base.BackColor = value; + } + } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler BackgroundImageChanged - { - add { } - remove { } - } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + new public event EventHandler BackColorChanged + { + add => base.BackColorChanged += value; + remove => base.BackColorChanged -= value; + } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler BackgroundImageLayoutChanged - { - add { } - remove { } - } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public override Image BackgroundImage + { + get + { + return base.BackgroundImage; + } + set + { + base.BackgroundImage = value; + } + } - public event ToolBarButtonClickEventHandler ButtonClick - { - add { } - remove { } - } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + new public event EventHandler BackgroundImageChanged + { + add => base.BackgroundImageChanged += value; + remove => base.BackgroundImageChanged -= value; + } - public event ToolBarButtonClickEventHandler ButtonDropDown - { - add { } - remove { } - } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public override ImageLayout BackgroundImageLayout + { + get + { + return base.BackgroundImageLayout; + } + set + { + base.BackgroundImageLayout = value; + } + } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler ForeColorChanged - { - add { } - remove { } - } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + new public event EventHandler BackgroundImageLayoutChanged + { + add => base.BackgroundImageLayoutChanged += value; + remove => base.BackgroundImageLayoutChanged -= value; + } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler ImeModeChanged - { - add { } - remove { } - } + /// + /// Gets or sets + /// the border style of the toolbar control. + /// + [ + SRCategory(nameof(SR.CatAppearance)), + DefaultValue(BorderStyle.None), + DispId(PInvokeCore.DISPID_BORDERSTYLE) + ] + public BorderStyle BorderStyle + { + get + { + return borderStyle; + } - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event PaintEventHandler Paint - { - add { } - remove { } - } + set + { + //valid values are 0x0 to 0x2 + SourceGenerated.EnumValidator.Validate(value); - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler RightToLeftChanged - { - add { } - remove { } - } + if (borderStyle != value) + { + borderStyle = value; - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public new event EventHandler TextChanged - { - add { } - remove { } - } + //UpdateStyles(); + RecreateHandle(); // Looks like we need to recreate the handle to avoid painting glitches + } + } + } + + /// + /// A collection of controls assigned to the + /// toolbar control. The property is read-only. + /// + [ + SRCategory(nameof(SR.CatBehavior)), + DesignerSerializationVisibility(DesignerSerializationVisibility.Content), + Localizable(true), + MergableProperty(false) + ] + public ToolBarButtonCollection Buttons + { + get + { + return buttonsCollection; + } + } + + /// + /// Gets or sets + /// the size of the buttons on the toolbar control. + /// + [ + SRCategory(nameof(SR.CatAppearance)), + RefreshProperties(RefreshProperties.All), + Localizable(true), + ] + public Size ButtonSize + { + get + { + if (buttonSize.IsEmpty) + { + + // Obtain the current buttonsize of the first button from the winctl control + // + if (IsHandleCreated && buttons is not null && buttonCount > 0) + { + int result = (int)PInvokeCore.SendMessage(this, PInvoke.TB_GETBUTTONSIZE); + if (result > 0) + { + return new Size(ToolBarNativeMethods.Util.LOWORD(result), ToolBarNativeMethods.Util.HIWORD(result)); + } + } + + if (TextAlign == ToolBarTextAlign.Underneath) + { + return new Size(39, 36); // Default button size + } + else + { + return new Size(23, 22); // Default button size + } + } + else + { + return buttonSize; + } + } + + set + { + + if (value.Width < 0 || value.Height < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, string.Format(SR.InvalidArgument, nameof(ButtonSize), value)); + } + + if (buttonSize != value) + { + buttonSize = value; + maxWidth = -1; // Force recompute of maxWidth + RecreateHandle(); + AdjustSize(Dock); + } + } + } + + /// + /// Returns the parameters needed to create the handle. Inheriting classes + /// can override this to provide extra functionality. They should not, + /// however, forget to get base.CreateParams first to get the struct + /// filled up with the basic info. + /// + protected override CreateParams CreateParams + { + get + { + CreateParams cp = base.CreateParams; + cp.ClassName = ToolBarNativeMethods.WC_TOOLBAR; + + // windows forms has it's own docking code. + // + cp.Style |= ToolBarNativeMethods.CCS_NOPARENTALIGN + | ToolBarNativeMethods.CCS_NORESIZE; + // | ToolBarNativeMethods.WS_CHILD was commented out since setTopLevel should be able to work. + + if (!Divider) + { + cp.Style |= ToolBarNativeMethods.CCS_NODIVIDER; + } + + if (Wrappable) + { + cp.Style |= ToolBarNativeMethods.TBSTYLE_WRAPABLE; + } + + if (ShowToolTips && !DesignMode) + { + cp.Style |= ToolBarNativeMethods.TBSTYLE_TOOLTIPS; + } + + cp.ExStyle &= (~(int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE); + cp.Style &= (~(int)WINDOW_STYLE.WS_BORDER); + switch (borderStyle) + { + case BorderStyle.Fixed3D: + cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE; + break; + case BorderStyle.FixedSingle: + cp.Style |= (int)WINDOW_STYLE.WS_BORDER; + break; + } + + switch (appearance) + { + case ToolBarAppearance.Normal: + break; + case ToolBarAppearance.Flat: + cp.Style |= ToolBarNativeMethods.TBSTYLE_FLAT; + break; + } + + switch (textAlign) + { + case ToolBarTextAlign.Underneath: + break; + case ToolBarTextAlign.Right: + cp.Style |= ToolBarNativeMethods.TBSTYLE_LIST; + break; + } + + return cp; + } + } + + protected override ImeMode DefaultImeMode + { + get + { + return ImeMode.Disable; + } + } + + /// + /// Deriving classes can override this to configure a default size for their control. + /// This is more efficient than setting the size in the control's constructor. + /// + protected override Size DefaultSize + { + get + { + return new Size(100, 22); + } + } + + /// + /// Gets or sets a value indicating + /// whether the toolbar displays a divider. + /// + [ + SRCategory(nameof(SR.CatAppearance)), + DefaultValue(true), + ] + public bool Divider + { + get + { + return toolBarState[TOOLBARSTATE_divider]; + } + + set + { + if (Divider != value) + { + + toolBarState[TOOLBARSTATE_divider] = value; + RecreateHandle(); + } + } + } + + /// + /// Sets the way in which this ToolBar is docked to its parent. We need to + /// override this to ensure autoSizing works correctly + /// + [ + Localizable(true), + DefaultValue(DockStyle.Top) + ] + public override DockStyle Dock + { + get { return base.Dock; } + + set + { + //valid values are 0x0 to 0x5 + SourceGenerated.EnumValidator.Validate(value); + + if (Dock != value) + { + if (value == DockStyle.Left || value == DockStyle.Right) + { + SetStyle(ControlStyles.FixedWidth, AutoSize); + SetStyle(ControlStyles.FixedHeight, false); + } + else + { + SetStyle(ControlStyles.FixedHeight, AutoSize); + SetStyle(ControlStyles.FixedWidth, false); + } + + AdjustSize(value); + base.Dock = value; + } + } + } + + /// + /// This property is overridden and hidden from statement completion + /// on controls that are based on Win32 Native Controls. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override bool DoubleBuffered + { + get + { + return base.DoubleBuffered; + } + set + { + base.DoubleBuffered = value; + } + } + + /// + /// Gets or sets a value indicating whether drop-down buttons on a + /// toolbar display down arrows. + /// + [ + DefaultValue(false), + SRCategory(nameof(SR.CatAppearance)), + Localizable(true), + ] + public bool DropDownArrows + { + get + { + return toolBarState[TOOLBARSTATE_dropDownArrows]; + } + + set + { + + if (DropDownArrows != value) + { + toolBarState[TOOLBARSTATE_dropDownArrows] = value; + RecreateHandle(); + } + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public override Color ForeColor + { + get + { + return base.ForeColor; + } + set + { + base.ForeColor = value; + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + new public event EventHandler ForeColorChanged + { + add => base.ForeColorChanged += value; + remove => base.ForeColorChanged -= value; + } + + /// + /// Gets or sets the collection of images available to the toolbar button + /// controls. + /// + [ + SRCategory(nameof(SR.CatBehavior)), + DefaultValue(null), + ] + public ImageList ImageList + { + get + { + return imageList; + } + set + { + if (value != imageList) + { + EventHandler recreateHandler = new EventHandler(ImageListRecreateHandle); + EventHandler disposedHandler = new EventHandler(DetachImageList); + + if (imageList is not null) + { + imageList.Disposed -= disposedHandler; + imageList.RecreateHandle -= recreateHandler; + } + + imageList = value; + + if (value is not null) + { + value.Disposed += disposedHandler; + value.RecreateHandle += recreateHandler; + } + + if (IsHandleCreated) + { + RecreateHandle(); + } + } + } + } + + /// + /// Gets the size of the images in the image list assigned to the + /// toolbar. + /// + [ + SRCategory(nameof(SR.CatBehavior)), + Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) + ] + public Size ImageSize + { + get + { + if (imageList is not null) + { + return imageList.ImageSize; + } + else + { + return new Size(0, 0); + } + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + new public ImeMode ImeMode + { + get + { + return base.ImeMode; + } + set + { + base.ImeMode = value; + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public new event EventHandler ImeModeChanged + { + add => base.ImeModeChanged += value; + remove => base.ImeModeChanged -= value; + } + + /// + /// The preferred height for this ToolBar control. This is + /// used by the AutoSizing code. + /// + internal int PreferredHeight + { + get + { + int height = 0; + + if (buttons is null || buttonCount == 0 || !IsHandleCreated) + { + height = ButtonSize.Height; + } + else + { + // get the first visible button and get it's height + // + RECT rect = new RECT(); + int firstVisible; + + for (firstVisible = 0; firstVisible < buttons.Length; firstVisible++) + { + if (buttons[firstVisible] is not null && buttons[firstVisible].Visible) + { + break; + } + } + + if (firstVisible == buttons.Length) + { + firstVisible = 0; + } + + PInvokeCore.SendMessage(this, PInvoke.TB_GETRECT, (WPARAM)firstVisible, ref rect); + + // height is the button's height plus some extra goo + // + height = rect.bottom - rect.top; + } + + // if the ToolBar is wrappable, and there is more than one row, make + // sure the height is correctly adjusted + // + if (Wrappable && IsHandleCreated) + { + height *= unchecked((int)PInvokeCore.SendMessage(this, PInvoke.TB_GETROWS)); + } + + height = (height > 0) ? height : 1; + + switch (borderStyle) + { + case BorderStyle.FixedSingle: + height += SystemInformation.BorderSize.Height; + break; + case BorderStyle.Fixed3D: + height += SystemInformation.Border3DSize.Height; + break; + } + + if (Divider) + { + height += 2; + } + + height += 4; + + return height; + } + + } + + /// + /// The preferred width for this ToolBar control. This is + /// used by AutoSizing code. + /// NOTE!!!!!!!!! This function assumes it's only going to get called + /// if the control is docked left or right [ie, it really + /// just returns a max width] + /// + internal int PreferredWidth + { + get + { + int width; + + // fortunately, we compute this value sometimes, so we can just + // use it if we have it. + // + if (maxWidth == -1) + { + // don't have it, have to recompute + // + if (!IsHandleCreated || buttons == null) + { + maxWidth = ButtonSize.Width; + } + else + { + + RECT rect = new RECT(); + + for (int x = 0; x < buttonCount; x++) + { + PInvokeCore.SendMessage(this, PInvoke.TB_GETRECT, 0, ref rect); + if ((rect.right - rect.left) > maxWidth) + { + maxWidth = rect.right - rect.left; + } + } + } + } + + width = maxWidth; + + if (borderStyle != BorderStyle.None) + { + width += SystemInformation.BorderSize.Height * 4 + 3; + } + + return width; + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public override RightToLeft RightToLeft + { + get + { + return base.RightToLeft; + } + set + { + base.RightToLeft = value; + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public new event EventHandler RightToLeftChanged + { + add => base.RightToLeftChanged += value; + remove => base.RightToLeftChanged -= value; + } + + /// + /// We need to track the current scale factor so that we can tell the + /// unmanaged control how to scale its buttons. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void ScaleCore(float dx, float dy) + { + currentScaleDX = dx; + currentScaleDY = dy; + base.ScaleCore(dx, dy); + UpdateButtons(); + } + + /// + /// We need to track the current scale factor so that we can tell the + /// unmanaged control how to scale its buttons. + /// + protected override void ScaleControl(SizeF factor, BoundsSpecified specified) + { + currentScaleDX = factor.Width; + currentScaleDY = factor.Height; + base.ScaleControl(factor, specified); + } + + /// + /// Gets or sets a value indicating whether the toolbar displays a + /// tool tip for each button. + /// + [ + SRCategory(nameof(SR.CatBehavior)), + DefaultValue(false), + Localizable(true), + ] + public bool ShowToolTips + { + get + { + return toolBarState[TOOLBARSTATE_showToolTips]; + } + set + { + if (ShowToolTips != value) + { + + toolBarState[TOOLBARSTATE_showToolTips] = value; + RecreateHandle(); + } + } + } + + [DefaultValue(false)] + new public bool TabStop + { + get + { + return base.TabStop; + } + set + { + base.TabStop = value; + } + } + + [ + Browsable(false), EditorBrowsable(EditorBrowsableState.Never), + Bindable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) + ] + public override string Text + { + get + { + return base.Text; + } + set + { + base.Text = value; + } + } + + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + new public event EventHandler TextChanged + { + add => base.TextChanged += value; + remove => base.TextChanged -= value; + } + + /// + /// Gets or sets the alignment of text in relation to each + /// image displayed on + /// the toolbar button controls. + /// + [ + SRCategory(nameof(SR.CatAppearance)), + DefaultValue(ToolBarTextAlign.Underneath), + Localizable(true), + ] + public ToolBarTextAlign TextAlign + { + get + { + return textAlign; + } + set + { + //valid values are 0x0 to 0x1 + SourceGenerated.EnumValidator.Validate(value); + + if (textAlign == value) + { + return; + } + + textAlign = value; + RecreateHandle(); + } + } + + /// + /// Gets + /// or sets a value + /// indicating whether the toolbar buttons wrap to the next line if the + /// toolbar becomes too small to display all the buttons + /// on the same line. + /// + [ + SRCategory(nameof(SR.CatBehavior)), + DefaultValue(true), + Localizable(true), + ] + public bool Wrappable + { + get + { + return toolBarState[TOOLBARSTATE_wrappable]; + } + set + { + if (Wrappable != value) + { + toolBarState[TOOLBARSTATE_wrappable] = value; + RecreateHandle(); + } + } + } + + /// + /// Occurs when a on the is clicked. + /// + [SRCategory(nameof(SR.CatBehavior))] + public event ToolBarButtonClickEventHandler ButtonClick + { + add => onButtonClick += value; + remove => onButtonClick -= value; + } + + /// + /// Occurs when a drop-down style or its down arrow is clicked. + /// + [SRCategory(nameof(SR.CatBehavior))] + public event ToolBarButtonClickEventHandler ButtonDropDown + { + add => onButtonDropDown += value; + remove => onButtonDropDown -= value; + } + + /// + /// ToolBar Onpaint. + /// + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public new event PaintEventHandler Paint + { + add => base.Paint += value; + remove => base.Paint -= value; + } + + /// + /// Adjusts the height or width of the ToolBar to make sure we have enough + /// room to show the buttons. + /// + // we pass in a value for dock rather than calling Dock ourselves + // because we can't change Dock until the size has been properly adjusted. + private void AdjustSize(DockStyle dock) + { + int saveSize = requestedSize; + try + { + if (dock == DockStyle.Left || dock == DockStyle.Right) + { + Width = AutoSize ? PreferredWidth : saveSize; + } + else + { + Height = AutoSize ? PreferredHeight : saveSize; + } + } + finally + { + requestedSize = saveSize; + } + } + + /// + /// This routine lets us change a bunch of things about the toolbar without + /// having each operation wait for the paint to complete. This must be + /// matched up with a call to endUpdate(). + /// + internal void BeginUpdate() + { + BeginUpdateInternal(); + } + + protected override void CreateHandle() + { + if (!RecreatingHandle) + { + using ThemingScope scope = new(Application.UseVisualStyles); + INITCOMMONCONTROLSEX icc = new INITCOMMONCONTROLSEX + { + dwICC = INITCOMMONCONTROLSEX_ICC.ICC_BAR_CLASSES + }; + PInvoke.InitCommonControlsEx(icc); + } + + base.CreateHandle(); + } + + /// + /// Resets the imageList to null. We wire this method up to the imageList's + /// Dispose event, so that we don't hang onto an imageList that's gone away. + /// + private void DetachImageList(object? sender, EventArgs e) + { + ImageList = null; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + // + lock (this) + { + // We need to mark the Disposing state here so buttonsCollection won't attempt to update + // the buttons. + bool currentDisposing = GetState(States.Disposing); + + try + { + SetState(States.Disposing, true); + + if (imageList is not null) + { + imageList.Disposed -= new EventHandler(DetachImageList); + imageList = null; + } + + if (buttonsCollection is not null) + { + ToolBarButton[] buttonCopy = new ToolBarButton[buttonsCollection.Count]; + ((ICollection)buttonsCollection).CopyTo(buttonCopy, 0); + buttonsCollection.Clear(); + + foreach (ToolBarButton b in buttonCopy) + { + b.Dispose(); + } + } + } + finally + { + SetState(States.Disposing, currentDisposing); + } + } + } + + base.Dispose(disposing); + } + + /// + /// This routine lets us change a bunch of things about the toolbar without + /// having each operation wait for the paint to complete. This must be + /// matched up with a call to beginUpdate(). + /// + internal void EndUpdate() + { + EndUpdateInternal(); + } + + /// + /// Forces the button sizes based on various different things. The default + /// ToolBar button sizing rules are pretty primitive and this tends to be + /// a little better, and lets us actually show things like DropDown Arrows + /// for ToolBars + /// + private void ForceButtonWidths() + { + if (buttons is not null && buttonSize.IsEmpty && IsHandleCreated) + { + + // force ourselves to re-compute this each time + // + maxWidth = -1; + + for (int x = 0; x < buttonCount; x++) + { + + ToolBarNativeMethods.TBBUTTONINFO tbbi = new() + { + cbSize = Marshal.SizeOf(), + cx = buttons[x].Width + }; + + if (tbbi.cx > maxWidth) + { + maxWidth = tbbi.cx; + } - protected virtual void OnButtonClick(ToolBarButtonClickEventArgs e) { } + tbbi.dwMask = (int)ToolBarNativeMethods.TBIF_SIZE; + PInvokeCore.SendMessage(this, PInvoke.TB_SETBUTTONINFO, (WPARAM)x, ref tbbi); + } + } + } - protected virtual void OnButtonDropDown(ToolBarButtonClickEventArgs e) { } + private void ImageListRecreateHandle(object sender, EventArgs e) + { + if (IsHandleCreated) + { + RecreateHandle(); + } + } - [EditorBrowsable(EditorBrowsableState.Never)] - protected override void ScaleCore(float dx, float dy) { } + private void Insert(int index, ToolBarButton button) + { + button.parent = this; + + if (buttons is null) + { + buttons = new ToolBarButton[4]; + } + else if (buttons.Length == buttonCount) + { + ToolBarButton[] newButtons = new ToolBarButton[buttonCount + 4]; + System.Array.Copy(buttons, 0, newButtons, 0, buttonCount); + buttons = newButtons; + } + + if (index < buttonCount) + { + System.Array.Copy(buttons, index, buttons, index + 1, buttonCount - index); + } + + buttons[index] = button; + buttonCount++; + } + + /// + /// Inserts a button at a given location on the toolbar control. + /// + private void InsertButton(int index, ToolBarButton value) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (index < 0 || ((buttons is not null) && (index > buttonCount))) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + // insert the button into our local array, and then into the + // real windows ToolBar control + // + Insert(index, value); + if (IsHandleCreated) + { + ToolBarNativeMethods.TBBUTTON tbbutton = value.GetTBBUTTON(index); + PInvokeCore.SendMessage(this, PInvoke.TB_INSERTBUTTON, (WPARAM)index, ref tbbutton); + } + + UpdateButtons(); + } + + /// + /// Adds a button to the ToolBar + /// + private int InternalAddButton(ToolBarButton button) + { + if (button is null) + { + throw new ArgumentNullException(nameof(button)); + } + + int index = buttonCount; + Insert(index, button); + return index; + } + + /// + /// Changes the data for a button in the ToolBar, and then does the appropriate + /// work to update the ToolBar control. + /// + internal void InternalSetButton(int index, ToolBarButton value, bool recreate, bool updateText) + { + // tragically, there doesn't appear to be a way to remove the + // string for the button if it has one, so we just have to leave + // it in there. + // + buttons[index].parent = null; + buttons[index].stringIndex = (IntPtr)(-1); + buttons[index] = value; + buttons[index].parent = this; + + if (IsHandleCreated) + { + ToolBarNativeMethods.TBBUTTONINFO tbbi = value.GetTBBUTTONINFO(updateText, index); + PInvokeCore.SendMessage(this, PInvoke.TB_SETBUTTONINFO, (WPARAM)index, ref tbbi); + + if (tbbi.pszText != IntPtr.Zero) + { + Marshal.FreeHGlobal(tbbi.pszText); + } + + if (recreate) + { + UpdateButtons(); + } + else + { + // after doing anything with the comctl ToolBar control, this + // appears to be a good idea. + // + PInvokeCore.SendMessage(this, PInvoke.TB_AUTOSIZE); + + ForceButtonWidths(); + Invalidate(); + } + } + } + + /// + /// Raises the + /// event. + /// + protected virtual void OnButtonClick(ToolBarButtonClickEventArgs e) + { + onButtonClick?.Invoke(this, e); + } + + /// + /// Raises the + /// event. + /// + protected virtual void OnButtonDropDown(ToolBarButtonClickEventArgs e) + { + onButtonDropDown?.Invoke(this, e); + } + + /// + /// Overridden from the control class so we can add all the buttons + /// and do whatever work needs to be done. + /// Don't forget to call base.OnHandleCreated. + /// + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + + // we have to set the button struct size, because they don't. + // + PInvokeCore.SendMessage(this, PInvoke.TB_BUTTONSTRUCTSIZE, (WPARAM)Marshal.SizeOf()); + + // set up some extra goo + // + if (DropDownArrows) + { + PInvokeCore.SendMessage(this, PInvoke.TB_SETEXTENDEDSTYLE, 0, (LPARAM)ToolBarNativeMethods.TBSTYLE_EX_DRAWDDARROWS); + } + + // if we have an imagelist, add it in now. + // + if (imageList is not null) + { + PInvokeCore.SendMessage(this, PInvoke.TB_SETIMAGELIST, 0, imageList.Handle); + } + + RealizeButtons(); + + // Force a repaint, as occasionally the ToolBar border does not paint properly + // (comctl ToolBar is flaky) + // + BeginUpdate(); + try + { + Size size = Size; + Size = new Size(size.Width + 1, size.Height); + Size = size; + } + finally + { + EndUpdate(); + } + } + + /// + /// The control is being resized. Make sure the width/height are correct. + /// + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + if (Wrappable) + { + AdjustSize(Dock); + } + } + + /// + /// Overridden to ensure that the buttons and the control resize properly + /// whenever the font changes. + /// + protected override void OnFontChanged(EventArgs e) + { + base.OnFontChanged(e); + if (IsHandleCreated) + { + if (!buttonSize.IsEmpty) + { + SendToolbarButtonSizeMessage(); + } + else + { + AdjustSize(Dock); + ForceButtonWidths(); + } + } + } + + /// + /// Sets all the button data into the ToolBar control + /// + private void RealizeButtons() + { + if (buttons is not null) + { + IntPtr ptbbuttons = IntPtr.Zero; + try + { + BeginUpdate(); + // go and add in all the strings for all of our buttons + // + for (int x = 0; x < buttonCount; x++) + { + if (buttons[x].Text.Length > 0) + { + string addString = buttons[x].Text + '\0'.ToString(); + buttons[x].stringIndex = PInvokeCore.SendMessage(this, PInvoke.TB_ADDSTRING, 0, addString); + } + else + { + buttons[x].stringIndex = (IntPtr)(-1); + } + } + + // insert the buttons and set their parent pointers + // + int cb = Marshal.SizeOf(); + int count = buttonCount; + ptbbuttons = Marshal.AllocHGlobal(checked(cb * count)); + + for (int x = 0; x < count; x++) + { + + ToolBarNativeMethods.TBBUTTON tbbutton = buttons[x].GetTBBUTTON(x); + Marshal.StructureToPtr(tbbutton, (IntPtr)(checked((long)ptbbuttons + (cb * x))), true); + buttons[x].parent = this; + } + + PInvokeCore.SendMessage(this, PInvoke.TB_ADDBUTTONS, (WPARAM)count, ptbbuttons); + + // after doing anything with the comctl ToolBar control, this + // appears to be a good idea. + // + PInvokeCore.SendMessage(this, PInvoke.TB_AUTOSIZE); + + // The win32 ToolBar control is somewhat unpredictable here. We + // have to set the button size after we've created all the + // buttons. Otherwise, we need to manually set the width of all + // the buttons so they look reasonable + // + if (!buttonSize.IsEmpty) + { + SendToolbarButtonSizeMessage(); + } + else + { + ForceButtonWidths(); + } + + AdjustSize(Dock); + } + finally + { + Marshal.FreeHGlobal(ptbbuttons); + EndUpdate(); + } + } + } + + private void RemoveAt(int index) + { + buttons[index].parent = null; + buttons[index].stringIndex = (IntPtr)(-1); + buttonCount--; + + if (index < buttonCount) + { + System.Array.Copy(buttons, index + 1, buttons, index, buttonCount - index); + } + + buttons[buttonCount] = null; + } + + /// + /// Resets the toolbar buttons to the minimum + /// size. + /// + private void ResetButtonSize() + { + buttonSize = Size.Empty; + RecreateHandle(); + } + + /// Sends a TB_SETBUTTONSIZE message to the unmanaged control, with size arguments properly scaled. + private void SendToolbarButtonSizeMessage() + { + PInvokeCore.SendMessage(this, PInvoke.TB_SETBUTTONSIZE, 0, ToolBarNativeMethods.Util.MAKELPARAM((int)(buttonSize.Width * currentScaleDX), (int)(buttonSize.Height * currentScaleDY))); + } + + /// + /// Overrides Control.setBoundsCore to enforce AutoSize. + /// + protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) + { + int originalHeight = height; + int originalWidth = width; + + base.SetBoundsCore(x, y, width, height, specified); + + Rectangle bounds = Bounds; + if (Dock == DockStyle.Left || Dock == DockStyle.Right) + { + if ((specified & BoundsSpecified.Width) != BoundsSpecified.None) + { + requestedSize = width; + } + + if (AutoSize) + { + width = PreferredWidth; + } + + if (width != originalWidth && Dock == DockStyle.Right) + { + int deltaWidth = originalWidth - width; + x += deltaWidth; + } + + } + else + { + if ((specified & BoundsSpecified.Height) != BoundsSpecified.None) + { + requestedSize = height; + } + + if (AutoSize) + { + height = PreferredHeight; + } + + if (height != originalHeight && Dock == DockStyle.Bottom) + { + int deltaHeight = originalHeight - height; + y += deltaHeight; + } + + } + + base.SetBoundsCore(x, y, width, height, specified); + } + + /// + /// Determines if the property needs to be persisted. + /// + private bool ShouldSerializeButtonSize() + { + return !buttonSize.IsEmpty; + } + + /// + /// Called by ToolTip to poke in that Tooltip into this ComCtl so that the Native ChildToolTip is not exposed. + /// + internal override void SetToolTip(ToolTip toolTip) + { + if (toolTip is null) + { + return; + } + + PInvokeCore.SendMessage(this, PInvoke.TB_SETTOOLTIPS, (WPARAM)toolTip.Handle); + + } + + /// + /// Returns a string representation for this control. + /// + public override string ToString() + { + string s = base.ToString(); + s += ", Buttons.Count: " + buttonCount.ToString(CultureInfo.CurrentCulture); + if (buttonCount > 0) + { + s += ", Buttons[0]: " + buttons[0].ToString(); + } + + return s; + } + + /// + /// Updates all the information in the ToolBar. Tragically, the win32 + /// control is pretty flakey, and the only real choice here is to recreate + /// the handle and re-realize all the buttons. + /// + internal void UpdateButtons() + { + if (IsHandleCreated) + { + RecreateHandle(); + } + } + + /// + /// The button clicked was a dropdown button. If it has a menu specified, + /// show it now. Otherwise, fire an onButtonDropDown event. + /// + private void WmNotifyDropDown(ref Message m) + { + var lParam = m.GetLParam(typeof(ToolBarNativeMethods.NMTOOLBAR)) ?? throw new InvalidOperationException("lparam of a received message should not be null"); + + ToolBarNativeMethods.NMTOOLBAR nmTB = (ToolBarNativeMethods.NMTOOLBAR)lParam; + + ToolBarButton tbb = buttons[nmTB.iItem]; + if (tbb is null) + { + throw new InvalidOperationException($"ToolBar button at index {nmTB.iItem} was not found."); + } + + OnButtonDropDown(new ToolBarButtonClickEventArgs(tbb)); + + Menu menu = tbb.DropDownMenu; + if (menu is not null) + { + RECT rc = new RECT(); + LegacyMenuNativeMethods.TPMPARAMS tpm = new(); + + PInvokeCore.SendMessage(this, PInvoke.TB_GETRECT, (WPARAM)nmTB.iItem, ref rc); + + if ((menu.GetType()).IsAssignableFrom(typeof(ContextMenu))) + { + ((ContextMenu)menu).Show(this, new Point(rc.left, rc.bottom)); + } + else + { + MainMenu main = menu.GetMainMenu(); + if (main is not null) + { + main.ProcessInitMenuPopup(menu.Handle); + } + + PInvokeCore.MapWindowPoints((HWND)nmTB.hdr.hwndFrom, HWND.Null, ref rc); + + tpm.rcExclude_left = rc.left; + tpm.rcExclude_top = rc.top; + tpm.rcExclude_right = rc.right; + tpm.rcExclude_bottom = rc.bottom; + + LegacyMenuSafeNativeMethods.TrackPopupMenuEx( + new HandleRef(menu, menu.Handle), + ToolBarNativeMethods.TPM_LEFTALIGN | + ToolBarNativeMethods.TPM_LEFTBUTTON | + ToolBarNativeMethods.TPM_VERTICAL, + rc.left, rc.bottom, + new HandleRef(this, Handle), tpm); + } + } + } + + private void WmNotifyNeedText(ref Message m) + { + ToolBarNativeMethods.TOOLTIPTEXT ttt = (ToolBarNativeMethods.TOOLTIPTEXT)m.GetLParam(typeof(ToolBarNativeMethods.TOOLTIPTEXT)); + int commandID = (int)ttt.hdr.idFrom; + ToolBarButton tbb = buttons[commandID]; + + if (tbb is not null && tbb.ToolTipText is not null) + { + ttt.lpszText = tbb.ToolTipText; + } + else + { + ttt.lpszText = null; + } + + ttt.hinst = IntPtr.Zero; + + // RightToLeft reading order + // + if (RightToLeft == RightToLeft.Yes) + { + ttt.uFlags |= (int)TOOLTIP_FLAGS.TTF_RTLREADING; + } + + Marshal.StructureToPtr(ttt, m.LParam, false); + } + + // Track the currently hot item since the user might be using the tab and + // arrow keys to navigate the toolbar and if that's the case, we'll need to know where to re- + // position the tooltip window when the underlying toolbar control attempts to display it. + private void WmNotifyHotItemChange(ref Message m) + { + var lParam = m.GetLParam(typeof(ToolBarNativeMethods.NMTBHOTITEM)) ?? throw new InvalidOperationException("lparam of a received message should not be null"); + + // Should we set the hot item? + ToolBarNativeMethods.NMTBHOTITEM nmTbHotItem = (ToolBarNativeMethods.NMTBHOTITEM)lParam; + if (ToolBarNativeMethods.HICF.HICF_ENTERING == (nmTbHotItem.dwFlags & ToolBarNativeMethods.HICF.HICF_ENTERING)) + { + hotItem = nmTbHotItem.idNew; + } + else if (ToolBarNativeMethods.HICF.HICF_LEAVING == (nmTbHotItem.dwFlags & ToolBarNativeMethods.HICF.HICF_LEAVING)) + { + hotItem = -1; + } + else if (ToolBarNativeMethods.HICF.HICF_MOUSE == (nmTbHotItem.dwFlags & ToolBarNativeMethods.HICF.HICF_MOUSE)) + { + hotItem = nmTbHotItem.idNew; + } + else if (ToolBarNativeMethods.HICF.HICF_ARROWKEYS == (nmTbHotItem.dwFlags & ToolBarNativeMethods.HICF.HICF_ARROWKEYS)) + { + hotItem = nmTbHotItem.idNew; + } + else if (ToolBarNativeMethods.HICF.HICF_ACCELERATOR == (nmTbHotItem.dwFlags & ToolBarNativeMethods.HICF.HICF_ACCELERATOR)) + { + hotItem = nmTbHotItem.idNew; + } + else if (ToolBarNativeMethods.HICF.HICF_DUPACCEL == (nmTbHotItem.dwFlags & ToolBarNativeMethods.HICF.HICF_DUPACCEL)) + { + hotItem = nmTbHotItem.idNew; + } + else if (ToolBarNativeMethods.HICF.HICF_RESELECT == (nmTbHotItem.dwFlags & ToolBarNativeMethods.HICF.HICF_RESELECT)) + { + hotItem = nmTbHotItem.idNew; + } + else if (ToolBarNativeMethods.HICF.HICF_LMOUSE == (nmTbHotItem.dwFlags & ToolBarNativeMethods.HICF.HICF_LMOUSE)) + { + hotItem = nmTbHotItem.idNew; + } + else if (ToolBarNativeMethods.HICF.HICF_TOGGLEDROPDOWN == (nmTbHotItem.dwFlags & ToolBarNativeMethods.HICF.HICF_TOGGLEDROPDOWN)) + { + hotItem = nmTbHotItem.idNew; + } + } + + private void WmReflectCommand(ref Message m) + { + int id = ToolBarNativeMethods.Util.LOWORD(m.WParam); + ToolBarButton tbb = buttons[id]; + + if (tbb != null) + { + ToolBarButtonClickEventArgs e = new ToolBarButtonClickEventArgs(tbb); + OnButtonClick(e); + } + + base.WndProc(ref m); + + ResetMouseEventArgs(); + } + + protected override void WndProc(ref Message m) + { + switch (m.Msg) + { + case (int)MessageId.WM_REFLECT_COMMAND: + WmReflectCommand(ref m); + break; + + case (int)PInvokeCore.WM_NOTIFY: + case (int)MessageId.WM_REFLECT_NOTIFY: + + var lParam = m.GetLParam(typeof(NMHDR)) ?? throw new InvalidOperationException("lparam of a received message should not be null"); + + NMHDR note = (NMHDR)lParam; + switch (note.code) + { + case PInvoke.TTN_NEEDTEXT: + WmNotifyNeedText(ref m); + m.Result = 1; + return; + case PInvoke.TTN_SHOW: + // Prevent the tooltip from displaying in the upper left corner of the + // desktop when the control is nowhere near that location. + WINDOWPLACEMENT wndPlacement = new(); + int nRet = PInvoke.GetWindowPlacement(note.hwndFrom, ref wndPlacement); + + // Is this tooltip going to be positioned in the upper left corner of the display, + // but nowhere near the toolbar button? + if (wndPlacement.rcNormalPosition.left == 0 && + wndPlacement.rcNormalPosition.top == 0 && + hotItem != -1) + { + + // Assume that we're going to vertically center the tooltip on the right edge of the current + // hot item. + + // Where is the right edge of the current hot item? + int buttonRight = 0; + for (int idx = 0; idx <= hotItem; idx++) + { + // How wide is the item at this index? (It could be a separator, and therefore a different width.) + buttonRight += buttonsCollection[idx].GetButtonWidth(); + } + + // Where can we place this tooltip so that it will be completely visible on the current display? + int tooltipWidth = wndPlacement.rcNormalPosition.right - wndPlacement.rcNormalPosition.left; + int tooltipHeight = wndPlacement.rcNormalPosition.bottom - wndPlacement.rcNormalPosition.top; + + // We'll need screen coordinates of this position for setting the tooltip's position + int x = Location.X + buttonRight + 1; + int y = Location.Y + (ButtonSize.Height / 2); + var leftTop = new Point(x, y); + PInvoke.ClientToScreen(HWND, ref leftTop); + + // Will the tooltip bleed off the top? + if (leftTop.Y < SystemInformation.WorkingArea.Y) + { + // Reposition the tooltip to be displayed below the button + leftTop.Y += (ButtonSize.Height / 2) + 1; + } + + // Will the tooltip bleed off the bottom? + if (leftTop.Y + tooltipHeight > SystemInformation.WorkingArea.Height) + { + // Reposition the tooltip to be displayed above the button + leftTop.Y -= ((ButtonSize.Height / 2) + tooltipHeight + 1); + } + + // Will the tooltip bleed off the right edge? + if (leftTop.X + tooltipWidth > SystemInformation.WorkingArea.Right) + { + // Move the tooltip far enough left that it will display in the working area + leftTop.X -= (ButtonSize.Width + tooltipWidth + 2); + } + + PInvoke.SetWindowPos(note.hwndFrom, HWND.Null, leftTop.X, leftTop.Y, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + m.Result = (IntPtr)1; + return; + } + + break; + + case ToolBarNativeMethods.TBN_HOTITEMCHANGE: + WmNotifyHotItemChange(ref m); + break; + + case ToolBarNativeMethods.TBN_QUERYINSERT: + m.Result = (IntPtr)1; + break; + + case ToolBarNativeMethods.TBN_DROPDOWN: + WmNotifyDropDown(ref m); + break; + } + + break; + + } + + base.WndProc(ref m); + } + + /// + /// Encapsulates a collection of controls for use by the + /// class. + /// + public class ToolBarButtonCollection : IList + { + private readonly ToolBar owner; + private bool suspendUpdate; + /// A caching mechanism for key accessor + /// We use an index here rather than control so that we don't have lifetime + /// issues by holding on to extra references. + private int lastAccessedIndex = -1; + + /// + /// Initializes a new instance of the class and assigns it to the specified toolbar. + /// + public ToolBarButtonCollection(ToolBar owner) + { + this.owner = owner; + } + + /// + /// Gets or sets the toolbar button at the specified indexed location in the + /// toolbar button collection. + /// + public virtual ToolBarButton this[int index] + { + get + { + if (index < 0 || ((owner.buttons != null) && (index >= owner.buttonCount))) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + return owner.buttons[index]; + } + set + { + + // Sanity check parameters + // + if (index < 0 || ((owner.buttons != null) && index >= owner.buttonCount)) + { + throw new ArgumentOutOfRangeException(nameof(index), index, string.Format(SR.InvalidArgument, nameof(index), index)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + owner.InternalSetButton(index, value, true, true); + } + } + + object IList.this[int index] + { + get + { + return this[index]; + } + set + { + if (value is ToolBarButton) + { + this[index] = (ToolBarButton)value; + } + else + { + throw new ArgumentException(LegacyToolBarSR.ToolBarBadToolBarButton, nameof(value)); + } + } + } + + /// + /// Retrieves the child control with the specified key. + /// + public virtual ToolBarButton this[string key] + { + get + { + // We do not support null and empty string as valid keys. + if (string.IsNullOrEmpty(key)) + { + return null; + } + + // Search for the key in our collection + int index = IndexOfKey(key); + if (IsValidIndex(index)) + { + return this[index]; + } + else + { + return null; + } + + } + } + + /// + /// Gets the number of buttons in the toolbar button collection. + /// + [Browsable(false)] + public int Count + { + get + { + return owner.buttonCount; + } + } + + object ICollection.SyncRoot + { + get + { + return this; + } + } + + bool ICollection.IsSynchronized + { + get + { + return false; + } + } + + bool IList.IsFixedSize + { + get + { + return false; + } + } + + public bool IsReadOnly + { + get + { + return false; + } + } + + /// + /// Adds a new toolbar button to + /// the end of the toolbar button collection. + /// + public int Add(ToolBarButton button) + { + + int index = owner.InternalAddButton(button); + + if (!suspendUpdate) + { + owner.UpdateButtons(); + } + + return index; + } + + public int Add(string text) + { + ToolBarButton button = new ToolBarButton(text); + return Add(button); + } + + int IList.Add(object? button) + { + if (button is ToolBarButton) + { + return Add((ToolBarButton)button); + } + else + { + throw new ArgumentException(LegacyToolBarSR.ToolBarBadToolBarButton, nameof(button)); + } + } + + public void AddRange(ToolBarButton[] buttons) + { + if (buttons == null) + { + throw new ArgumentNullException(nameof(buttons)); + } + + try + { + suspendUpdate = true; + foreach (ToolBarButton button in buttons) + { + Add(button); + } + } + finally + { + suspendUpdate = false; + owner.UpdateButtons(); + } + } + + /// + /// Removes + /// all buttons from the toolbar button collection. + /// + public void Clear() + { + + if (owner.buttons == null) + { + return; + } + + for (int x = owner.buttonCount; x > 0; x--) + { + if (owner.IsHandleCreated) + { + PInvokeCore.SendMessage(owner, PInvoke.TB_DELETEBUTTON, (WPARAM)(x - 1)); + } + + owner.RemoveAt(x - 1); + } + + owner.buttons = null; + owner.buttonCount = 0; + if (!owner.Disposing) + { + owner.UpdateButtons(); + } + } + + public bool Contains(ToolBarButton button) + { + return IndexOf(button) != -1; + } + + bool IList.Contains(object? button) + { + if (button is ToolBarButton) + { + return Contains((ToolBarButton)button); + } + else + { + return false; + } + } + + /// + /// Returns true if the collection contains an item with the specified key, false otherwise. + /// + public virtual bool ContainsKey(string key) + { + return IsValidIndex(IndexOfKey(key)); + } + + void ICollection.CopyTo(Array dest, int index) + { + if (owner.buttonCount > 0) + { + System.Array.Copy(owner.buttons, 0, dest, index, owner.buttonCount); + } + } + + public int IndexOf(ToolBarButton button) + { + for (int index = 0; index < Count; ++index) + { + if (this[index] == button) + { + return index; + } + } + + return -1; + } + + int IList.IndexOf(object? button) + { + if (button is ToolBarButton) + { + return IndexOf((ToolBarButton)button); + } + else + { + return -1; + } + } + + /// + /// The zero-based index of the first occurrence of value within the entire CollectionBase, if found; otherwise, -1. + /// + public virtual int IndexOfKey(string key) + { + // Step 0 - Arg validation + if (string.IsNullOrEmpty(key)) + { + return -1; // we dont support empty or null keys. + } + + // step 1 - check the last cached item + if (IsValidIndex(lastAccessedIndex)) + { + if (WindowsFormsUtils.SafeCompareStrings(this[lastAccessedIndex].Name, key, /* ignoreCase = */ true)) + { + return lastAccessedIndex; + } + } + + // step 2 - search for the item + for (int i = 0; i < Count; i++) + { + if (WindowsFormsUtils.SafeCompareStrings(this[i].Name, key, /* ignoreCase = */ true)) + { + lastAccessedIndex = i; + return i; + } + } + + // step 3 - we didn't find it. Invalidate the last accessed index and return -1. + lastAccessedIndex = -1; + return -1; + } + + public void Insert(int index, ToolBarButton button) + { + owner.InsertButton(index, button); + } + + void IList.Insert(int index, object? button) + { + if (button is ToolBarButton) + { + Insert(index, (ToolBarButton)button); + } + else + { + throw new ArgumentException(LegacyToolBarSR.ToolBarBadToolBarButton, nameof(button)); + } + } + + /// + /// Determines if the index is valid for the collection. + /// + private bool IsValidIndex(int index) + { + return ((index >= 0) && (index < Count)); + } + + /// + /// Removes + /// a given button from the toolbar button collection. + /// + public void RemoveAt(int index) + { + int count = (owner.buttons == null) ? 0 : owner.buttonCount; + + if (index < 0 || index >= count) + { + throw new ArgumentOutOfRangeException(nameof(index), string.Format(SR.InvalidArgument, "index", index.ToString(CultureInfo.CurrentCulture))); + } + + if (owner.IsHandleCreated) + { + PInvokeCore.SendMessage(owner, PInvoke.TB_DELETEBUTTON, (WPARAM)index); + } + + owner.RemoveAt(index); + owner.UpdateButtons(); + + } + + /// + /// Removes the child control with the specified key. + /// + public virtual void RemoveByKey(string key) + { + int index = IndexOfKey(key); + if (IsValidIndex(index)) + { + RemoveAt(index); + } + } + + public void Remove(ToolBarButton button) + { + int index = IndexOf(button); + if (index != -1) + { + RemoveAt(index); + } + } + + void IList.Remove(object? button) + { + if (button is ToolBarButton) + { + Remove((ToolBarButton)button); + } + } + + /// + /// Returns an enumerator that can be used to iterate + /// through the toolbar button collection. + /// + public IEnumerator GetEnumerator() + { + return new ArraySubsetEnumerator(owner.buttons, owner.buttonCount); + } + } + + } } + + diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarAppearance.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarAppearance.cs index dc6994f3243..5d1c2977748 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarAppearance.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarAppearance.cs @@ -1,22 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. -using System.ComponentModel; +#pragma warning disable IDE0073 -namespace System.Windows.Forms; - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.ToolBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -public enum ToolBarAppearance +namespace System.Windows.Forms { - Normal = 0, - Flat = 1, + /// + /// Specifies the type of toolbar to display. + /// + public enum ToolBarAppearance + { + /// + /// The toolbar and buttons appear as standard three dimensional controls. + /// + Normal = 0, + + /// + /// The toolbar and buttons appear flat, but the buttons change to three + /// dimensional as the mouse pointer moves over them. + /// + Flat = 1, + } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButton.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButton.cs index d66883d57aa..d809b23905b 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButton.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButton.cs @@ -1,137 +1,876 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma warning disable CS8632, CSIsNull001, CSIsNull002, IDE1006, IDE0040, CA1805, CA1822, RS0016, RS0036, SA1005, SA1129, SA1400, SA1505, SA1508, IDE0031, IDE0063, IDE0073, IDE0078 using System.ComponentModel; using System.Drawing; using System.Drawing.Design; - -namespace System.Windows.Forms; +using System.Text; +using ToolBarNativeMethods = System.Windows.Forms.LegacyToolBarNativeMethods; +using Marshal = System.Runtime.InteropServices.Marshal; #nullable disable -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.ToolBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -[Designer($"System.Windows.Forms.Design.ToolBarButtonDesigner, {Assemblies.SystemDesign}")] -[DefaultProperty(nameof(Text))] -[ToolboxItem(false)] -[DesignTimeVisible(false)] -public class ToolBarButton : Component +namespace System.Windows.Forms { - public ToolBarButton() => throw new PlatformNotSupportedException(); + /// + /// Represents a Windows toolbar button. + /// + [ + Designer($"System.Windows.Forms.Design.ToolBarButtonDesigner, {Assemblies.SystemDesign}"), + DefaultProperty(nameof(Text)), + ToolboxItem(false), + DesignTimeVisible(false), + ] + public class ToolBarButton : Component + { + string text; + string name = null; + string tooltipText; + bool enabled = true; + bool visible = true; + bool pushed = false; + bool partialPush = false; + private int commandId = -1; // the cached command id of the button. + private ToolBarButtonImageIndexer imageIndexer; - public ToolBarButton(string text) => throw new PlatformNotSupportedException(); + ToolBarButtonStyle style = ToolBarButtonStyle.PushButton; - [DefaultValue(null)] - [TypeConverter(typeof(ReferenceConverter))] - public Menu DropDownMenu - { - get => throw null; - set { } - } + object userData; - [DefaultValue(true)] - [Localizable(true)] - public bool Enabled - { - get => throw null; - set { } - } + // These variables below are used by the ToolBar control to help + // it manage some information about us. - [TypeConverter(typeof(ImageIndexConverter))] - [Editor($"System.Windows.Forms.Design.ImageIndexEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))] - [DefaultValue(-1)] - [RefreshProperties(RefreshProperties.Repaint)] - [Localizable(true)] - public int ImageIndex - { - get => throw null; - set { } - } + /// + /// If this button has a string, what it's index is in the ToolBar's + /// internal list of strings. Needs to be package protected. + /// + internal IntPtr stringIndex = (IntPtr)(-1); - [TypeConverter(typeof(ImageKeyConverter))] - [Editor($"System.Windows.Forms.Design.ImageIndexEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))] - [DefaultValue("")] - [Localizable(true)] - [RefreshProperties(RefreshProperties.Repaint)] - public string ImageKey - { - get => throw null; - set { } - } + /// + /// Our parent ToolBar control. + /// + internal ToolBar parent; - [Browsable(false)] - public string Name - { - get => throw null; - set { } - } + /// + /// For DropDown buttons, we can optionally show a + /// context menu when the button is dropped down. + /// + internal Menu dropDownMenu = null; - [Browsable(false)] - public ToolBar Parent => throw null; + /// + /// Initializes a new instance of the class. + /// + public ToolBarButton() + { + } - [DefaultValue(false)] - public bool PartialPush - { - get => throw null; - set { } - } + public ToolBarButton(string text) : base() + { + Text = text; + } - [DefaultValue(false)] - public bool Pushed - { - get => throw null; - set { } - } + // We need a special way to defer to the ToolBar's image + // list for indexing purposes. + internal class ToolBarButtonImageIndexer : ImageList.Indexer + { + private readonly ToolBarButton owner; - public Rectangle Rectangle => throw null; + public ToolBarButtonImageIndexer(ToolBarButton button) + { + owner = button; + } - [DefaultValue(ToolBarButtonStyle.PushButton)] - [RefreshProperties(RefreshProperties.Repaint)] - public ToolBarButtonStyle Style - { - get => throw null; - set { } - } + public override ImageList ImageList + { + get + { + if ((owner != null) && (owner.parent != null)) + { + return owner.parent.ImageList; + } - [Localizable(false)] - [Bindable(true)] - [DefaultValue(null)] - [TypeConverter(typeof(StringConverter))] - public object Tag - { - get => throw null; - set { } - } + return null; + } + set { Debug.Assert(false, "We should never set the image list"); } + } + } - [Localizable(true)] - [DefaultValue("")] - public string Text - { - get => throw null; - set { } - } + internal ToolBarButtonImageIndexer ImageIndexer + { + get + { + return imageIndexer ??= new ToolBarButtonImageIndexer(this); + } + } - [Localizable(true)] - [DefaultValue("")] - public string ToolTipText - { - get => throw null; - set { } - } + /// + /// + /// Indicates the menu to be displayed in + /// the drop-down toolbar button. + /// + [ + DefaultValue(null), + TypeConverter(typeof(ReferenceConverter)), + ] + public Menu DropDownMenu + { + get + { + return dropDownMenu; + } - [DefaultValue(true)] - [Localizable(true)] - public bool Visible - { - get => throw null; - set { } + set + { + //The dropdownmenu must be of type ContextMenu, Main & Items are invalid. + // + if (value != null && !(value is ContextMenu)) + { + throw new ArgumentException(LegacyToolBarSR.ToolBarButtonInvalidDropDownMenuType, nameof(value)); + } + + dropDownMenu = value; + } + } + + /// + /// Indicates whether the button is enabled or not. + /// + [ + DefaultValue(true), + Localizable(true), + ] + public bool Enabled + { + get + { + return enabled; + } + + set + { + if (enabled != value) + { + + enabled = value; + + if (parent != null && parent.IsHandleCreated) + { + PInvokeCore.SendMessage(parent, PInvoke.TB_ENABLEBUTTON, (WPARAM)FindButtonIndex(), + enabled ? 1 : 0); + } + } + } + } + + /// + /// Indicates the index + /// value of the image assigned to the button. + /// + [ + TypeConverter(typeof(ImageIndexConverter)), + Editor($"System.Windows.Forms.Design.ImageIndexEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor)), + DefaultValue(-1), + RefreshProperties(RefreshProperties.Repaint), + Localizable(true) + ] + public int ImageIndex + { + get + { + return ImageIndexer.Index; + } + set + { + if (ImageIndexer.Index != value) + { + if (value < -1) + { + throw new ArgumentOutOfRangeException(nameof(ImageIndex), value, string.Format(SR.InvalidArgument, nameof(ImageIndex), value)); + } + + ImageIndexer.Index = value; + UpdateButton(false); + } + } + } + + /// + /// Indicates the index + /// value of the image assigned to the button. + /// + [ + TypeConverter(typeof(ImageKeyConverter)), + Editor($"System.Windows.Forms.Design.ImageIndexEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor)), + DefaultValue(""), + Localizable(true), + RefreshProperties(RefreshProperties.Repaint) + ] + public string ImageKey + { + get + { + return ImageIndexer.Key; + } + set + { + if (ImageIndexer.Key != value) + { + ImageIndexer.Key = value; + UpdateButton(false); + } + } + } + + /// + /// Name of this control. The designer will set this to the same + /// as the programatic Id "(name)" of the control - however this + /// property has no bearing on the runtime aspects of this control. + /// + [Browsable(false)] + public string Name + { + get + { + return WindowsFormsUtils.GetComponentName(this, name); + } + set + { + if (value == null || value.Length == 0) + { + name = null; + } + else + { + name = value; + } + + if (Site != null) + { + Site.Name = name; + } + } + } + + /// + /// Indicates the toolbar control that the toolbar button is assigned to. This property is + /// read-only. + /// + [ + Browsable(false), + ] + public ToolBar Parent + { + get + { + return parent; + } + } + + /// + /// + /// Indicates whether a toggle-style toolbar button + /// is partially pushed. + /// + [ + DefaultValue(false), + ] + public bool PartialPush + { + get + { + if (parent == null || !parent.IsHandleCreated) + { + return partialPush; + } + else + { + if ((int)PInvokeCore.SendMessage(parent, PInvoke.TB_ISBUTTONINDETERMINATE, (WPARAM)FindButtonIndex()) != 0) + { + partialPush = true; + } + else + { + partialPush = false; + } + + return partialPush; + } + } + set + { + if (partialPush != value) + { + partialPush = value; + UpdateButton(false); + } + } + } + + /// + /// Indicates whether a toggle-style toolbar button is currently in the pushed state. + /// + [ + DefaultValue(false), + ] + public bool Pushed + { + get + { + if (parent == null || !parent.IsHandleCreated) + { + return pushed; + } + else + { + return GetPushedState(); + } + } + set + { + if (value != Pushed) + { // Getting property Pushed updates pushed member variable + pushed = value; + UpdateButton(false, false, false); + } + } + } + + /// + /// Indicates the bounding rectangle for a toolbar button. This property is + /// read-only. + /// + public Rectangle Rectangle + { + get + { + if (parent != null) + { + RECT rc = new RECT(); + PInvokeCore.SendMessage(parent, PInvoke.TB_GETRECT, (WPARAM)FindButtonIndex(), ref rc); + return Rectangle.FromLTRB(rc.left, rc.top, rc.right, rc.bottom); + } + + return Rectangle.Empty; + } + } + + /// + /// Indicates the style of the + /// toolbar button. + /// + [ + DefaultValue(ToolBarButtonStyle.PushButton), + RefreshProperties(RefreshProperties.Repaint) + ] + public ToolBarButtonStyle Style + { + get + { + return style; + } + set + { + //valid values are 0x1 to 0x4 + SourceGenerated.EnumValidator.Validate(value); + + if (style == value) + { + return; + } + + style = value; + UpdateButton(true); + } + } + + [ + SRCategory(nameof(SR.CatData)), + Localizable(false), + Bindable(true), + SRDescription(nameof(SR.ControlTagDescr)), + DefaultValue(null), + TypeConverter(typeof(StringConverter)), + ] + public object Tag + { + get + { + return userData; + } + set + { + userData = value; + } + } + + /// + /// Indicates the text that is displayed on the toolbar button. + /// + [ + Localizable(true), + DefaultValue("") + ] + public string Text + { + get + { + return text ?? ""; + } + set + { + if (string.IsNullOrEmpty(value)) + { + value = null; + } + + if ((value == null && text != null) || + (value != null && (text == null || !text.Equals(value)))) + { + text = value; + // Adding a mnemonic requires a handle recreate. + UpdateButton(WindowsFormsUtils.ContainsMnemonic(text), true, true); + } + } + } + + /// + /// + /// Indicates + /// the text that appears as a tool tip for a control. + /// + [ + Localizable(true), + DefaultValue("") + ] + public string ToolTipText + { + get + { + return tooltipText ?? ""; + } + set + { + tooltipText = value; + } + } + + /// + /// + /// Indicates whether the toolbar button + /// is visible. + /// + [ + DefaultValue(true), + Localizable(true) + ] + public bool Visible + { + get + { + return visible; + } + set + { + if (visible != value) + { + visible = value; + UpdateButton(false); + } + } + } + + /// + /// This is somewhat nasty -- by default, the windows ToolBar isn't very + /// clever about setting the width of buttons, and has a very primitive + /// algorithm that doesn't include for things like drop down arrows, etc. + /// We need to do a bunch of work here to get all the widths correct. Ugh. + /// + internal short Width + { + get + { + Debug.Assert(parent != null, "Parent should be non-null when button width is requested"); + + int width = 0; + ToolBarButtonStyle style = Style; + + Size edge = SystemInformation.Border3DSize; + if (style != ToolBarButtonStyle.Separator) + { + + // COMPAT: this will force handle creation. + // we could use the measurement graphics, but it looks like this has been like this since Everett. + using (Graphics g = parent.CreateGraphicsInternal()) + { + + Size buttonSize = parent.buttonSize; + if (!(buttonSize.IsEmpty)) + { + width = buttonSize.Width; + } + else + { + if (parent.ImageList != null || !string.IsNullOrEmpty(Text)) + { + Size imageSize = parent.ImageSize; + Size textSize = Size.Ceiling(g.MeasureString(Text, parent.Font)); + if (parent.TextAlign == ToolBarTextAlign.Right) + { + if (textSize.Width == 0) + { + width = imageSize.Width + edge.Width * 4; + } + else + { + width = imageSize.Width + textSize.Width + edge.Width * 6; + } + } + else + { + if (imageSize.Width > textSize.Width) + { + width = imageSize.Width + edge.Width * 4; + } + else + { + width = textSize.Width + edge.Width * 4; + } + } + + if (style == ToolBarButtonStyle.DropDownButton && parent.DropDownArrows) + { + width += ToolBar.DDARROW_WIDTH; + } + } + else + { + width = parent.ButtonSize.Width; + } + } + } + } + else + { + width = edge.Width * 2; + } + + return (short)width; + } + + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (parent != null) + { + int index = FindButtonIndex(); + if (index != -1) + { + parent.Buttons.RemoveAt(index); + } + } + } + + base.Dispose(disposing); + } + + /// + /// Finds out index in the parent. + /// + private int FindButtonIndex() + { + for (int x = 0; x < parent.Buttons.Count; x++) + { + if (parent.Buttons[x] == this) + { + return x; + } + } + + return -1; + } + + // This is necessary to get the width of the buttons in the toolbar, + // including the width of separators, so that we can accurately position the tooltip adjacent + // to the currently hot button when the user uses keyboard navigation to access the toolbar. + internal int GetButtonWidth() + { + // Assume that this button is the same width as the parent's ButtonSize's Width + int buttonWidth = Parent.ButtonSize.Width; + + ToolBarNativeMethods.TBBUTTONINFO button = new() + { + cbSize = Marshal.SizeOf(), + dwMask = (int)ToolBarNativeMethods.TBIF_SIZE + }; + + int buttonID = (int)PInvokeCore.SendMessage(Parent, PInvoke.TB_GETBUTTONINFO, (WPARAM)commandId, ref button); + if (buttonID != -1) + { + buttonWidth = button.cx; + } + + return buttonWidth; + } + + private bool GetPushedState() + { + if ((int)PInvokeCore.SendMessage(parent, PInvoke.TB_ISBUTTONCHECKED, (WPARAM)FindButtonIndex()) != 0) + { + pushed = true; + } + else + { + pushed = false; + } + + return pushed; + } + + /// + /// Returns a TBBUTTON object that represents this ToolBarButton. + /// + internal ToolBarNativeMethods.TBBUTTON GetTBBUTTON(int commandId) + { + ToolBarNativeMethods.TBBUTTON button = new() + { + iBitmap = ImageIndexer.ActualIndex, + + // set up the state of the button + // + fsState = 0 + }; + if (enabled) + { + button.fsState |= (byte)ToolBarNativeMethods.TBSTATE_ENABLED; + } + + if (partialPush && style == ToolBarButtonStyle.ToggleButton) + { + button.fsState |= (byte)ToolBarNativeMethods.TBSTATE_INDETERMINATE; + } + + if (pushed) + { + button.fsState |= (byte)ToolBarNativeMethods.TBSTATE_CHECKED; + } + + if (!visible) + { + button.fsState |= (byte)ToolBarNativeMethods.TBSTATE_HIDDEN; + } + + // set the button style + // + switch (style) + { + case ToolBarButtonStyle.PushButton: + button.fsStyle = (byte)ToolBarNativeMethods.TBSTYLE_BUTTON; + break; + case ToolBarButtonStyle.ToggleButton: + button.fsStyle = (byte)ToolBarNativeMethods.TBSTYLE_CHECK; + break; + case ToolBarButtonStyle.Separator: + button.fsStyle = (byte)ToolBarNativeMethods.TBSTYLE_SEP; + break; + case ToolBarButtonStyle.DropDownButton: + button.fsStyle = (byte)ToolBarNativeMethods.TBSTYLE_DROPDOWN; + break; + + } + + button.dwData = (IntPtr)0; + button.iString = stringIndex; + this.commandId = commandId; + button.idCommand = commandId; + + return button; + } + + /// + /// Returns a TBBUTTONINFO object that represents this ToolBarButton. + /// + internal ToolBarNativeMethods.TBBUTTONINFO GetTBBUTTONINFO(bool updateText, int newCommandId) + { + ToolBarNativeMethods.TBBUTTONINFO button = new() + { + cbSize = Marshal.SizeOf(), + dwMask = (int)(ToolBarNativeMethods.TBIF_IMAGE + | ToolBarNativeMethods.TBIF_STATE | ToolBarNativeMethods.TBIF_STYLE) + }; + + // Older platforms interpret null strings as empty, which forces the button to + // leave space for text. + // The only workaround is to avoid having comctl update the text. + if (updateText) + { + button.dwMask |= (int)ToolBarNativeMethods.TBIF_TEXT; + } + + button.iImage = ImageIndexer.ActualIndex; + + if (newCommandId != commandId) + { + commandId = newCommandId; + button.idCommand = newCommandId; + button.dwMask |= (int)ToolBarNativeMethods.TBIF_COMMAND; + } + + // set up the state of the button + // + button.fsState = 0; + if (enabled) + { + button.fsState |= (byte)ToolBarNativeMethods.TBSTATE_ENABLED; + } + + if (partialPush && style == ToolBarButtonStyle.ToggleButton) + { + button.fsState |= (byte)ToolBarNativeMethods.TBSTATE_INDETERMINATE; + } + + if (pushed) + { + button.fsState |= (byte)ToolBarNativeMethods.TBSTATE_CHECKED; + } + + if (!visible) + { + button.fsState |= (byte)ToolBarNativeMethods.TBSTATE_HIDDEN; + } + + // set the button style + // + switch (style) + { + case ToolBarButtonStyle.PushButton: + button.fsStyle = (byte)ToolBarNativeMethods.TBSTYLE_BUTTON; + break; + case ToolBarButtonStyle.ToggleButton: + button.fsStyle = (byte)ToolBarNativeMethods.TBSTYLE_CHECK; + break; + case ToolBarButtonStyle.Separator: + button.fsStyle = (byte)ToolBarNativeMethods.TBSTYLE_SEP; + break; + case ToolBarButtonStyle.DropDownButton: + button.fsStyle = (byte)ToolBarNativeMethods.TBSTYLE_DROPDOWN; + break; + } + + if (text == null) + { + button.pszText = Marshal.StringToHGlobalAuto("\0\0"); + } + else + { + string textValue = text; + PrefixAmpersands(ref textValue); + button.pszText = Marshal.StringToHGlobalAuto(textValue); + } + + return button; + } + + private void PrefixAmpersands(ref string value) + { + // Due to a comctl32 problem, ampersands underline the next letter in the + // text string, but the accelerators don't work. + // So in this function, we prefix ampersands with another ampersand + // so that they actually appear as ampersands. + // + + // Sanity check parameter + // + if (value == null || value.Length == 0) + { + return; + } + + // If there are no ampersands, we don't need to do anything here + // + if (value.IndexOf('&') < 0) + { + return; + } + + // Insert extra ampersands + // + StringBuilder newString = new StringBuilder(); + for (int i = 0; i < value.Length; ++i) + { + if (value[i] == '&') + { + if (i < value.Length - 1 && value[i + 1] == '&') + { + ++i; // Skip the second ampersand + } + + newString.Append("&&"); + } + else + { + newString.Append(value[i]); + } + } + + value = newString.ToString(); + } + + public override string ToString() + { + return "ToolBarButton: " + Text + ", Style: " + Style.ToString("G"); + } + + /// + /// When a button property changes and the parent control is created, + /// we need to make sure it gets the new button information. + /// If Text was changed, call the next overload. + /// + internal void UpdateButton(bool recreate) + { + UpdateButton(recreate, false, true); + } + + /// + /// When a button property changes and the parent control is created, + /// we need to make sure it gets the new button information. + /// + private void UpdateButton(bool recreate, bool updateText, bool updatePushedState) + { + // It looks like ToolBarButtons with a DropDownButton tend to + // lose the DropDownButton very easily - so we need to recreate + // the button each time it changes just to be sure. + // + if (style == ToolBarButtonStyle.DropDownButton && parent != null && parent.DropDownArrows) + { + recreate = true; + } + + // we just need to get the Pushed state : this asks the Button its states and sets + // the private member "pushed" to right value.. + + // this member is used in "InternalSetButton" which calls GetTBBUTTONINFO(bool updateText) + // the GetButtonInfo method uses the "pushed" variable.. + + //rather than setting it ourselves .... we asks the button to set it for us.. + if (updatePushedState && parent != null && parent.IsHandleCreated) + { + GetPushedState(); + } + + if (parent != null) + { + int index = FindButtonIndex(); + if (index != -1) + { + parent.InternalSetButton(index, this, recreate, updateText); + } + } + } } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonClickEventArgs.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonClickEventArgs.cs index 431ae6aab82..3c85205e093 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonClickEventArgs.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonClickEventArgs.cs @@ -1,29 +1,30 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. -using System.ComponentModel; +#pragma warning disable IDE0073, RS0036 -namespace System.Windows.Forms; - -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.ToolBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -public class ToolBarButtonClickEventArgs : EventArgs +namespace System.Windows.Forms { - public ToolBarButtonClickEventArgs(ToolBarButton button) => throw new PlatformNotSupportedException(); - - public ToolBarButton Button + /// + /// Provides data for the + /// event. + /// + public class ToolBarButtonClickEventArgs : EventArgs { - get => throw null; - set { } + /// + /// Initializes a new instance of the + /// class. + /// + public ToolBarButtonClickEventArgs(ToolBarButton button) + { + Button = button; + } + + /// + /// Specifies the + /// that was clicked. + /// + public ToolBarButton Button { get; set; } } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonClickEventHandler.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonClickEventHandler.cs index f701edcc316..17783cbd50f 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonClickEventHandler.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonClickEventHandler.cs @@ -1,20 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; - -namespace System.Windows.Forms; - -#nullable disable - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.ToolBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -public delegate void ToolBarButtonClickEventHandler(object sender, ToolBarButtonClickEventArgs e); +// See the LICENSE file in the project root for more information. + +#pragma warning disable IDE0073, RS0036 + +namespace System.Windows.Forms +{ + /// + /// Represents a method that will handle the + /// event of a . + /// + public delegate void ToolBarButtonClickEventHandler(object sender, ToolBarButtonClickEventArgs e); +} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonStyle.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonStyle.cs index 6bf122955e5..4babfd26fb1 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonStyle.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarButtonStyle.cs @@ -1,24 +1,35 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. -using System.ComponentModel; +#pragma warning disable IDE0073 -namespace System.Windows.Forms; - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.ToolBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -public enum ToolBarButtonStyle +namespace System.Windows.Forms { - PushButton = 1, - ToggleButton = 2, - Separator = 3, - DropDownButton = 4, + public enum ToolBarButtonStyle + { + /// + /// A standard, three-dimensional button. + /// + PushButton = 1, + + /// + /// A toggle button that appears sunken when clicked and retains the + /// sunken appearance until clicked again. + /// + ToggleButton = 2, + + /// + /// A space or line between toolbar buttons. The appearance depends on + /// the value of the + /// property. + /// + Separator = 3, + + /// + /// A drop down control that displays a menu or other window when + /// clicked. + /// + DropDownButton = 4, + } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarTextAlign.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarTextAlign.cs index 2ea70b12f2d..a8976872fbc 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarTextAlign.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Unsupported/ToolBar/ToolBarTextAlign.cs @@ -1,22 +1,24 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. -using System.ComponentModel; +#pragma warning disable IDE0073 -namespace System.Windows.Forms; - -/// -/// This type is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. -/// -[Obsolete( - Obsoletions.ToolBarMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -[Browsable(false)] -public enum ToolBarTextAlign +namespace System.Windows.Forms { - Underneath = 0, - Right = 1, + /// + /// Specifies the alignment of text on the toolbar button control. + /// + public enum ToolBarTextAlign + { + /// + /// The text is aligned underneath the toolbar button image. + /// + Underneath = 0, + + /// + /// The text is aligned to the right of the toolbar button image. + /// + Right = 1, + } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Form.cs b/src/System.Windows.Forms/System/Windows/Forms/Form.cs index c3900d09c6b..43074b0f95f 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Form.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Form.cs @@ -124,6 +124,9 @@ public partial class Form : ContainerControl private static readonly int s_propFormerlyActiveMdiChild = PropertyStore.CreateKey(); private static readonly int s_propMdiChildFocusable = PropertyStore.CreateKey(); + private static readonly int s_propMainMenu = PropertyStore.CreateKey(); + private static readonly int s_propMergedMenu = PropertyStore.CreateKey(); + private static readonly int s_propCurMenu = PropertyStore.CreateKey(); private static readonly int s_propDummyMdiMenu = PropertyStore.CreateKey(); private static readonly int s_propMainMenuStrip = PropertyStore.CreateKey(); private static readonly int s_propMdiWindowListStrip = PropertyStore.CreateKey(); @@ -1248,6 +1251,8 @@ public event EventHandler? MaximumSizeChanged remove => Events.RemoveHandler(s_maximumSizeChangedEvent, value); } + internal override bool HasMenu => TopLevel && Menu is not null && Menu.ItemCount > 0; + [SRCategory(nameof(SR.CatWindowStyle))] [DefaultValue(null)] [SRDescription(nameof(SR.FormMenuStripDescr))] @@ -3275,7 +3280,9 @@ private Size ComputeWindowSize(Size clientSize) private Size ComputeWindowSize(Size clientSize, WINDOW_STYLE style, WINDOW_EX_STYLE exStyle) { RECT result = new(clientSize); - AdjustWindowRectExForControlDpi(ref result, style, false, exStyle); + + AdjustWindowRectExForControlDpi(ref result, style, HasMenu, exStyle); + return result.Size; } @@ -3534,6 +3541,18 @@ protected override void Dispose(bool disposing) MainMenuStrip = null; } + if (Properties.TryGetValue(s_propMergedMenu, out MainMenu? mergedMenu)) + { + if (mergedMenu.ownerForm == this || mergedMenu.form is null) + { + mergedMenu.Dispose(); + } + + Properties.RemoveValue(s_propMergedMenu); + } + + Properties.RemoveValue(s_propCurMenu); + if (Properties.TryGetValue(s_propOwner, out Form? owner)) { owner.RemoveOwnedForm(this); @@ -3971,7 +3990,20 @@ protected void CenterToScreen() /// Invalidates the merged menu, forcing the menu to be recreated if /// needed again. /// - private void InvalidateMergedMenu() => ParentForm?.UpdateMenuHandles(); + private void InvalidateMergedMenu() + { + if (Properties.TryGetValue(s_propMergedMenu, out MainMenu? mergedMenu)) + { + if (mergedMenu.ownerForm == this) + { + mergedMenu.Dispose(); + } + + Properties.RemoveValue(s_propMergedMenu); + } + + ParentForm?.UpdateMenuHandles(); + } /// /// Arranges the Multiple Document Interface @@ -3982,6 +4014,76 @@ public void LayoutMdi(MdiLayout value) _ctlClient?.LayoutMdi(value); } + internal void MenuChanged(int change, Menu? menu) + { +#pragma warning disable WFDEV006 // Type or member is obsolete + Form? parentForm = ParentForm; + if (parentForm is not null && this == parentForm.ActiveMdiChildInternal) + { + parentForm.MenuChanged(change, menu); + + return; + } + + switch (change) + { + case Forms.Menu.CHANGE_ITEMS: + case Forms.Menu.CHANGE_MERGE: + if (_ctlClient is null || !_ctlClient.IsHandleCreated) + { + if (ReferenceEquals(menu, Menu) && change == Forms.Menu.CHANGE_ITEMS) + { + UpdateMenuHandles(); + } + + break; + } + + if (IsHandleCreated) + { + UpdateMenuHandles(); + } + + Control.ControlCollection children = _ctlClient.Controls; + for (int i = children.Count - 1; i >= 0; i--) + { + if (children[i] is not Form childForm) + { + continue; + } + + if (childForm.Properties.TryGetValue(s_propMergedMenu, out MainMenu? mergedMenu)) + { + if (mergedMenu.ownerForm == childForm) + { + mergedMenu.Dispose(); + } + + childForm.Properties.RemoveValue(s_propMergedMenu); + } + } + + UpdateMenuHandles(); + break; + case Forms.Menu.CHANGE_VISIBLE: + if (ReferenceEquals(menu, Menu) + || (ActiveMdiChildInternal is not null && ReferenceEquals(menu, ActiveMdiChildInternal.Menu))) + { + UpdateMenuHandles(); + } + + break; + case Forms.Menu.CHANGE_MDI: + if (_ctlClient is not null && _ctlClient.IsHandleCreated) + { + UpdateMenuHandles(); + } + + break; + } +#pragma warning restore WFDEV006 + } + /// /// The activate event is fired when the form is activated. /// @@ -4209,6 +4311,13 @@ protected override void OnHandleCreated(EventArgs e) { SetScreenCaptureModeInternal(FormScreenCaptureMode); } + +#pragma warning disable WFDEV006 // Type or member is obsolete + if (Menu is not null || MainMenuStrip is not null || IsMdiContainer) + { + UpdateMenuHandles(recreateMenu: true); + } +#pragma warning restore WFDEV006 } /// @@ -4706,6 +4815,14 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) return true; } +#pragma warning disable WFDEV006 // Type or member is obsolete + if (Properties.TryGetValue(s_propCurMenu, out MainMenu? curMenu) + && curMenu.ProcessCmdKey(ref msg, keyData)) + { + return true; + } +#pragma warning restore WFDEV006 + // Process MDI accelerator keys. bool retValue = false; MSG win32Message = msg.ToMSG(); @@ -6170,6 +6287,24 @@ private unsafe void UpdateMenuHandles(bool recreateMenu = false) return; } +#pragma warning disable WFDEV006 // Type or member is obsolete + MainMenu? currentMenu = TopLevel + ? ActiveMdiChildInternal?.MergedMenuPrivate ?? Menu + : null; +#pragma warning restore WFDEV006 + + Properties.AddOrRemoveValue(s_propCurMenu, currentMenu); + + if (currentMenu is not null) + { + currentMenu.form = this; + PInvoke.SetMenu(this, (HMENU)(nint)currentMenu.Handle); + CommonProperties.xClearPreferredSizeCache(this); + PInvoke.DrawMenuBar(this); + _formStateEx[s_formStateExUpdateMenuHandlesDeferred] = 0; + return; + } + if (_ctlClient is null || !_ctlClient.IsHandleCreated) { PInvoke.SetMenu(this, HMENU.Null); @@ -6853,6 +6988,57 @@ private void WmExitMenuLoop(ref Message m) base.WndProc(ref m); } + /// + /// WM_INITMENUPOPUP handler. + /// + private void WmInitMenuPopup(ref Message m) + { +#pragma warning disable WFDEV006 // Type or member is obsolete + if (Properties.TryGetValue(s_propCurMenu, out MainMenu? curMenu) + && curMenu.ProcessInitMenuPopup((nint)m.WParamInternal)) + { + return; + } +#pragma warning restore WFDEV006 + + base.WndProc(ref m); + } + + /// + /// Handles the WM_MENUCHAR message. + /// + private void WmMenuChar(ref Message m) + { +#pragma warning disable WFDEV006 // Type or member is obsolete + if (!Properties.TryGetValue(s_propCurMenu, out MainMenu? curMenu) + && Properties.TryGetValue(s_propFormMdiParent, out Form? formMdiParent) + && formMdiParent.Menu is not null) + { + PInvokeCore.PostMessage(formMdiParent, PInvokeCore.WM_SYSCOMMAND, (WPARAM)PInvoke.SC_KEYMENU, (LPARAM)(nint)m.WParamInternal); + m.ResultInternal = (LRESULT)(nint)PARAM.FromLowHigh(0, 1); + return; + } + + curMenu?.WmMenuChar(ref m); + if (m.ResultInternal != 0) + { + return; + } +#pragma warning restore WFDEV006 + + base.WndProc(ref m); + } + + /// + /// WM_UNINITMENUPOPUP handler. + /// + private void WmUnInitMenuPopup(ref Message _) + { +#pragma warning disable WFDEV006 // Type or member is obsolete + Menu?.OnCollapse(EventArgs.Empty); +#pragma warning restore WFDEV006 + } + /// /// WM_GETMINMAXINFO handler /// @@ -6975,6 +7161,23 @@ private void WmNcButtonDown(ref Message m) /// private void WmNCDestroy(ref Message m) { +#pragma warning disable WFDEV006 // Type or member is obsolete + MainMenu? mainMenu = Menu; + MainMenu? curMenu = Properties.GetValueOrDefault(s_propCurMenu); + MainMenu? mergedMenu = Properties.GetValueOrDefault(s_propMergedMenu); + + mainMenu?.ClearHandles(); + if (!ReferenceEquals(curMenu, mainMenu)) + { + curMenu?.ClearHandles(); + } + + if (!ReferenceEquals(mergedMenu, curMenu) && !ReferenceEquals(mergedMenu, mainMenu)) + { + mergedMenu?.ClearHandles(); + } +#pragma warning restore WFDEV006 + base.WndProc(ref m); // Destroy the owner window, if we created one. We @@ -7200,6 +7403,15 @@ protected override void WndProc(ref Message m) // case PInvokeCore.WM_WINDOWPOSCHANGING: // WmWindowPosChanging(ref m); // break; + case PInvokeCore.WM_INITMENUPOPUP: + WmInitMenuPopup(ref m); + break; + case PInvokeCore.WM_UNINITMENUPOPUP: + WmUnInitMenuPopup(ref m); + break; + case PInvokeCore.WM_MENUCHAR: + WmMenuChar(ref m); + break; case PInvokeCore.WM_ENTERMENULOOP: WmEnterMenuLoop(ref m); break; @@ -7233,37 +7445,89 @@ protected override void WndProc(ref Message m) // Unsupported types don't support nullability. #nullable disable - /// - /// This property is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.MainMenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] + [SRCategory(nameof(SR.CatWindowStyle))] + [DefaultValue(null)] + [TypeConverter(typeof(ReferenceConverter))] [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public MainMenu Menu { - get; - set; + get => Properties.GetValueOrDefault(s_propMainMenu); + set + { + MainMenu mainMenu = Menu; + + if (mainMenu == value) + { + return; + } + + mainMenu?.form = null; + + Properties.AddOrRemoveValue(s_propMainMenu, value); + + if (value is not null) + { + value.form?.Menu = null; + + value.form = this; + } + + if (_formState[s_formStateSetClientSize] == 1 && !IsHandleCreated) + { + Size clientSize = ClientSize; + + ClientSize = clientSize; + } + + MenuChanged(Forms.Menu.CHANGE_ITEMS, value); + } } - /// - /// This property is provided for binary compatibility with .NET Framework and is not intended to be used directly from your code. - /// - [Obsolete( - Obsoletions.MainMenuMessage, - error: false, - DiagnosticId = Obsoletions.UnsupportedControlsDiagnosticId, - UrlFormat = Obsoletions.SharedUrlFormat)] - [EditorBrowsable(EditorBrowsableState.Never)] + [SRCategory(nameof(SR.CatWindowStyle))] [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Advanced)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public MainMenu MergedMenu + public MainMenu MergedMenu => MergedMenuPrivate; + + private MainMenu MergedMenuPrivate { - get; + get + { + if (!Properties.TryGetValue(s_propFormMdiParent, out Form formMdiParent) + || formMdiParent is null) + { + return null; + } + + if (Properties.TryGetValue(s_propMergedMenu, out MainMenu mergedMenu) + && mergedMenu is not null) + { + return mergedMenu; + } + + MainMenu parentMenu = formMdiParent.Menu; + MainMenu mainMenu = Menu; + + if (mainMenu is null) + { + return parentMenu; + } + + if (parentMenu is null) + { + return mainMenu; + } + + mergedMenu = new MainMenu + { + ownerForm = this + }; + + mergedMenu.MergeMenu(parentMenu); + mergedMenu.MergeMenu(mainMenu); + Properties.AddOrRemoveValue(s_propMergedMenu, mergedMenu); + return mergedMenu; + } } #nullable enable }