PropertyBox/SRxPropertyBoxControl.psm1.txt
|
<#
================================================================================ SRxPropertyBoxControl.psm1 (STEP 3 - fully implemented) ----------------------------------------------------------------------------- Self-contained WinForms UserControl encapsulating the entire "Term custom properties" subsystem, decoupled from the Explorer host. Host uses it by COMPOSITION + DYNAMIC DISPATCH (untyped field). WinForms/Drawing must be loaded before this module is imported. Create via New-SRxPropertyBoxControl. Host contract: Properties : SourcePath, Icons(@{Save;Stop;Trash;Close;Busy}) Callbacks : OnNavigate($token), OnClose(), OnStatus($text) Methods : BindContext($ctx), HasUnsavedChanges(), RequestNavigate($ctx), RequestClose(), ApplyIcons(), Shutdown() Exposes : GlobalManagerPanel() A $ctx is @{ TermId; DisplayName; Tag(byref to node.Tag); Token(the node) }. Node-scoped state persists on $ctx.Tag (PropUpdate/EditBase/Draft/CustomProperties). ================================================================================ #> # Bring the property-box logic + merge functions into THIS module's scope so the # class methods (ConvertTo-SRxLogicalProps, Split-SRxChunkedProperties, # Start/Get/Stop-SRxTermBatchOperation, Merge-SRxTermProperties, Show-SRxMergeDialog, # Sync-SRxLocalTermBatch, Remove-SRxBatchTempFiles) can resolve them. Import-Module (Join-Path $PSScriptRoot 'SRxPropertyBox.psm1') -DisableNameChecking -Force Import-Module (Join-Path $PSScriptRoot 'SRxTermMerge.psm1') -DisableNameChecking -Force class SRxPropertyBoxControl : System.Windows.Forms.UserControl { # ---- public config ---- [string] $SourcePath = '' [hashtable] $Icons = @{} # ---- public callbacks ---- [scriptblock] $OnNavigate = $null [scriptblock] $OnClose = $null [scriptblock] $OnStatus = $null # ---- internal UI ---- hidden [System.Windows.Forms.ToolStrip] $_title = $null hidden [System.Windows.Forms.ToolStripLabel] $_titleLabel = $null hidden [System.Windows.Forms.ToolStripButton] $_btnSave = $null hidden [System.Windows.Forms.ToolStripButton] $_btnClose = $null hidden [System.Windows.Forms.DataGridView] $_dataGridView = $null hidden [System.Windows.Forms.DataGridViewImageColumn] $_colDelete = $null hidden [System.Windows.Forms.Panel] $_propOverlay = $null hidden [System.Windows.Forms.Label] $_propOverlayMsg = $null hidden [System.Windows.Forms.Button] $_propOverlaySave = $null hidden [System.Windows.Forms.Button] $_propOverlayDiscard = $null hidden [System.Windows.Forms.Button] $_propOverlayCancel = $null # ---- global manager bar (built here, docked by host) ---- hidden [System.Windows.Forms.Panel] $_globalMgr = $null hidden [System.Windows.Forms.Label] $_globalMgrLabel = $null hidden [System.Windows.Forms.PictureBox] $_globalMgrKill = $null hidden [System.Windows.Forms.PictureBox] $_globalMgrBusy = $null # ---- engine ---- hidden [System.Windows.Forms.Timer] $_batchTimer = $null hidden [System.Windows.Forms.Timer] $_spinTimer = $null hidden [System.Windows.Forms.Timer] $_dbReloadTimer = $null hidden [System.Collections.Hashtable] $_activeBatches = @{} hidden [System.Drawing.Font] $_strikeFont = $null hidden [boolean] $_suppressGuard = $false hidden [int] $_hoverDelRow = -1 # ---- current context + pending overlay action ---- hidden [object] $_currentCtx = $null hidden [object] $_pendingTargetCtx = $null hidden [string] $_pendingAction = '' SRxPropertyBoxControl() { $this.Dock = [System.Windows.Forms.DockStyle]::Fill $this.buildLayout() $this.buildGlobalManager() $this.buildTimers() } # ========================================================== layout hidden [void] buildLayout() { $this._title = [System.Windows.Forms.ToolStrip]::new() $this._title.Dock = [System.Windows.Forms.DockStyle]::Top $this._title.GripStyle = [System.Windows.Forms.ToolStripGripStyle]::Hidden $this._title.BackColor = [System.Drawing.SystemColors]::ControlLight $this._title.ImageScalingSize = [System.Drawing.Size]::new(20, 20) $this._btnSave = [System.Windows.Forms.ToolStripButton]::new() $this._btnSave.Name = '_btnSave' $this._btnSave.DisplayStyle = [System.Windows.Forms.ToolStripItemDisplayStyle]::Image $this._btnSave.Alignment = [System.Windows.Forms.ToolStripItemAlignment]::Left $this._btnSave.AutoSize = $false $this._btnSave.Size = [System.Drawing.Size]::new(24, 24) $this._btnSave.ImageScaling = [System.Windows.Forms.ToolStripItemImageScaling]::SizeToFit $this._btnSave.AutoToolTip = $false $this._btnSave.ToolTipText = '' $this._btnSave.Enabled = $false $this._btnSave.Visible = $false $this._btnSave.Tag = $this $this._btnSave.add_Click({ param($s,$e) $s.Tag.onSaveButtonClick() }) $this._btnSave.add_MouseEnter({ param($s,$e) $s.Tag.onSaveButtonMouseEnter() }) $this._btnSave.add_MouseLeave({ param($s,$e) $s.Tag.onSaveButtonMouseLeave() }) $this._titleLabel = [System.Windows.Forms.ToolStripLabel]::new('Term custom properties') $this._btnClose = [System.Windows.Forms.ToolStripButton]::new() $this._btnClose.Alignment = [System.Windows.Forms.ToolStripItemAlignment]::Right $this._btnClose.DisplayStyle = [System.Windows.Forms.ToolStripItemDisplayStyle]::Image $this._btnClose.AutoSize = $false $this._btnClose.Size = [System.Drawing.Size]::new(24, 24) $this._btnClose.ImageScaling = [System.Windows.Forms.ToolStripItemImageScaling]::SizeToFit $this._btnClose.AutoToolTip = $false $this._btnClose.Tag = $this $this._btnClose.add_Click({ param($s,$e) $s.Tag.RequestClose() }) [void]$this._title.Items.Add($this._btnSave) [void]$this._title.Items.Add($this._titleLabel) [void]$this._title.Items.Add($this._btnClose) $this._dataGridView = [System.Windows.Forms.DataGridView]::new() $this._dataGridView.Dock = [System.Windows.Forms.DockStyle]::Fill $this._dataGridView.Name = 'dataGridView' $this._dataGridView.BorderStyle = 'None' $this._dataGridView.ColumnHeadersVisible = $false $this._dataGridView.RowHeadersVisible = $false $this._dataGridView.EditMode = [System.Windows.Forms.DataGridViewEditMode]::EditOnKeystrokeOrF2 $this._dataGridView.AllowUserToAddRows = $true $this._colDelete = New-Object System.Windows.Forms.DataGridViewImageColumn $this._colDelete.Name = 'Del' $this._colDelete.HeaderText = '' $this._colDelete.Width = 24 $this._colDelete.ImageLayout = [System.Windows.Forms.DataGridViewImageCellLayout]::Zoom $this._colDelete.DefaultCellStyle.NullValue = $null $this._colDelete.DefaultCellStyle.BackColor = [System.Drawing.Color]::White $this._colDelete.DefaultCellStyle.SelectionBackColor = [System.Drawing.Color]::White $this._colDelete.Resizable = [System.Windows.Forms.DataGridViewTriState]::False [void]$this._dataGridView.Columns.Add($this._colDelete) $colKey = [System.Windows.Forms.DataGridViewTextBoxColumn]::new() $colKey.Name = 'Key'; $colKey.HeaderText = 'Attribute'; $colKey.Width = 100 [void]$this._dataGridView.Columns.Add($colKey) $colVal = [System.Windows.Forms.DataGridViewTextBoxColumn]::new() $colVal.Name = 'Value'; $colVal.HeaderText = 'Value' $colVal.AutoSizeMode = [System.Windows.Forms.DataGridViewAutoSizeColumnMode]::Fill [void]$this._dataGridView.Columns.Add($colVal) $this._strikeFont = [System.Drawing.Font]::new($this._dataGridView.Font, [System.Drawing.FontStyle]::Strikeout) $this._dataGridView.Tag = $this $this._dataGridView.add_CellContentClick({ param($s,$e) $s.Tag.onGridDeleteClick($e.RowIndex, $e.ColumnIndex) }) $this._dataGridView.add_CellValueChanged({ param($s,$e) $s.Tag.onGridValueChanged($e.RowIndex, $e.ColumnIndex) }) $this._dataGridView.add_CellFormatting({ param($s,$e) $s.Tag.onGridFormatting($e) }) $this._dataGridView.add_CellBeginEdit({ param($s,$e) $s.Tag.onGridBeginEdit($e) }) $this._dataGridView.add_KeyDown({ param($s,$e) $s.Tag.onGridKeyDown($e) }) $this._dataGridView.add_CellMouseEnter({ param($s,$e) $s.Tag.onGridCellMouseEnter($e.RowIndex, $e.ColumnIndex) }) $this._dataGridView.add_CellMouseLeave({ param($s,$e) $s.Tag.onGridCellMouseLeave($e.RowIndex, $e.ColumnIndex) }) $this._dataGridView.add_CurrentCellDirtyStateChanged({ param($s,$e) if ($s.IsCurrentCellDirty) { $s.CommitEdit([System.Windows.Forms.DataGridViewDataErrorContexts]::Commit) } }) # overlay (child of grid, Dock=Fill => covers grid, auto-resizes) $this._propOverlay = [System.Windows.Forms.Panel]::new() $this._propOverlay.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250) $this._propOverlay.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle $this._propOverlay.Visible = $false $this._propOverlay.Dock = [System.Windows.Forms.DockStyle]::Fill $this._propOverlayMsg = [System.Windows.Forms.Label]::new() $this._propOverlayMsg.AutoSize = $false $this._propOverlayMsg.Font = [System.Drawing.Font]::new('Segoe UI', 9) $this._propOverlayMsg.ForeColor = [System.Drawing.Color]::FromArgb(30, 30, 30) $this._propOverlayMsg.Location = [System.Drawing.Point]::new(16, 16) $this._propOverlayMsg.Size = [System.Drawing.Size]::new(360, 48) $this._propOverlayMsg.Text = 'You have unsaved property changes.' [void]$this._propOverlay.Controls.Add($this._propOverlayMsg) $this._propOverlaySave = [System.Windows.Forms.Button]::new() $this._propOverlaySave.Text = 'Save changes' $this._propOverlaySave.Font = [System.Drawing.Font]::new('Segoe UI', 9) $this._propOverlaySave.Size = [System.Drawing.Size]::new(160, 30) $this._propOverlaySave.Location = [System.Drawing.Point]::new(16, 76) $this._propOverlaySave.FlatStyle = [System.Windows.Forms.FlatStyle]::System $this._propOverlaySave.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $this._propOverlaySave.ImageAlign = [System.Drawing.ContentAlignment]::MiddleLeft $this._propOverlaySave.TextImageRelation = [System.Windows.Forms.TextImageRelation]::ImageBeforeText $this._propOverlaySave.Padding = [System.Windows.Forms.Padding]::new(6, 0, 0, 0) $this._propOverlaySave.Tag = $this $this._propOverlaySave.add_Click({ param($s,$e) $s.Tag.onOverlaySave() }) [void]$this._propOverlay.Controls.Add($this._propOverlaySave) $this._propOverlayDiscard = [System.Windows.Forms.Button]::new() $this._propOverlayDiscard.Text = 'Discard changes' $this._propOverlayDiscard.Font = [System.Drawing.Font]::new('Segoe UI', 9) $this._propOverlayDiscard.Size = [System.Drawing.Size]::new(160, 30) $this._propOverlayDiscard.Location = [System.Drawing.Point]::new(16, 112) $this._propOverlayDiscard.FlatStyle = [System.Windows.Forms.FlatStyle]::System $this._propOverlayDiscard.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $this._propOverlayDiscard.Tag = $this $this._propOverlayDiscard.add_Click({ param($s,$e) $s.Tag.onOverlayDiscard() }) [void]$this._propOverlay.Controls.Add($this._propOverlayDiscard) $this._propOverlayCancel = [System.Windows.Forms.Button]::new() $this._propOverlayCancel.Text = 'Cancel' $this._propOverlayCancel.Font = [System.Drawing.Font]::new('Segoe UI', 9) $this._propOverlayCancel.Size = [System.Drawing.Size]::new(160, 30) $this._propOverlayCancel.Location = [System.Drawing.Point]::new(16, 148) $this._propOverlayCancel.FlatStyle = [System.Windows.Forms.FlatStyle]::System $this._propOverlayCancel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $this._propOverlayCancel.Tag = $this $this._propOverlayCancel.add_Click({ param($s,$e) $s.Tag.onOverlayCancel() }) [void]$this._propOverlay.Controls.Add($this._propOverlayCancel) [void]$this._dataGridView.Controls.Add($this._propOverlay) [void]$this.Controls.Add($this._dataGridView) [void]$this.Controls.Add($this._title) } hidden [void] buildGlobalManager() { $this._globalMgr = [System.Windows.Forms.Panel]::new() $this._globalMgr.Height = 30 $this._globalMgr.Dock = [System.Windows.Forms.DockStyle]::Top $this._globalMgr.BackColor = [System.Drawing.Color]::White $this._globalMgr.Visible = $false $this._globalMgrKill = [System.Windows.Forms.PictureBox]::new() $this._globalMgrKill.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom $this._globalMgrKill.Size = [System.Drawing.Size]::new(20, 20) $this._globalMgrKill.Location = [System.Drawing.Point]::new(8, 5) $this._globalMgrKill.BackColor = [System.Drawing.Color]::Transparent $this._globalMgrKill.Cursor = [System.Windows.Forms.Cursors]::Hand $this._globalMgrKill.Tag = $this $this._globalMgrKill.add_Click({ param($s,$e) $s.Tag.killAllBatches() }) [void]$this._globalMgr.Controls.Add($this._globalMgrKill) $this._globalMgrBusy = [System.Windows.Forms.PictureBox]::new() $this._globalMgrBusy.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom $this._globalMgrBusy.Size = [System.Drawing.Size]::new(20, 20) $this._globalMgrBusy.Location = [System.Drawing.Point]::new(36, 5) $this._globalMgrBusy.BackColor = [System.Drawing.Color]::Transparent $this._globalMgrBusy.Visible = $false [void]$this._globalMgr.Controls.Add($this._globalMgrBusy) $this._globalMgrLabel = [System.Windows.Forms.Label]::new() $this._globalMgrLabel.AutoSize = $true $this._globalMgrLabel.Font = [System.Drawing.Font]::new('Segoe UI', 9) $this._globalMgrLabel.ForeColor = [System.Drawing.Color]::FromArgb(30, 30, 30) $this._globalMgrLabel.Location = [System.Drawing.Point]::new(64, 7) $this._globalMgrLabel.Text = 'Properties update in progress' [void]$this._globalMgr.Controls.Add($this._globalMgrLabel) } hidden [void] buildTimers() { $this._batchTimer = [System.Windows.Forms.Timer]::new() $this._batchTimer.Interval = 500 $this._batchTimer.Tag = $this $this._batchTimer.add_tick({ param($s,$e) $s.Tag.pollBatches() }) $this._spinTimer = [System.Windows.Forms.Timer]::new() $this._spinTimer.Interval = 120 $this._spinTimer.Tag = $this $this._spinTimer.add_tick({ param($s,$e) $s.Tag.spinGlobalManager() }) $this._dbReloadTimer = [System.Windows.Forms.Timer]::new() $this._dbReloadTimer.Interval = 750 $this._dbReloadTimer.Tag = $this $this._dbReloadTimer.add_tick({ param($s,$e) $s.Tag.onDbReloadTick() }) } # ========================================================== public API [System.Windows.Forms.Panel] GlobalManagerPanel() { return $this._globalMgr } [void] ApplyIcons() { try { if ($this.Icons.ContainsKey('Save') -and $this.Icons['Save']) { $this._btnSave.Image = $this.Icons['Save'] } if ($this.Icons.ContainsKey('Close') -and $this.Icons['Close']) { $this._btnClose.Image = $this.Icons['Close']; $this._globalMgrKill.Image = $this.Icons['Close'] } if ($this.Icons.ContainsKey('Busy') -and $this.Icons['Busy']) { $this._globalMgrBusy.Image = $this.Icons['Busy'] } $this.updateSaveButton() } catch { Write-Host "ApplyIcons - $($_.Exception.Message)" } } [void] BindContext($ctx) { $this._currentCtx = $ctx $this.bindNodeToBox($ctx) } [bool] HasUnsavedChanges() { return $this.nodeHasChanges() } [bool] RequestNavigate($targetCtx) { try { if (-not $this.nodeHasChanges()) { return $true } $this._pendingTargetCtx = $targetCtx $this._pendingAction = 'switch' $this.showUnsavedOverlay() return $false } catch { Write-Host "RequestNavigate - $($_.Exception.Message)"; return $true } } [void] RequestClose() { try { if ($this.nodeHasChanges()) { $this._pendingAction = 'close' $this.showUnsavedOverlay() } else { $this.doClose() } } catch { Write-Host "RequestClose - $($_.Exception.Message)" } } [void] Shutdown() { try { if ($this._batchTimer) { $this._batchTimer.Stop() } if ($this._spinTimer) { $this._spinTimer.Stop() } if ($this._dbReloadTimer) { $this._dbReloadTimer.Stop() } foreach ($entry in @($this._activeBatches.GetEnumerator())) { try { Stop-SRxTermBatchOperation -Operation $entry.Value.Op | Out-Null } catch {} } $this._activeBatches.Clear() } catch { Write-Host "Shutdown - $($_.Exception.Message)" } } # ========================================================== callback helpers hidden [void] setStatus([string]$text) { try { if ($this.OnStatus) { & $this.OnStatus $text } } catch { } } hidden [void] doClose() { try { if ($this.OnClose) { & $this.OnClose } } catch { } } # ========================================================== grid model hidden [void] bindNodeToBox($ctx) { try { $this._suppressGuard = $true [void]$this._dataGridView.Rows.Clear() if ($null -ne $ctx) { if ($ctx.Tag.PSObject.Properties.Name -contains 'Draft' -and $ctx.Tag.Draft) { $this.loadDraftRows($ctx.Tag.Draft) } else { $props = $ctx.Tag.CustomProperties if ($null -ne $props) { $logical = ConvertTo-SRxLogicalProps -Raw ([hashtable]$this.toHashtable($props)) foreach ($k in ($logical.Keys | Sort-Object)) { $idx = $this._dataGridView.Rows.Add() $row = $this._dataGridView.Rows[$idx] $row.Cells['Key'].Value = $k $row.Cells['Value'].Value = $logical[$k] $row.Tag = [pscustomobject]@{ Key=$k; Original=[string]$logical[$k]; IsNew=$false; DeleteIntent=$false } $row.Cells['Key'].ReadOnly = $true } } } } $this._dataGridView.ClearSelection() $this.updateSaveButton() } catch { Write-Host "bindNodeToBox - $($_.Exception.Message)" } finally { $this._suppressGuard = $false } } hidden [hashtable] toHashtable($obj) { $h = @{} if ($obj -is [hashtable]) { foreach ($k in $obj.Keys) { $h[$k] = $obj[$k] } } elseif ($null -ne $obj) { foreach ($p in $obj.PSObject.Properties) { $h[$p.Name] = $p.Value } } return $h } hidden [void] loadDraftRows($draft) { $this._suppressGuard = $true foreach ($r in $draft.Rows) { $idx = $this._dataGridView.Rows.Add() $row = $this._dataGridView.Rows[$idx] $row.Cells['Key'].Value = $r.Key $row.Cells['Value'].Value = $r.Value $row.Tag = [pscustomobject]@{ Key=$r.Key; Original=$r.Original; IsNew=$r.IsNew; DeleteIntent=$r.DeleteIntent } $row.Cells['Key'].ReadOnly = (-not $r.IsNew) } $this._suppressGuard = $false } hidden [boolean] nodeHasChanges() { foreach ($gr in $this._dataGridView.Rows) { if ($gr.IsNewRow) { continue } if (-not $gr.Tag) { continue } if ($gr.Tag.IsNew) { return $true } if ($gr.Tag.DeleteIntent) { return $true } if ([string]$gr.Cells['Value'].Value -ne [string]$gr.Tag.Original) { return $true } } return $false } hidden [hashtable] buildDesiredSet() { $desired = @{} foreach ($gr in $this._dataGridView.Rows) { if ($gr.IsNewRow) { continue } if (-not $gr.Tag) { continue } if ($gr.Tag.DeleteIntent) { continue } $k = [string]$gr.Cells['Key'].Value if ([string]::IsNullOrWhiteSpace($k)) { continue } $desired[$k] = [string]$gr.Cells['Value'].Value } return $desired } hidden [hashtable] buildOriginalSet() { $orig = @{} foreach ($gr in $this._dataGridView.Rows) { if ($gr.IsNewRow) { continue } if (-not $gr.Tag -or $gr.Tag.IsNew) { continue } $orig[[string]$gr.Tag.Key] = [string]$gr.Tag.Original } return $orig } # ========================================================== grid handlers hidden [void] onGridBeginEdit($e) { try { if ($e.RowIndex -lt 0) { return } $row = $this._dataGridView.Rows[$e.RowIndex] if ($row.IsNewRow) { return } $col = $this._dataGridView.Columns[$e.ColumnIndex].Name if ($col -eq 'Del') { $e.Cancel = $true; return } if ($col -eq 'Key' -and $row.Tag -and -not $row.Tag.IsNew) { $e.Cancel = $true } if ($col -eq 'Value' -and $row.Tag -and $row.Tag.DeleteIntent) { $e.Cancel = $true } } catch { Write-Host "onGridBeginEdit - $($_.Exception.Message)" } } hidden [void] onGridValueChanged([int]$rowIndex, [int]$columnIndex) { try { if ($this._suppressGuard) { return } if ($rowIndex -lt 0) { return } $row = $this._dataGridView.Rows[$rowIndex] if ($row.IsNewRow) { return } if (-not $row.Tag) { $row.Tag = [pscustomobject]@{ Key=[string]$row.Cells['Key'].Value; Original=''; IsNew=$true; DeleteIntent=$false } $row.Cells['Key'].ReadOnly = $false } $this.captureEditBase($this._currentCtx) $this._dataGridView.InvalidateRow($rowIndex) $this.updateSaveButton() } catch { Write-Host "onGridValueChanged - $($_.Exception.Message)" } } hidden [void] onGridKeyDown($e) { try { if ($e.KeyCode -ne [System.Windows.Forms.Keys]::Escape) { return } $cell = $this._dataGridView.CurrentCell if ($null -eq $cell) { return } $row = $this._dataGridView.Rows[$cell.RowIndex] if ($row.IsNewRow -or -not $row.Tag) { return } $col = $this._dataGridView.Columns[$cell.ColumnIndex].Name if ($col -eq 'Value' -and -not $row.Tag.IsNew) { $this._dataGridView.CancelEdit() $row.Cells['Value'].Value = $row.Tag.Original $this._dataGridView.InvalidateRow($cell.RowIndex) $this.updateSaveButton() $e.Handled = $true } } catch { Write-Host "onGridKeyDown - $($_.Exception.Message)" } } hidden [void] onGridDeleteClick([int]$rowIndex, [int]$columnIndex) { try { if ($rowIndex -lt 0 -or $columnIndex -lt 0) { return } if ($this._dataGridView.Columns[$columnIndex].Name -ne 'Del') { return } $row = $this._dataGridView.Rows[$rowIndex] if ($row.IsNewRow -or -not $row.Tag) { return } if ($row.Tag.IsNew) { [void]$this._dataGridView.Rows.RemoveAt($rowIndex) } else { $row.Tag.DeleteIntent = -not $row.Tag.DeleteIntent $this.captureEditBase($this._currentCtx) $this._dataGridView.InvalidateRow($rowIndex) } $this.updateSaveButton() } catch { Write-Host "onGridDeleteClick - $($_.Exception.Message)" } } hidden [void] onGridCellMouseEnter([int]$rowIndex, [int]$columnIndex) { try { if ($rowIndex -lt 0 -or $columnIndex -lt 0) { return } if ($this._dataGridView.Columns[$columnIndex].Name -ne 'Del') { return } $row = $this._dataGridView.Rows[$rowIndex] if ($row.IsNewRow -or -not $row.Tag) { return } $cell = $row.Cells['Del'] $cell.Style.BackColor = [System.Drawing.Color]::FromArgb(225, 235, 245) $this._dataGridView.Cursor = [System.Windows.Forms.Cursors]::Hand $this._hoverDelRow = $rowIndex } catch { Write-Host "onGridCellMouseEnter - $($_.Exception.Message)" } } hidden [void] onGridCellMouseLeave([int]$rowIndex, [int]$columnIndex) { try { if ($rowIndex -lt 0 -or $columnIndex -lt 0) { return } if ($this._dataGridView.Columns[$columnIndex].Name -ne 'Del') { return } $row = $this._dataGridView.Rows[$rowIndex] $cell = $row.Cells['Del'] $cell.Style.BackColor = [System.Drawing.Color]::White $this._dataGridView.Cursor = [System.Windows.Forms.Cursors]::Default $this._hoverDelRow = -1 } catch { Write-Host "onGridCellMouseLeave - $($_.Exception.Message)" } } hidden [void] onGridFormatting($e) { try { if ($e.RowIndex -lt 0) { return } $row = $this._dataGridView.Rows[$e.RowIndex] $col = $this._dataGridView.Columns[$e.ColumnIndex].Name if ($row.IsNewRow -or -not $row.Tag) { if ($col -eq 'Del') { $e.Value = $null } return } $t = $row.Tag if ($col -eq 'Del') { if ($this.Icons.ContainsKey('Trash') -and $this.Icons['Trash']) { $e.Value = $this.Icons['Trash'] } else { $e.Value = $null } return } if ($t.DeleteIntent) { $e.CellStyle.Font = $this._strikeFont $e.CellStyle.ForeColor = [System.Drawing.Color]::Gray } elseif ($t.IsNew) { $e.CellStyle.ForeColor = [System.Drawing.Color]::Red } elseif ($col -eq 'Value') { if ([string]$row.Cells['Value'].Value -ne [string]$t.Original) { $e.CellStyle.ForeColor = [System.Drawing.Color]::Red } else { $e.CellStyle.ForeColor = [System.Drawing.Color]::Black } } } catch { Write-Host "onGridFormatting - $($_.Exception.Message)" } } # ========================================================== save button hidden [void] updateSaveButton() { try { $ctx = $this._currentCtx $hasChanges = $this.nodeHasChanges() $running = ($null -ne $ctx -and ($ctx.Tag.PSObject.Properties.Name -contains 'PropUpdate') -and $ctx.Tag.PropUpdate) $this._btnSave.Text = '' $this._btnSave.AutoToolTip = $false $this._btnSave.ToolTipText = '' if ($hasChanges) { $this._btnSave.Visible = $true $this._btnSave.Enabled = $true if ($this.Icons.ContainsKey('Save') -and $this.Icons['Save']) { $this._btnSave.Image = $this.Icons['Save'] } } elseif ($running) { $this._btnSave.Visible = $true $this._btnSave.Enabled = $true if ($this.Icons.ContainsKey('Stop') -and $this.Icons['Stop']) { $this._btnSave.Image = $this.Icons['Stop'] } elseif ($this.Icons.ContainsKey('Save') -and $this.Icons['Save']) { $this._btnSave.Image = $this.Icons['Save'] } } else { $this._btnSave.Visible = $false $this._btnSave.Enabled = $false } } catch { Write-Host "updateSaveButton - $($_.Exception.Message)" } } hidden [void] onSaveButtonMouseEnter() { try { if (-not $this._btnSave.Enabled) { return } $this._btnSave.BackColor = [System.Drawing.Color]::FromArgb(225, 235, 245) if ($null -ne $this._title) { $this._title.Cursor = [System.Windows.Forms.Cursors]::Hand } } catch { Write-Host "onSaveButtonMouseEnter - $($_.Exception.Message)" } } hidden [void] onSaveButtonMouseLeave() { try { $this._btnSave.BackColor = [System.Drawing.Color]::Transparent if ($null -ne $this._title) { $this._title.Cursor = [System.Windows.Forms.Cursors]::Default } } catch { Write-Host "onSaveButtonMouseLeave - $($_.Exception.Message)" } } hidden [void] onSaveButtonClick() { try { $ctx = $this._currentCtx if ($null -eq $ctx) { return } if ($this.nodeHasChanges()) { $this.startBatchForNode($ctx) } elseif (($ctx.Tag.PSObject.Properties.Name -contains 'PropUpdate') -and $ctx.Tag.PropUpdate) { $this.killNodeBatch($ctx) } } catch { Write-Host "onSaveButtonClick - $($_.Exception.Message)" } } # ========================================================== batch engine hidden [void] startBatchForNode($ctx) { try { $termId = [string]$ctx.TermId if ([string]::IsNullOrEmpty($termId)) { $termId = [string]$ctx.Tag.termID } $desired = $this.buildDesiredSet() $original = $this.buildOriginalSet() $op = Start-SRxTermBatchOperation -TermId $termId -DesiredLogical $desired ` -OriginalLogical $original -BaselineModified '' ` -SourcePath $this.SourcePath -HideWindow if ($op.StartError) { $this.setStatus("Batch failed to start: $($op.StartError)"); return } $ctx.Tag | Add-Member -NotePropertyName PropUpdate -NotePropertyValue $op -Force $this._activeBatches[[string]$op.ProcessId] = @{ Ctx = $ctx; Op = $op } $ctx.Tag.CustomProperties = (Split-SRxChunkedProperties -Logical $desired) if ($ctx.Tag.PSObject.Properties.Name -contains 'Draft') { $ctx.Tag.Draft = $null } $this.bindNodeToBox($ctx) $this.setStatus("Batch started for '$($ctx.DisplayName)' (PID $($op.ProcessId)).") if (-not $this._batchTimer.Enabled) { $this._batchTimer.Start() } $this.updateGlobalManager() $this.updateSaveButton() } catch { Write-Host "startBatchForNode - $($_.Exception.Message)" } } hidden [void] pollBatches() { try { if ($this._activeBatches.Count -eq 0) { $this._batchTimer.Stop(); $this.updateGlobalManager(); return } $done = @() $finishedTermIds = @() foreach ($entry in @($this._activeBatches.GetEnumerator())) { $key = $entry.Key $ctx = $entry.Value.Ctx $op = $entry.Value.Op $st = Get-SRxTermBatchOperationState -Operation $op if ($st.State -eq 'Running') { continue } if ($st.State -eq 'Completed' -and $st.Success) { if ($st.Data.PropsAfter) { $ctx.Tag.CustomProperties = $this.toHashtable($st.Data.PropsAfter) } elseif ($st.Data.PropsAfterRaw) { $ctx.Tag.CustomProperties = $this.toHashtable((ConvertTo-SRxLogicalProps -Raw ([hashtable]$this.toHashtable($st.Data.PropsAfterRaw)))) } if ($st.Data.OuterXml) { $ctx.Tag.OuterXml = $st.Data.OuterXml } if ($ctx.Tag.PSObject.Properties.Name -contains 'EditBase') { $ctx.Tag.EditBase = $null } if ($st.Data.SyncWarning) { $this.setStatus("Saved '$($ctx.DisplayName)' (note: $($st.Data.SyncWarning))") } else { $this.setStatus("Saved '$($ctx.DisplayName)'.") } $this.scheduleDbReload() } elseif ($st.State -eq 'Conflict') { $this.handleConflict($ctx, $op, $st) } else { $this.setStatus("Batch failed for '$($ctx.DisplayName)': $($st.Error)") } Remove-SRxBatchTempFiles -Operation $op if (($ctx.Tag.PSObject.Properties.Name -contains 'PropUpdate') -and $ctx.Tag.PropUpdate -and ` ([string]$ctx.Tag.PropUpdate.ProcessId -eq [string]$op.ProcessId)) { $ctx.Tag.PropUpdate = $null } $done += $key $finishedTermIds += [string]$ctx.TermId } foreach ($k in $done) { $this._activeBatches.Remove($k) } $sel = $this._currentCtx if ($null -ne $sel -and ($finishedTermIds -contains [string]$sel.TermId)) { if (-not $this._dataGridView.IsCurrentCellInEditMode) { $this.bindNodeToBox($sel) } else { $this.updateSaveButton() } } else { $this.updateSaveButton() } if ($this._activeBatches.Count -eq 0) { $this._batchTimer.Stop() $this.setStatus('Ready') } $this.updateGlobalManager() } catch { Write-Host "pollBatches - $($_.Exception.Message)" } } hidden [void] scheduleDbReload() { try { if ($null -eq $this._dbReloadTimer) { return } $this._dbReloadTimer.Stop() $this._dbReloadTimer.Start() } catch { Write-Host "scheduleDbReload - $($_.Exception.Message)" } } hidden [void] onDbReloadTick() { try { $this._dbReloadTimer.Stop() if ($this._activeBatches.Count -gt 0 -or $this._dataGridView.IsCurrentCellInEditMode) { $this._dbReloadTimer.Start() return } if (Get-Command Connect-SRxProvisioningDatabase_JSON -ErrorAction SilentlyContinue) { Connect-SRxProvisioningDatabase_JSON | Out-Null } } catch { Write-Host "onDbReloadTick - $($_.Exception.Message)" } } hidden [void] captureEditBase($ctx) { try { if ($null -eq $ctx) { return } if ($ctx.Tag.PSObject.Properties.Name -contains 'EditBase' -and $ctx.Tag.EditBase) { return } $baseLogical = @{} $props = $ctx.Tag.CustomProperties if ($null -ne $props) { $baseLogical = ConvertTo-SRxLogicalProps -Raw ([hashtable]$this.toHashtable($props)) } $ctx.Tag | Add-Member -NotePropertyName EditBase -NotePropertyValue $baseLogical -Force } catch { Write-Host "captureEditBase - $($_.Exception.Message)" } } hidden [void] handleConflict($ctx, $op, $st) { try { $base = @{} if ($ctx.Tag.PSObject.Properties.Name -contains 'EditBase' -and $ctx.Tag.EditBase) { $base = [hashtable]$this.toHashtable($ctx.Tag.EditBase) } $mine = @{} if ($op.DesiredLogical) { $mine = [hashtable]$this.toHashtable($op.DesiredLogical) } $theirs = @{} if ($st.Data -and $st.Data.RemoteProps) { $theirs = [hashtable]$this.toHashtable($st.Data.RemoteProps) } $remoteModified = if ($st.Data -and $st.Data.RemoteModified) { [string]$st.Data.RemoteModified } else { '' } $baselineModified = if ($ctx.Tag.PSObject.Properties.Name -contains 'BaselineModified' -and $ctx.Tag.BaselineModified) { [string]$ctx.Tag.BaselineModified } else { '' } $merge = Merge-SRxTermProperties -Base $base -Mine $mine -Theirs $theirs $mergedSet = $null if ($merge.HasConflicts) { $mergedSet = Show-SRxMergeDialog -MergeResult $merge -TermName ([string]$ctx.DisplayName) ` -RemoteModified $remoteModified -BaselineModified $baselineModified if ($null -eq $mergedSet) { $this.setStatus("Merge cancelled for '$($ctx.DisplayName)'; local changes kept, nothing written.") return } } else { $mergedSet = $merge.Merged $this.setStatus("Auto-merged non-conflicting changes for '$($ctx.DisplayName)'.") } $termId = [string]$op.TermId $forceOp = Start-SRxTermBatchOperation -TermId $termId -DesiredLogical $mergedSet ` -OriginalLogical @{} -BaselineModified '' -SourcePath $this.SourcePath -Force -HideWindow if ($forceOp.StartError) { $this.setStatus("Merged commit failed to start: $($forceOp.StartError)") return } $ctx.Tag | Add-Member -NotePropertyName PropUpdate -NotePropertyValue $forceOp -Force $this._activeBatches[[string]$forceOp.ProcessId] = @{ Ctx = $ctx; Op = $forceOp } $ctx.Tag.CustomProperties = (Split-SRxChunkedProperties -Logical $mergedSet) if ($ctx.Tag.PSObject.Properties.Name -contains 'EditBase') { $ctx.Tag.EditBase = $null } if ($ctx.Tag.PSObject.Properties.Name -contains 'BaselineModified') { $ctx.Tag.BaselineModified = $null } if ($ctx.Tag.PSObject.Properties.Name -contains 'Draft') { $ctx.Tag.Draft = $null } if ($null -ne $this._currentCtx -and ([string]$this._currentCtx.TermId -eq [string]$ctx.TermId)) { $this.bindNodeToBox($ctx) } if (-not $this._batchTimer.Enabled) { $this._batchTimer.Start() } $this.updateGlobalManager() $this.updateSaveButton() $this.setStatus("Committing merged result for '$($ctx.DisplayName)' (PID $($forceOp.ProcessId))...") } catch { Write-Host "handleConflict - $($_.Exception.Message)" } } hidden [void] killNodeBatch($ctx) { try { if (($ctx.Tag.PSObject.Properties.Name -contains 'PropUpdate') -and $ctx.Tag.PropUpdate) { [void](Stop-SRxTermBatchOperation -Operation $ctx.Tag.PropUpdate) $this.setStatus("Killed batch for '$($ctx.DisplayName)'.") } } catch { Write-Host "killNodeBatch - $($_.Exception.Message)" } } hidden [void] killAllBatches() { try { foreach ($entry in @($this._activeBatches.GetEnumerator())) { try { Stop-SRxTermBatchOperation -Operation $entry.Value.Op | Out-Null } catch {} } $this.setStatus("Killed all running property batches.") } catch { Write-Host "killAllBatches - $($_.Exception.Message)" } } hidden [void] updateGlobalManager() { try { $active = ($this._activeBatches.Count -gt 0) $this._globalMgr.Visible = $active if ($null -ne $this._globalMgrBusy) { $this._globalMgrBusy.Visible = $active } if ($active) { $this._globalMgrLabel.Text = "Properties update in progress ($($this._activeBatches.Count))" if (-not $this._spinTimer.Enabled) { $this._spinTimer.Start() } } else { if ($this._spinTimer.Enabled) { $this._spinTimer.Stop() } } } catch { Write-Host "updateGlobalManager - $($_.Exception.Message)" } } hidden [void] spinGlobalManager() { try { $this._globalMgrLabel.Text = "Properties update in progress ($($this._activeBatches.Count))" } catch { } } # ========================================================== overlay hidden [void] showUnsavedOverlay() { try { $name = '' if ($null -ne $this._currentCtx) { $name = [string]$this._currentCtx.DisplayName } $this._propOverlayMsg.Text = "You have unsaved property changes for '$name'.`r`nWhat would you like to do?" if ($this.Icons.ContainsKey('Save') -and $this.Icons['Save']) { $this._propOverlaySave.Image = $this.Icons['Save'] } $this._propOverlay.Dock = [System.Windows.Forms.DockStyle]::Fill $this._propOverlay.Visible = $true $this._propOverlay.BringToFront() } catch { Write-Host "showUnsavedOverlay - $($_.Exception.Message)" } } hidden [void] hideUnsavedOverlay() { try { $this._propOverlay.Visible = $false } catch { Write-Host "hideUnsavedOverlay - $($_.Exception.Message)" } } hidden [void] finishPendingAction() { try { $action = $this._pendingAction $this._pendingAction = '' if ($action -eq 'switch') { $target = $this._pendingTargetCtx $this._pendingTargetCtx = $null if ($null -ne $target -and $this.OnNavigate) { & $this.OnNavigate $target.Token } } elseif ($action -eq 'close') { $this.doClose() } } catch { Write-Host "finishPendingAction - $($_.Exception.Message)" } } hidden [void] onOverlaySave() { try { $ctx = $this._currentCtx $this.hideUnsavedOverlay() if ($null -ne $ctx) { $this.startBatchForNode($ctx) } $this.finishPendingAction() } catch { Write-Host "onOverlaySave - $($_.Exception.Message)" } } hidden [void] onOverlayDiscard() { try { $ctx = $this._currentCtx if ($null -ne $ctx) { if ($ctx.Tag.PSObject.Properties.Name -contains 'Draft') { $ctx.Tag.Draft = $null } if ($ctx.Tag.PSObject.Properties.Name -contains 'EditBase') { $ctx.Tag.EditBase = $null } } $this.hideUnsavedOverlay() if ($null -ne $ctx) { $this.bindNodeToBox($ctx) } $this.finishPendingAction() } catch { Write-Host "onOverlayDiscard - $($_.Exception.Message)" } } hidden [void] onOverlayCancel() { try { $this._pendingTargetCtx = $null $this._pendingAction = '' $this.hideUnsavedOverlay() } catch { Write-Host "onOverlayCancel - $($_.Exception.Message)" } } } function New-SRxPropertyBoxControl { [OutputType([object])] param() return [SRxPropertyBoxControl]::new() } Export-ModuleMember -Function New-SRxPropertyBoxControl |