sources/Google.Solutions.IapDesktop.Application/Windows/ToolWindowViewBase.cs (308 lines of code) (raw):
//
// Copyright 2020 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
using Google.Solutions.Common.Diagnostics;
using Google.Solutions.Common.Runtime;
using Google.Solutions.Common.Util;
using Google.Solutions.IapDesktop.Application.Profile.Settings;
using Google.Solutions.IapDesktop.Application.Theme;
using Google.Solutions.IapDesktop.Core.ObjectModel;
using Google.Solutions.Mvvm.Binding;
using Google.Solutions.Mvvm.Theme;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using WeifenLuo.WinFormsUI.Docking;
#nullable disable
namespace Google.Solutions.IapDesktop.Application.Windows
{
[ComVisible(false)]
[SkipCodeCoverage("GUI plumbing")]
public partial class ToolWindowViewBase : DockContent
{
private readonly DockPanel panel;
/// <summary>
/// State to use when opening/restoring the window next time.
/// </summary>
private DockState restoreState;
private void UpdateRestoreState(DockState newState)
{
Debug.Assert(this.DesignMode || this.restoreState != DockState.Unknown);
Debug.Assert(this.DesignMode || this.restoreState != DockState.Float);
Debug.Assert(this.DesignMode || this.restoreState != DockState.Hidden);
switch (newState)
{
case DockState.Unknown:
case DockState.Float:
//
// We don't restore these states, ignore.
//
break;
case DockState.Document:
case DockState.DockTop:
case DockState.DockLeft:
case DockState.DockBottom:
case DockState.DockRight:
case DockState.DockTopAutoHide:
case DockState.DockLeftAutoHide:
case DockState.DockBottomAutoHide:
case DockState.DockRightAutoHide:
//
// These are good states to restore to.
//
this.restoreState = newState;
break;
case DockState.Hidden:
//
// Ignore and keep the last good restore state instead.
//
break;
}
}
public ToolWindowViewBase()
{
InitializeComponent();
}
public ToolWindowViewBase(
IMainWindow mainWindow,
ToolWindowStateRepository stateRepository,
DockState defaultDockState) : this()
{
this.panel = mainWindow
.ExpectNotNull(nameof(mainWindow))
.MainPanel;
// Read persisted window state.
var state = stateRepository
.ExpectNotNull(nameof(stateRepository))
.GetSetting(
GetType().Name, // Unique name of tool window
defaultDockState);
this.restoreState = state.DockState.Value;
// Save persisted window state.
this.Disposed += (sender, args) =>
{
try
{
//
// Persist the restore state. This may or may not
// be the same we read during startup.
//
state.DockState.Value = this.restoreState;
stateRepository.SetSetting(state);
}
catch (Exception e)
{
ApplicationTraceSource.Log.TraceWarning(
"Saving tool window state failed: {0}", e.Message);
}
};
//
// When a tool window contains an ActiveX control, then
// it's possible that we receive mouse events while
// the window is being disposed (reentrancy).
//
// If we let these mouse events touch the context menu, then
// we're causing an ObjectDisposedException (b/237985825,
// b/238222518).
//
// To prevent this from happening, we have to detach the context
// menu *before* the ActiveX is disposed. The Dispose event
// is called *after* the ActiveX is disposed -- therefore,
// register as a component.
//
this.components.Add(Disposable
.Create((() =>
{
this.TabPageContextMenu = null;
this.TabPageContextMenuStrip = null;
}))
.ToComponent());
}
//---------------------------------------------------------------------
// Show/Hide.
//---------------------------------------------------------------------
public bool IsClosed { get; private set; } = false;
public bool ShowCloseMenuItemInContextMenu
{
get => this.closeMenuItem.Visible;
set => this.closeMenuItem.Visible = false;
}
public void CloseSafely()
{
if (this.HideOnClose)
{
Hide();
}
else
{
Close();
}
}
/// <summary>
/// Show or reactivate window.
/// </summary>
protected virtual void ShowWindow()
{
Debug.Assert(this.panel != null);
Debug.Assert(this.boundWindow != null, "Window has been bound");
this.TabText = this.Text;
//
// NB. IsHidden indicates that the window is not shown at all,
// not even as auto-hide.
//
if (this.IsHidden)
{
// Show in default position.
Show(this.panel, this.restoreState);
}
//
// If the window is in auto-hide mode, simply activating
// is not enough.
//
switch (this.VisibleState)
{
case DockState.DockTopAutoHide:
case DockState.DockBottomAutoHide:
case DockState.DockLeftAutoHide:
case DockState.DockRightAutoHide:
this.panel.ActiveAutoHideContent = this;
break;
}
//
// Move focus to window.
//
Activate();
//
// If an auto-hide window loses focus and closes, we fail to
// catch that event.
// To force an update, disregard the cached state and re-raise
// the UserVisibilityChanged event.
//
OnUserVisibilityChanged(true);
this.wasUserVisible = true;
}
public bool IsAutoHide
{
get
{
switch (this.VisibleState)
{
case DockState.DockTopAutoHide:
case DockState.DockBottomAutoHide:
case DockState.DockLeftAutoHide:
case DockState.DockRightAutoHide:
return true;
default:
return false;
}
}
set
{
switch (this.VisibleState)
{
case DockState.DockTop:
case DockState.DockTopAutoHide:
this.DockState = DockState.DockTopAutoHide;
break;
case DockState.DockBottom:
case DockState.DockBottomAutoHide:
this.DockState = DockState.DockBottomAutoHide;
break;
case DockState.DockLeft:
case DockState.DockLeftAutoHide:
this.DockState = DockState.DockLeftAutoHide;
break;
case DockState.DockRight:
case DockState.DockRightAutoHide:
this.DockState = DockState.DockRightAutoHide;
break;
}
}
}
public bool IsDocked
{
get
{
switch (this.VisibleState)
{
case DockState.DockTop:
case DockState.DockBottom:
case DockState.DockLeft:
case DockState.DockRight:
return true;
default:
return false;
}
}
}
public bool IsDockable => this.IsDocked || this.IsAutoHide || this.IsFloat;
//---------------------------------------------------------------------
// Window events.
//---------------------------------------------------------------------
private void closeMenuItem_Click(object sender, System.EventArgs e)
{
CloseSafely();
}
private void ToolWindow_KeyUp(object sender, KeyEventArgs e)
{
if (this.DockState != DockState.Document && e.Shift && e.KeyCode == Keys.Escape)
{
CloseSafely();
}
}
//---------------------------------------------------------------------
// Track visibility.
//
// NB. The DockPanel library does not provide good properties or evens
// that would allow you to determine whether a window is effectively
// visible to the user or not.
//
// This table shows the value of key properties based on the window state:
//
//
// ---------------------------------------------------------------------------------------
// | | | | | Pane.ActiveContent
// | State | IsActivated | IsFloat | Visible/DockState | == this
// ---------------------------------------------------------------------------------------
// Float | Single pane | (any) | TRUE | Float | TRUE
// | Split pane, focus | FALSE | TRUE | Float | TRUE
// | Split pane, no focus| TRUE | TRUE | Float | TRUE
// | Background | FALSE | TRUE | Float | FALSE
// ---------------------------------------------------------------------------------------
// AutoHide | Single | (any) | FALSE | DockRightAutoHide | TRUE
// | Background | (any) | FALSE | DockRightAutoHide | TRUE
// ---------------------------------------------------------------------------------------
// Dock | Single pane | TRUE | FALSE | DockRight | TRUE
// | Split pane, focus | FALSE (!) | FALSE | DockRight | TRUE
// | Split pane, no focus| TRUE (!) | FALSE | DockRight | TRUE
// | Background | FALSE | FALSE | DockRight | FALSE
// -----------------------------------------------------------------------------------------
//
// IsHidden is TRUE during construction, and FALSE ever after.
// When docked and hidden, the size is reset to (0, 0)
//
protected bool IsInBackground =>
(this.IsFloat && this.Pane.ActiveContent != this) ||
(this.IsAutoHide && this.Size.Height == 0 && this.Size.Width == 0) ||
(this.IsDocked && this.Pane.ActiveContent != this);
protected bool IsUserVisible => !this.IsHidden && !this.IsInBackground;
private bool wasUserVisible = false;
private void RaiseUserVisibilityChanged()
{
// Only call OnUserVisibilityChanged if there really was a change.
if (this.IsUserVisible != this.wasUserVisible)
{
OnUserVisibilityChanged(this.IsUserVisible);
this.wasUserVisible = this.IsUserVisible;
}
}
protected override void OnEnter(EventArgs e)
{
base.OnEnter(e);
UpdateRestoreState(this.DockState);
RaiseUserVisibilityChanged();
}
protected override void OnLeave(EventArgs e)
{
base.OnLeave(e);
UpdateRestoreState(this.DockState);
RaiseUserVisibilityChanged();
}
protected override void OnVisibleChanged(EventArgs e)
{
base.OnVisibleChanged(e);
UpdateRestoreState(this.DockState);
RaiseUserVisibilityChanged();
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
this.IsClosed = false;
UpdateRestoreState(this.DockState);
RaiseUserVisibilityChanged();
}
protected override void OnClosed(EventArgs e)
{
// NB. This method might be invoked more than once if a disconnect
// event coincides (which is reasnably common when closing the app
// with active sessions).
base.OnClosed(e);
this.IsClosed = true;
}
protected virtual void OnUserVisibilityChanged(bool visible)
{
// Can be overridden in derived class.
}
//---------------------------------------------------------------------
// Factory and MVVM binding.
//---------------------------------------------------------------------
private object boundWindow;
/// <summary>
/// Gets or creates a MVVM-enabled tool window and prepares it for viewing.
/// Callers have the opportunity to customize the view model before calling
/// .Show() on the returned object.
/// </summary>
internal static BoundToolWindow<TToolWindowView, TToolWindowViewModel> GetToolWindow<TToolWindowView, TToolWindowViewModel>(
IServiceProvider serviceProvider)
where TToolWindowView : ToolWindowViewBase, IView<TToolWindowViewModel>
where TToolWindowViewModel : ViewModelBase
{
//
// NB. ToolWindows can be singletons, and we must not bind them
// multiple times.
//
var view = serviceProvider.GetService<TToolWindowView>();
if (view.boundWindow != null)
{
//
// This is a singleton and it has been bound before.
//
return (BoundToolWindow<TToolWindowView, TToolWindowViewModel>)view.boundWindow;
}
else
{
//
// This is new object (transient or singleton), and it
// has not been bound yet.
//
// Create an intermediate object that lets the caller initialize the
// view model before calling Show().
//
var boundWindow = new BoundToolWindow<TToolWindowView, TToolWindowViewModel>(
view,
serviceProvider.GetService<TToolWindowViewModel>(),
serviceProvider.GetService<IBindingContext>(),
serviceProvider.GetService<IToolWindowTheme>());
view.boundWindow = boundWindow;
if (view.HideOnClose)
{
Debug.Assert(
((ServiceRegistry)serviceProvider).Registrations[typeof(TToolWindowView)] == ServiceLifetime.Singleton,
"HideOnClose windows should be singletons");
}
return boundWindow;
}
}
internal class BoundToolWindow<TToolWindowView, TToolWindowViewModel>
: IToolWindow<TToolWindowView, TToolWindowViewModel>
where TToolWindowView : ToolWindowViewBase, IView<TToolWindowViewModel>
where TToolWindowViewModel : ViewModelBase
{
private bool bound = false;
private readonly IControlTheme theme;
private readonly TToolWindowView view;
private readonly IBindingContext bindingContext;
public BoundToolWindow(
TToolWindowView view,
TToolWindowViewModel viewModel,
IBindingContext bindingContext,
IControlTheme theme)
{
this.view = view.ExpectNotNull(nameof(view));
this.ViewModel = viewModel.ExpectNotNull(nameof(viewModel));
this.bindingContext = bindingContext.ExpectNotNull(nameof(bindingContext));
this.theme = theme;
}
public TToolWindowViewModel ViewModel { get; }
/// <summary>
/// Explicitly perform a bind to access the view. Prefer
/// to call Show() instead.
/// </summary>
public TToolWindowView Bind()
{
if (!this.bound)
{
//
// The caller had sufficient opportunity to initialize
// the view mode, so we can now bind it to the view.
//
TopLevelWindow<TToolWindowView, TToolWindowViewModel, IToolWindowTheme>.Bind(
this.view,
this.ViewModel,
this.theme,
this.bindingContext);
this.bound = true;
}
return this.view;
}
/// <summary>
/// Bind and show the tool window.
/// </summary>
public void Show()
{
Bind();
this.view.ShowWindow();
}
}
}
}