sources/Google.Solutions.IapDesktop.Application/ToolWindows/ProjectExplorer/ProjectExplorerView.cs (342 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.Apis.Auth; using Google.Solutions.Apis.Crm; using Google.Solutions.Common.Diagnostics; using Google.Solutions.Common.Util; using Google.Solutions.IapDesktop.Application.Profile.Settings; using Google.Solutions.IapDesktop.Application.Properties; using Google.Solutions.IapDesktop.Application.ToolWindows.ProjectExplorer; using Google.Solutions.IapDesktop.Application.Windows.Dialog; using Google.Solutions.IapDesktop.Application.Windows.ProjectPicker; using Google.Solutions.IapDesktop.Core.ObjectModel; using Google.Solutions.IapDesktop.Core.ProjectModel; using Google.Solutions.Mvvm.Binding; using Google.Solutions.Mvvm.Binding.Commands; using Google.Solutions.Mvvm.Controls; using System; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows.Forms; using WeifenLuo.WinFormsUI.Docking; #pragma warning disable IDE1006 // Naming Styles namespace Google.Solutions.IapDesktop.Application.Windows.ProjectExplorer { [ComVisible(false)] [SkipCodeCoverage("Logic is in view model")] public partial class ProjectExplorerView : ToolWindowViewBase, IProjectExplorer, IView<ProjectExplorerViewModel> { private readonly IMainWindow mainForm; private readonly IJobService jobService; private readonly IAuthorization authorization; private readonly IExceptionDialog exceptionDialog; private readonly IProjectPickerDialog projectPickerDialog; private readonly Service<IResourceManagerClient> resourceManagerAdapter; private Bound<ProjectExplorerViewModel> viewModel; private Bound<CommandContainer<IProjectModelNode>> contextMenuCommands; private Bound<CommandContainer<IProjectModelNode>> toolbarCommands; public ICommandContainer<IProjectModelNode> ContextMenuCommands => this.contextMenuCommands.Value; public ICommandContainer<IProjectModelNode> ToolbarCommands => this.toolbarCommands.Value; public ProjectExplorerView(IServiceProvider serviceProvider) : base( serviceProvider.GetService<IMainWindow>(), serviceProvider.GetService<ToolWindowStateRepository>(), DockState.DockLeft) { InitializeComponent(); this.mainForm = serviceProvider.GetService<IMainWindow>(); this.jobService = serviceProvider.GetService<IJobService>(); this.authorization = serviceProvider.GetService<IAuthorization>(); this.exceptionDialog = serviceProvider.GetService<IExceptionDialog>(); this.projectPickerDialog = serviceProvider.GetService<IProjectPickerDialog>(); this.resourceManagerAdapter = serviceProvider.GetService<Service<IResourceManagerClient>>(); // // This window is a singleton, so we never want it to be closed, // just hidden. // Debug.Assert( ((ServiceRegistry)serviceProvider).Registrations[typeof(IProjectExplorer)] == ServiceLifetime.Singleton, "Service must be registered as singleton for HideOnClose to work"); this.HideOnClose = true; } public void Bind( ProjectExplorerViewModel viewModel, IBindingContext bindingContext) { this.viewModel.Value = viewModel; // // Bind tree view. // this.treeView.BindChildren(node => node.GetFilteredChildrenAsync(false)); this.treeView.BindImageIndex(node => node.ImageIndex); this.treeView.BindSelectedImageIndex(node => node.ImageIndex); this.treeView.BindIsExpanded(node => node.IsExpanded); this.treeView.BindIsLeaf(node => node.IsLeaf); this.treeView.BindText(node => node.Text); this.treeView.Bind(viewModel.RootNode, bindingContext); this.treeView.OnControlPropertyChange( c => c.SelectedModelNode, node => viewModel.SelectedNode = node, bindingContext); this.treeView.LoadingChildrenFailed += (sender, args) => { if (!args.Exception.IsCancellation()) { this.exceptionDialog.Show( this, "Loading project failed", args.Exception); } }; // // Bind search box and progress bar. // var searchButton = this.searchTextBox.AddOverlayButton(Resources.Search_16); this.progressBar.BindProperty( c => c.Enabled, viewModel, m => m.IsLoading, bindingContext); this.progressBar.BindProperty( c => c.Visible, viewModel, m => m.IsLoading, bindingContext); this.searchTextBox.BindProperty( c => c.Text, viewModel, m => m.InstanceFilter, bindingContext); // // Menus. // var contextSource = new ContextSource<IProjectModelNode>(); viewModel.OnPropertyChange( m => m.SelectedNode, node => { // // NB. Due to lazily loading, the model might not // be available yet. // if (node != null && node.IsLoaded) { contextSource.Context = node.ModelNode; } }, bindingContext); this.contextMenuCommands.Value = new CommandContainer<IProjectModelNode>( ToolStripItemDisplayStyle.ImageAndText, contextSource, bindingContext); this.toolbarCommands.Value = new CommandContainer<IProjectModelNode>( ToolStripItemDisplayStyle.Image, contextSource, bindingContext); // // Toolbar. // this.linuxInstancesToolStripMenuItem.BindProperty( c => c.Checked, viewModel, m => m.IsLinuxIncluded, bindingContext); this.windowsInstancesToolStripMenuItem.BindProperty( c => c.Checked, viewModel, m => m.IsWindowsIncluded, bindingContext); this.refreshButton.BindObservableCommand( viewModel, m => m.RefreshSelectedNodeCommand, bindingContext); // // Context menu. // this.contextMenuCommands.Value.AddCommand( new ContextCommand<IProjectModelNode>( "&Unload projects...", node => node is IProjectModelCloudNode ? CommandState.Enabled : CommandState.Unavailable, _ => UnloadProjectsAsync()) { Id = "UnloadProject", ActivityText = "Unloading projects" }); this.contextMenuCommands.Value.AddCommand( new ContextCommand<IProjectModelNode>( "&Refresh project", _ => viewModel.IsRefreshProjectsCommandVisible ? CommandState.Enabled : CommandState.Unavailable, _ => viewModel.RefreshSelectedNodeAsync()) { Id = "RefreshProject", Image = Resources.Refresh_16, ActivityText = "Refreshing project" }); this.contextMenuCommands.Value.AddCommand( new ContextCommand<IProjectModelNode>( "Refresh &all projects", _ => viewModel.IsRefreshAllProjectsCommandVisible ? CommandState.Enabled : CommandState.Unavailable, _ => viewModel.RefreshAsync(false)) { Id = "RefeshAllProjects", Image = Resources.Refresh_16, ActivityText = "Refreshing project" }); this.contextMenuCommands.Value.AddCommand( new ContextCommand<IProjectModelNode>( "&Unload project", _ => viewModel.IsUnloadProjectCommandVisible ? CommandState.Enabled : CommandState.Unavailable, _ => viewModel.UnloadSelectedProjectAsync()) { Id = "UnloadProject", ActivityText = "Unloading project" }); this.contextMenuCommands.Value.AddSeparator(); this.contextMenuCommands.Value.AddCommand( new ContextCommand<IProjectModelNode>( "Open in Cloud Consol&e", _ => viewModel.IsCloudConsoleCommandVisible ? CommandState.Enabled : CommandState.Unavailable, _ => viewModel.OpenInCloudConsole()) { Id = "OpenCloudConsole", ActivityText = "Opening Cloud Console" }); this.contextMenuCommands.Value.AddCommand( new ContextCommand<IProjectModelNode>( "Configure IAP a&ccess", _ => viewModel.IsCloudConsoleCommandVisible ? CommandState.Enabled : CommandState.Unavailable, _ => viewModel.ConfigureIapAccess()) { Id = "ConfigureIapAccess", ActivityText = "Opening Cloud Console" }); this.contextMenuCommands.Value.AddSeparator(); // // All commands added, apply to menu. // this.contextMenuCommands.Value.BindTo( this.contextMenu.Items, bindingContext); this.toolbarCommands.Value.BindTo( this.toolStrip.Items, bindingContext); } private async Task<bool> AddNewProjectAsync() { try { await this.jobService .RunAsync( new JobDescription("Loading projects..."), _ => this.authorization .Session .ApiCredential .GetAccessTokenForRequestAsync()) .ConfigureAwait(true); // // Show project picker // if (this.projectPickerDialog .SelectCloudProjects( this, "Add", this.resourceManagerAdapter.Activate(), out var projects) == DialogResult.OK) { await this.viewModel .Value .AddProjectsAsync(projects.ToArray()) .ConfigureAwait(true); return true; } else { return false; } } catch (Exception e) when (e.IsCancellation()) { // Ignore. return false; } catch (Exception e) { this.exceptionDialog.Show( this, "Adding project failed", e); return false; } } private async Task UnloadProjectsAsync() { if (this.projectPickerDialog.SelectLocalProjects( this, "Unload", this.viewModel.Value.Projects, out var projects) == DialogResult.OK) { await this.viewModel .Value .RemoveProjectsAsync(projects.ToArray()) .ConfigureAwait(true); } } private void treeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) { this.contextMenuCommands.Value.ExecuteDefaultCommand(); } //--------------------------------------------------------------------- // Tool bar event handlers. //--------------------------------------------------------------------- private async void addButton_Click(object sender, EventArgs args) => await AddNewProjectAsync().ConfigureAwait(true); //--------------------------------------------------------------------- // Other Windows event handlers. //--------------------------------------------------------------------- protected override void OnResize(EventArgs e) { base.OnResize(e); if (this.searchTextBox != null) { // // DPI scaling can produce a gap between controls. // Rearrange controls to remove this gap. // this.searchTextBox.Top = this.toolStrip.Height; this.progressBar.Top = this.searchTextBox.Bottom; var gap = this.treeView.Top - this.progressBar.Bottom; this.treeView.Top -= gap; this.treeView.Height += gap; } } private async void ProjectExplorerWindow_Shown(object sender, EventArgs _) { try { // // Expand projects. // // NB. It's not safe to do this in the constructor // because some of the dependencies might not be ready yet. // var projects = await this.viewModel.Value.ExpandRootAsync() .ConfigureAwait(true); // // Force-select the root node to update menus. // this.viewModel.Value.SelectedNode = this.viewModel.Value.RootNode; if (!projects.Any()) { // No projects in inventory yet - pop open the 'Add Project' // dialog to get the user started. await AddNewProjectAsync().ConfigureAwait(true); } } catch (Exception e) when (e.IsCancellation()) { // Most likely, the user rejected to reauthorize. Quit the app. this.mainForm.Close(); } catch (Exception e) { this.exceptionDialog.Show( this.mainForm, "Loading projects failed", e); // Do not close the application, otherwise the user has no // chance to remediate the situation by unloading the offending // project. } } private void ProjectExplorerWindow_KeyDown(object sender, KeyEventArgs e) { // NB. Hook KeyDown instead of KeyUp event to not interfere with // child dialogs. With KeyUp, we'd get an event if a child dialog // is dismissed by pressing Enter. if (e.KeyCode == Keys.F5) { this.refreshButton.PerformClick(); } else if (e.KeyCode == Keys.F3 || (e.Control && e.KeyCode == Keys.F)) { this.searchTextBox.Focus(); } else if (e.KeyCode == Keys.Enter) { this.contextMenuCommands.Value.ExecuteDefaultCommand(); } else { this.contextMenuCommands.Value.ExecuteCommandByKey(e.KeyCode); } } //--------------------------------------------------------------------- // IProjectExplorer. //--------------------------------------------------------------------- public async Task ShowAddProjectDialogAsync() { // // NB. The project explorer might be hidden and no project // might have been loaded yet. // if (await AddNewProjectAsync().ConfigureAwait(true)) { // // Show the window. That might kick off an asynchronous // Refresh if the window previously was not visible. // ShowWindow(); } } public IProjectModelNode? SelectedNode { get => this.viewModel.Value.SelectedNode?.ModelNode; } internal class NodeTreeView : BindableTreeView<ProjectExplorerViewModel.ViewModelNode> { } } }