fix(ux): owner-anchor dialogs + sidebar reopen + Klear pane title

Three related polish fixes:

1. Modal dialogs (File-to-Case / Compose-from-Case / Settings) used to
   open without an Owner, so they would sink behind other windows and
   the user perceived Outlook as "frozen". AttachOwnerToOutlook now
   sets WindowInteropHelper.Owner to the foreground HWND (Win32
   GetForegroundWindow with MainWindowHandle as fallback) before
   ShowDialog. Also sets WindowStartupLocation=CenterOwner and pumps
   SetForegroundWindow + Activate on Loaded.

2. Sidebar pane title was "פרטי תיק (EspoCRM)" — renamed to
   "Klear · פרטי תיק" to match the new branding.

3. New ribbon button "פרטי תיק" (Marcus-Law group, between Klear and
   הגדרות) re-opens the sidebar after the user clicks its X.
   Implementation:
     - SidebarController.ShowForActiveExplorer() toggles existing pane
       Visible back on, or attaches one if missing.
     - ExplorerBinding.Pane is now public on the binding for this
       reuse.
     - AddInHost.ShowSidebar() forwards to the controller.
     - ExplorerRibbon.OnShowSidebar wires the ribbon callback.
   imageMso = ReadingPaneShow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
PointStar
2026-05-11 17:10:32 +03:00
parent 3bd8b6dfbb
commit dc04a96bdd
4 changed files with 103 additions and 4 deletions
@@ -86,6 +86,11 @@ namespace OutlookAddin.Ribbon
Globals.ThisAddIn.AddInHost.LaunchSettings();
}
public void OnShowSidebar(IRibbonControl control)
{
Globals.ThisAddIn.AddInHost.ShowSidebar();
}
public bool OnGetComposeFromCaseVisible(IRibbonControl control)
{
// Show only on outgoing (compose) mail inspectors.
@@ -12,6 +12,13 @@
imageMso="MoveToFolder"
onAction="OnFileToEspoCrm"
getEnabled="OnGetFileToEspoCrmEnabled" />
<button id="ShowSidebar"
label="פרטי תיק"
screentip="הצג את סרגל פרטי התיק"
supertip="פותח מחדש את הסרגל הימני שמציג את איש הקשר והתיקים המקושרים לשולח של המייל הנבחר."
size="large"
imageMso="ReadingPaneShow"
onAction="OnShowSidebar" />
<button id="OpenEspoCrmSettings"
label="הגדרות"
screentip="הגדרות תוסף Klear"
+61
View File
@@ -10,6 +10,7 @@ using MarcusLaw.OutlookAddin.Core.Logging;
using MarcusLaw.OutlookAddin.Core.Models;
using MarcusLaw.OutlookAddin.Core.Security;
using MarcusLaw.OutlookAddin.Core.Services;
using System.Windows.Interop;
using MarcusLaw.OutlookAddin.UI.Dialogs;
using MarcusLaw.OutlookAddin.UI.ViewModels;
using Microsoft.Extensions.Caching.Memory;
@@ -78,6 +79,53 @@ namespace OutlookAddin.Services
HookItemSend();
}
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
/// <summary>
/// Wires a WPF <see cref="Window"/> to be a modal child of whichever
/// Outlook window is currently in the foreground (Explorer or
/// Inspector). Without this, ShowDialog() launches the dialog with
/// no owner — it sinks behind other windows and the user sees
/// Outlook as "frozen" until they alt-tab back to find it.
/// </summary>
private void AttachOwnerToOutlook(Window dialog)
{
try
{
var hwnd = GetForegroundWindow();
if (hwnd == IntPtr.Zero)
{
hwnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
}
if (hwnd == IntPtr.Zero) return;
new WindowInteropHelper(dialog).Owner = hwnd;
dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner;
dialog.ShowInTaskbar = false;
dialog.Topmost = false;
// First chance to get focus once shown.
dialog.Loaded += (_, __) =>
{
try
{
var helper = new WindowInteropHelper(dialog);
if (helper.Handle != IntPtr.Zero) SetForegroundWindow(helper.Handle);
dialog.Activate();
}
catch { /* ignore */ }
};
}
catch (Exception ex)
{
Logger.Warning(ex, "AttachOwnerToOutlook failed");
}
}
private static void EnableModernTls()
{
try
@@ -361,6 +409,7 @@ namespace OutlookAddin.Services
var vm = new ComposeFromCaseViewModel(CrmClient!, Logger);
var dialog = new ComposeFromCaseDialog(vm);
AttachOwnerToOutlook(dialog);
var ok = dialog.ShowDialog();
if (ok != true || vm.Result == null) return;
@@ -419,6 +468,16 @@ namespace OutlookAddin.Services
/// Opens the Settings dialog. On Save, tears down and rebuilds the CRM
/// client/filing/retry stack so the new credentials take effect immediately.
/// </summary>
/// <summary>
/// Re-opens the right-hand sidebar for the active Explorer. Used by
/// the "פרטי תיק" ribbon button so the user can recover after
/// clicking the pane's X.
/// </summary>
public void ShowSidebar()
{
_sidebarController?.ShowForActiveExplorer();
}
public bool LaunchSettings()
{
var vm = new SettingsViewModel(Settings, SettingsManager, CredentialStore, Logger);
@@ -432,6 +491,7 @@ namespace OutlookAddin.Services
Logger.Warning(ex, "LaunchSettings: folder tree load failed");
}
var dialog = new SettingsDialog(vm);
AttachOwnerToOutlook(dialog);
var ok = dialog.ShowDialog();
if (ok != true) return false;
@@ -489,6 +549,7 @@ namespace OutlookAddin.Services
};
var dialog = new FileToCaseDialog(vm);
AttachOwnerToOutlook(dialog);
var ok = dialog.ShowDialog();
Logger.Information(
"FileToCase dialog closed (ok={Ok}, selected id={Id}, entityType={Type})",
+30 -4
View File
@@ -22,7 +22,7 @@ namespace OutlookAddin.Services
/// </summary>
public sealed class SidebarController : IDisposable
{
private const string PaneTitle = "פרטי תיק (EspoCRM)";
private const string PaneTitle = "Klear · פרטי תיק";
private const int DefaultWidth = 320;
private readonly CustomTaskPaneCollection _taskPanes;
@@ -141,6 +141,32 @@ namespace OutlookAddin.Services
finally { Marshal.Release(ptr); }
}
/// <summary>
/// Brings the sidebar back for the current Explorer — used by the
/// ribbon "פרטי תיק" button so the user can re-open the pane after
/// closing its X.
/// </summary>
public void ShowForActiveExplorer()
{
try
{
var explorer = _app.ActiveExplorer();
if (explorer == null) return;
var key = ExplorerKey(explorer);
if (_bindings.TryGetValue(key, out var binding))
{
binding.Pane.Visible = true;
return;
}
AttachToExplorer(explorer);
}
catch (Exception ex)
{
_logger.Warning(ex, "SidebarController.ShowForActiveExplorer failed");
}
}
private void TryOpenInBrowser(string url)
{
try
@@ -177,7 +203,7 @@ namespace OutlookAddin.Services
private sealed class ExplorerBinding : IDisposable
{
private readonly CustomTaskPane _pane;
public CustomTaskPane Pane { get; }
private readonly System.Windows.Forms.UserControl _hostControl;
private readonly ElementHost _element;
private readonly ExplorerSelectionHandler _handler;
@@ -193,7 +219,7 @@ namespace OutlookAddin.Services
ExplorerSelectionHandler handler,
Outlook.Explorer explorer)
{
_pane = pane;
Pane = pane;
_hostControl = hostControl;
_element = element;
ViewModel = viewModel;
@@ -204,7 +230,7 @@ namespace OutlookAddin.Services
public void Dispose()
{
try { _handler.Dispose(); } catch { /* ignore */ }
try { _pane.Visible = false; } catch { /* ignore */ }
try { Pane.Visible = false; } catch { /* ignore */ }
try { _element.Dispose(); } catch { /* ignore */ }
try { _hostControl.Dispose(); } catch { /* ignore */ }
}