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(); } } } }