/**************************************************************************** ** ** Copyright (C) 2022 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt VS Tools. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Imaging; using Microsoft.Win32; namespace QtVsTools.Options { using Common; using QtVsTools.Core; using static Common.EnumExt; public enum BuildHost { [String("Windows")] Windows, [String("Linux SSH")] LinuxSSH, [String("Linux WSL")] LinuxWSL, } public partial class QtVersionsTable : UserControl { LazyFactory Lazy { get; } = new LazyFactory(); public QtVersionsTable() { InitializeComponent(); } [Flags] public enum Column { IsDefault = 0x10, VersionName = 0x20, Host = 0x40, Path = 0x80, Compiler = 0x100 } [Flags] public enum State { Unknown = 0x00, Existing = 0x01, Removed = 0x02, Modified = 0x04 } public class Field { public string Value { get; set; } public Control Control { get; set; } public DataGridCell Cell { get; set; } private string error; public string ValidationError { set { UpdateUi = value != error; error = value; } get { return error; } } public bool IsValid => string.IsNullOrEmpty(ValidationError); public ToolTip ToolTip => IsValid ? null : new ToolTip() { Content = ValidationError }; public int SelectionStart { get; set; } public bool UpdateUi { get; private set; } = false; } public class Row { static LazyFactory StaticLazy { get; } = new LazyFactory(); LazyFactory Lazy { get; } = new LazyFactory(); public Dictionary Fields => Lazy.Get(() => Fields, () => GetValues() .Select(field => new KeyValuePair(field, null)) .ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value)); public Field FieldDefault => Fields[Column.IsDefault] ?? (Fields[Column.IsDefault] = new Field()); public bool IsDefault { get => (FieldDefault.Value == true.ToString()); set => FieldDefault.Value = value.ToString(); } public Field FieldVersionName => Fields[Column.VersionName] ?? (Fields[Column.VersionName] = new Field()); public string VersionName { get => FieldVersionName.Value; set => FieldVersionName.Value = value; } public string InitialVersionName { get; set; } public Field FieldHost => Fields[Column.Host] ?? (Fields[Column.Host] = new Field()); public BuildHost Host { get => FieldHost.Value.Cast(defaultValue: BuildHost.Windows); set => FieldHost.Value = value.Cast(); } public Field FieldPath => Fields[Column.Path] ?? (Fields[Column.Path] = new Field()); public string Path { get => FieldPath.Value; set => FieldPath.Value = value; } public Field FieldCompiler => Fields[Column.Compiler] ?? (Fields[Column.Compiler] = new Field()); public string Compiler { get => FieldCompiler.Value; set => FieldCompiler.Value = value; } public bool LastRow { get; set; } public bool DefaultEnabled => !IsDefault && !LastRow; public bool NameEnabled => !LastRow; public bool CompilerEnabled => (Host != BuildHost.Windows); public Visibility RowContentVisibility => LastRow ? Visibility.Hidden : Visibility.Visible; public Visibility ButtonAddVisibility => LastRow ? Visibility.Visible : Visibility.Hidden; public Visibility ButtonBrowseVisibility => (!LastRow && Host == BuildHost.Windows) ? Visibility.Visible : Visibility.Hidden; public Thickness PathMargin => new Thickness(((Host == BuildHost.Windows) ? 22 : 2), 0, 2, 0); public FontWeight FontWeight => IsDefault ? FontWeights.Bold : FontWeights.Normal; public static ImageSource ExplorerIcon => StaticLazy.Get(() => ExplorerIcon, () => GetExplorerIcon()); public State State { get; set; } = State.Unknown; public bool RowVisible => State != State.Removed; } Field FocusedField { get; set; } List Rows => Lazy.Get(() => Rows, () => new List()); public IEnumerable Versions => Rows.TakeWhile(item => !item.LastRow); public void UpdateVersions(IEnumerable versions) { Rows.Clear(); Rows.AddRange(versions); Rows.Add(new Row { LastRow = true }); DataGrid.ItemsSource = Rows; FocusedField = null; Validate(true); Rows.ForEach(item => item.State = State.Existing); } public IEnumerable GetErrorMessages() { Validate(true); return Versions .Where(v => v.State != State.Removed) .SelectMany(v => v.Fields.Values.Select(f => f.ValidationError)) .Where(s => !string.IsNullOrEmpty(s)) .Distinct(); } void Validate(bool mustRefresh) { //////////////////////// // Validate cell values foreach (var version in Versions) { if (!version.State.HasFlag(State.Modified)) continue; ////////////////////// // Default validation if (version.State.HasFlag((State)Column.IsDefault)) { version.FieldDefault.ValidationError = null; if (version.IsDefault && version.Host != BuildHost.Windows) version.FieldDefault.ValidationError = "Default version: Host must be Windows"; mustRefresh |= version.FieldDefault.UpdateUi; } /////////////////// // Name validation if (version.State.HasFlag((State)Column.VersionName)) { version.FieldVersionName.ValidationError = null; if (string.IsNullOrEmpty(version.VersionName)) { version.FieldVersionName.ValidationError = "Name cannot be empty"; } else if (Versions.Where(otherVersion => otherVersion != version && otherVersion.VersionName == version.VersionName).Any()) { version.FieldVersionName.ValidationError = "Duplicate version names"; } mustRefresh |= version.FieldVersionName.UpdateUi; } /////////////////// // Host validation if (version.State.HasFlag((State)Column.Host)) { version.FieldHost.ValidationError = null; if (version.IsDefault && version.Host != BuildHost.Windows) version.FieldHost.ValidationError = "Default version: Host must be Windows"; mustRefresh |= version.FieldHost.UpdateUi; } /////////////////// // Path validation if (version.State.HasFlag((State)Column.Path)) { version.FieldPath.ValidationError = null; if (string.IsNullOrEmpty(version.Path)) { version.FieldPath.ValidationError = "Path cannot be empty"; } else if (version.Host == BuildHost.Windows) { string path = NormalizePath(version.Path); if (path == null) { version.FieldPath.ValidationError = "Invalid path format"; } else { if (!QMake.Exists(path)) version.FieldPath.ValidationError = "Cannot find qmake.exe"; } } mustRefresh |= version.FieldPath.UpdateUi; } /////////////////////// // Compiler validation if (version.State.HasFlag((State)Column.Compiler)) { version.FieldCompiler.ValidationError = null; if (string.IsNullOrEmpty(version.Compiler)) version.FieldCompiler.ValidationError = "Compiler cannot be empty"; mustRefresh |= version.FieldCompiler.UpdateUi; } } ////////////////////////////////////// // Refresh versions table if required if (mustRefresh) { // Reset bindings foreach (var version in Versions) { foreach (var field in version.Fields.Values) { field.Control = null; field.Cell = null; } } // Refresh UI DataGrid.Items.Refresh(); } } static readonly Brush InvalidCellBackground = new DrawingBrush { TileMode = TileMode.Tile, Viewport = new Rect(0.0, 0.0, 10.0, 10.0), ViewportUnits = BrushMappingMode.Absolute, Viewbox = new Rect(0.0, 0.0, 10.0, 10.0), ViewboxUnits = BrushMappingMode.Absolute, Drawing = new DrawingGroup { Children = new DrawingCollection { new GeometryDrawing { Brush = Brushes.Red, Geometry = new RectangleGeometry(new Rect(5, 0, 5, 10)) } } }, Transform = new RotateTransform { Angle = -135.0, CenterX = 0.5, CenterY = 0.5 }, Opacity = 0.25 }; void Control_Loaded(object sender, RoutedEventArgs e) { if (sender is Control control && GetBinding(control) is Row version) { if (version.LastRow) return; if (string.IsNullOrEmpty(control.Name) || !control.Name.TryCast(out Column column)) return; var field = version.Fields[column]; field.Control = control; field.Cell = FindContainingCell(control); if (field.Cell != null) { field.Cell.Background = field.IsValid ? Brushes.Transparent : InvalidCellBackground; } if (field == FocusedField) control.Focus(); if (control is TextBox textBox && field.SelectionStart >= 0) textBox.Select(field.SelectionStart, 0); } } void ComboBox_Loaded(object sender, RoutedEventArgs e) { if (sender is ComboBox comboBox && GetBinding(comboBox) is Row version) { comboBox.IsEnabled = false; var hosts = GetValues(typeof(BuildHost)); comboBox.ItemsSource = hosts; comboBox.Text = version.Host.Cast(); comboBox.SelectedIndex = (int)version.Host; comboBox.IsEnabled = true; } Control_Loaded(sender, e); } void Control_GotFocus(object sender, RoutedEventArgs e) { if (sender is Control control && GetBinding(control) is Row version) { if (string.IsNullOrEmpty(control.Name) || !control.Name.TryCast(out Column column)) return; var field = version.Fields[column]; if (field.Control != control) return; FocusedField = field; } } void Control_LostFocus(object sender, RoutedEventArgs e) { if (sender is Control control && GetBinding(control) is Row version) { if (string.IsNullOrEmpty(control.Name) || !control.Name.TryCast(out Column column)) return; var field = version.Fields[column]; if (field != FocusedField || field.Control != control) return; FocusedField = null; } } void TextBox_SelectionChanged(object sender, RoutedEventArgs e) { if (sender is TextBox textBox && GetBinding(textBox) is Row version) { if (string.IsNullOrEmpty(textBox.Name) || !textBox.Name.TryCast(out Column column)) return; var field = version.Fields[column]; if (field.Control != textBox) return; field.SelectionStart = textBox.SelectionStart; } } void TextBox_TextChanged(object sender, TextChangedEventArgs e) { if (sender is TextBox textBox && GetBinding(textBox) is Row version) { if (string.IsNullOrEmpty(textBox.Name) || !textBox.Name.TryCast(out Column column)) return; var field = version.Fields[column]; if (field == null || field.Control != textBox || field.Value == textBox.Text) return; field.SelectionStart = textBox.SelectionStart; field.Value = textBox.Text; version.State |= State.Modified | (State)column; Validate(false); } } void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (sender is ComboBox comboBox && GetBinding(comboBox) is Row version) { if (!comboBox.IsEnabled || comboBox.SelectedIndex < 0) return; if (string.IsNullOrEmpty(comboBox.Name) || !comboBox.Name.TryCast(out Column column)) return; string comboBoxValue = comboBox.Items[comboBox.SelectedIndex] as string; var field = version.Fields[column]; if (field == null || field.Control != comboBox || field.Value == comboBoxValue) return; field.Value = comboBoxValue; version.State |= State.Modified | (State)Column.Host; bool mustRefresh = false; if (version.Host != BuildHost.Windows && version.Compiler == "msvc") { version.Compiler = "g++"; version.FieldCompiler.SelectionStart = version.Compiler.Length; version.State |= (State)Column.Compiler; mustRefresh = true; } else if (version.Host == BuildHost.Windows && version.Compiler != "msvc") { version.Compiler = "msvc"; version.FieldCompiler.SelectionStart = version.Compiler.Length; version.State |= (State)Column.Compiler; mustRefresh = true; } Validate(mustRefresh); } } static void SetDefaultState(ref Row version, bool value) { version.IsDefault = value; version.State |= State.Modified | (State)Column.IsDefault; } void Default_Click(object sender, RoutedEventArgs e) { if (sender is CheckBox checkBox && GetBinding(checkBox) is Row version) { var defaultVersion = Rows.FirstOrDefault(row => row.IsDefault); if (defaultVersion != null) SetDefaultState(ref defaultVersion, false); SetDefaultState(ref version, true); Validate(true); } } void Add_Click(object sender, RoutedEventArgs e) { var version = new Row() { IsDefault = !Versions.Any(x => x.State != State.Removed), Host = BuildHost.Windows, Path = "", Compiler = "msvc", LastRow = false, State = State.Modified | (State)Column.VersionName | (State)Column.Host | (State)Column.Path | (State)Column.Compiler }; if (version.IsDefault) version.State |= (State)Column.IsDefault; Rows.Insert(Rows.Count - 1, version); FocusedField = version.FieldVersionName; Validate(true); } void Remove_Click(object sender, RoutedEventArgs e) { if (sender is Button button && GetBinding(button) is Row version) { version.State = State.Removed; if (version.IsDefault) { var first = Versions.FirstOrDefault(x => x.State != State.Removed); if (first != null) SetDefaultState(ref first, true); } Validate(true); } } static string NormalizePath(string path) { if (string.IsNullOrEmpty(path)) return path; try { return Path.GetFullPath(new Uri(path).LocalPath) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) .ToUpperInvariant(); } catch (UriFormatException) { return null; } } void Explorer_Click(object sender, RoutedEventArgs e) { if (sender is Button button && GetBinding(button) is Row version) { var openFileDialog = new OpenFileDialog() { AddExtension = false, CheckFileExists = true, CheckPathExists = true, Filter = "qmake Executable|qmake.exe", Title = "Qt VS Tools - Select qmake.exe" }; if (openFileDialog.ShowDialog() == true) { var qmakePath = openFileDialog.FileName; var qmakeDir = Path.GetDirectoryName(qmakePath); var previousPath = NormalizePath(version.Path); if (Path.GetFileName(qmakeDir) .Equals("bin", StringComparison.InvariantCultureIgnoreCase)) { qmakeDir = Path.GetDirectoryName(qmakeDir); version.Path = qmakeDir; } else { version.Path = qmakePath; } if (previousPath != NormalizePath(version.Path)) version.State |= State.Modified | (State)Column.Path; if (string.IsNullOrEmpty(version.VersionName)) { version.VersionName = string.Format("{0}_{1}", Path.GetFileName(Path.GetDirectoryName(qmakeDir)), Path.GetFileName(qmakeDir)) .Replace(" ", "_"); version.State |= State.Modified | (State)Column.VersionName; } Validate(true); } } } static ImageSource GetExplorerIcon() { var pathWindowsExplorer = string.Format(@"{0}\explorer.exe", Environment.GetFolderPath(Environment.SpecialFolder.Windows)); NativeAPI.SHFILEINFO shellFileInfo = new NativeAPI.SHFILEINFO(); NativeAPI.SHGetFileInfo(pathWindowsExplorer, 0, ref shellFileInfo, Marshal.SizeOf(shellFileInfo), NativeAPI.SHGFI.Icon | NativeAPI.SHGFI.SmallIcon); if (shellFileInfo.hIcon == IntPtr.Zero) return null; var iconImageSource = Imaging.CreateBitmapSourceFromHIcon( shellFileInfo.hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); NativeAPI.DestroyIcon(shellFileInfo.hIcon); return iconImageSource; } static object GetBinding(FrameworkElement control) { if (control == null || control.BindingGroup == null || control.BindingGroup.Items == null || control.BindingGroup.Items.Count == 0) { return null; } return control.BindingGroup.Items[0]; } static DataGridCell FindContainingCell(DependencyObject control) { while (control != null) { if (control is ContentPresenter contentPresenter && contentPresenter.Parent is DataGridCell cell) { return cell; } control = VisualTreeHelper.GetParent(control); } return null; } } }