/****************************************************************************
|
**
|
** 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<Column, Field> Fields => Lazy.Get(() =>
|
Fields, () => GetValues<Column>()
|
.Select(field => new KeyValuePair<Column, Field>(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<string>();
|
}
|
|
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<Row> Rows => Lazy.Get(() => Rows, () => new List<Row>());
|
public IEnumerable<Row> Versions => Rows.TakeWhile(item => !item.LastRow);
|
|
public void UpdateVersions(IEnumerable<Row> 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<string> 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<string>(typeof(BuildHost));
|
comboBox.ItemsSource = hosts;
|
comboBox.Text = version.Host.Cast<string>();
|
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;
|
}
|
}
|
}
|