2023-06-09 11:44:39 +02:00

503 lines
22 KiB
C#

//
// Copyright 2020 Electronic Arts Inc.
//
// The Command & Conquer Map Editor and corresponding source code is free
// software: you can redistribute it and/or modify it under the terms of
// the GNU General Public License as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
// The Command & Conquer Map Editor and corresponding source code is distributed
// in the hope that it will be useful, but with permitted additional restrictions
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
// distributed with this program. You should have received a copy of the
// GNU General Public License along with permitted additional restrictions
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
using MobiusEditor.Interface;
using MobiusEditor.Model;
using MobiusEditor.Utility;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
namespace MobiusEditor.Controls
{
public partial class ObjectProperties : UserControl
{
private Bitmap infoImage;
private bool isMockObject;
private HouseType originalHouse;
private String originalTrigger;
private int originalStrength;
public IGamePlugin Plugin { get; private set; }
private string triggerToolTip;
private INotifyPropertyChanged obj;
public INotifyPropertyChanged Object
{
get => obj;
set
{
if (!ReferenceEquals(obj, value))
{
// old obj
if (obj != null)
{
obj.PropertyChanged -= Obj_PropertyChanged;
}
// new obj
obj = value;
if (obj != null)
{
if (obj is Building bld)
{
AdjustToStructurePrebuiltStatus(bld, GetHouseComboBox());
}
obj.PropertyChanged += Obj_PropertyChanged;
}
Rebind();
}
}
}
public ObjectProperties()
{
InitializeComponent();
infoImage = new Bitmap(27, 27);
infoImage.SetResolution(96, 96);
using (Graphics g = Graphics.FromImage(infoImage))
{
g.DrawIcon(SystemIcons.Information, new Rectangle(0, 0, infoImage.Width, infoImage.Height));
}
lblTriggerInfo.Image = infoImage;
lblTriggerInfo.ImageAlign = ContentAlignment.MiddleCenter;
}
private void ObjectProperties_Load(Object sender, EventArgs e)
{
// Fix for the fact the resize in the very first Rebind() call never works correctly,
// because the UI is not initialised yet at that point.
//this.Height = tableLayoutPanel1.PreferredSize.Height;
}
private void ObjectProperties_Resize(Object sender, EventArgs e)
{
//int prefH = tableLayoutPanel1.PreferredSize.Height + 10;
//if (this.Height != prefH)
//{
// this.Height = prefH;
//}
}
public void Initialize(IGamePlugin plugin, bool isMockObject)
{
this.isMockObject = isMockObject;
Plugin = plugin;
plugin.Map.TriggersUpdated -= Triggers_CollectionChanged;
plugin.Map.TriggersUpdated += Triggers_CollectionChanged;
houseComboBox.DataSource = plugin.Map.Houses.Select(t => new TypeItem<HouseType>(t.Type.Name, t.Type)).ToArray();
missionComboBox.DataSource = plugin.Map.MissionTypes;
UpdateDataSource();
Disposed += (sender, e) =>
{
Object = null;
plugin.Map.TriggersUpdated -= Triggers_CollectionChanged;
};
}
private void Triggers_CollectionChanged(object sender, EventArgs e)
{
UpdateDataSource();
}
private void UpdateDataSource()
{
string selected = triggerComboBox.SelectedItem as string;
triggerComboBox.DataBindings.Clear();
triggerComboBox.DataSource = null;
triggerComboBox.Items.Clear();
string[] items;
string[] filteredEvents;
string[] filteredActions;
Boolean isAircraft = obj is Unit un && un.Type.IsAircraft;
Boolean isOnMap = true;
switch (obj)
{
case Infantry infantry:
case Unit unit:
items = Plugin.Map.FilterUnitTriggers().Select(t => t.Name).Distinct().ToArray();
filteredEvents = Plugin.Map.EventTypes.Where(ev => Plugin.Map.UnitEventTypes.Contains(ev)).Distinct().ToArray();
filteredActions = Plugin.Map.ActionTypes.Where(ac => Plugin.Map.UnitActionTypes.Contains(ac)).Distinct().ToArray();
break;
case Building building:
isOnMap = building.IsPrebuilt;
items = Plugin.Map.FilterStructureTriggers().Select(t => t.Name).Distinct().ToArray();
filteredEvents = Plugin.Map.EventTypes.Where(ac => Plugin.Map.BuildingEventTypes.Contains(ac)).Distinct().ToArray();
filteredActions = Plugin.Map.ActionTypes.Where(ac => Plugin.Map.BuildingActionTypes.Contains(ac)).Distinct().ToArray();
break;
default:
items = Plugin.Map.Triggers.Select(t => t.Name).Distinct().ToArray();
filteredEvents = null;
filteredActions = null;
break;
}
HashSet<string> allowedTriggers = new HashSet<string>(items);
items = Trigger.None.Yield().Concat(Plugin.Map.Triggers.Select(t => t.Name).Where(t => allowedTriggers.Contains(t)).Distinct()).ToArray();
int selectIndex = selected == null ? 0 : Enumerable.Range(0, items.Length).FirstOrDefault(x => String.Equals(items[x], selected, StringComparison.OrdinalIgnoreCase));
triggerComboBox.DataSource = items;
triggerComboBox.Enabled = !isAircraft && isOnMap;
triggerToolTip = Map.MakeAllowedTriggersToolTip(filteredEvents, filteredActions);
if (obj != null)
{
triggerComboBox.DataBindings.Add("SelectedItem", obj, "Trigger");
}
triggerComboBox.SelectedItem = items[selectIndex];
}
private void Rebind()
{
houseComboBox.DataBindings.Clear();
strengthNud.DataBindings.Clear();
directionComboBox.DataBindings.Clear();
missionComboBox.DataBindings.Clear();
triggerComboBox.DataBindings.Clear();
basePriorityNud.DataBindings.Clear();
prebuiltCheckBox.DataBindings.Clear();
sellableCheckBox.DataBindings.Clear();
rebuildCheckBox.DataBindings.Clear();
if (obj == null)
{
return;
}
switch (obj)
{
case Infantry infantry:
{
houseComboBox.Enabled = true;
directionComboBox.DataSource = Plugin.Map.UnitDirectionTypes.Select(t => new TypeItem<DirectionType>(t.Name, t)).ToArray();
missionComboBox.DataBindings.Add("SelectedItem", obj, "Mission");
missionLabel.Visible = missionComboBox.Visible = true;
basePriorityLabel.Visible = basePriorityNud.Visible = false;
prebuiltCheckBox.Visible = false;
sellableCheckBox.Visible = false;
rebuildCheckBox.Visible = false;
}
break;
case Unit unit:
{
houseComboBox.Enabled = true;
directionComboBox.DataSource = Plugin.Map.UnitDirectionTypes.Select(t => new TypeItem<DirectionType>(t.Name, t)).ToArray();
missionComboBox.DataBindings.Add("SelectedItem", obj, "Mission");
missionLabel.Visible = missionComboBox.Visible = true;
basePriorityLabel.Visible = basePriorityNud.Visible = false;
prebuiltCheckBox.Visible = false;
sellableCheckBox.Visible = false;
rebuildCheckBox.Visible = false;
}
break;
case Building building:
{
houseComboBox.Enabled = building.IsPrebuilt;
bool directionVisible = (building.Type != null) && building.Type.HasTurret;
directionComboBox.DataSource = Plugin.Map.BuildingDirectionTypes.Select(t => new TypeItem<DirectionType>(t.Name, t)).ToArray();
directionLabel.Visible = directionVisible;
directionComboBox.Visible = directionVisible;
missionLabel.Visible = missionComboBox.Visible = false;
switch (Plugin.GameType)
{
case GameType.TiberianDawn:
{
basePriorityLabel.Visible = basePriorityNud.Visible = true;
prebuiltCheckBox.Visible = true;
prebuiltCheckBox.Enabled = building.BasePriority >= 0;
basePriorityNud.DataBindings.Add("Value", obj, "BasePriority");
prebuiltCheckBox.DataBindings.Add("Checked", obj, "IsPrebuilt");
sellableCheckBox.Visible = false;
rebuildCheckBox.Visible = false;
}
break;
case GameType.RedAlert:
{
basePriorityLabel.Visible = basePriorityNud.Visible = true;
prebuiltCheckBox.Visible = true;
prebuiltCheckBox.Enabled = building.BasePriority >= 0;
basePriorityNud.DataBindings.Add("Value", obj, "BasePriority");
prebuiltCheckBox.DataBindings.Add("Checked", obj, "IsPrebuilt");
sellableCheckBox.DataBindings.Add("Checked", obj, "Sellable");
rebuildCheckBox.DataBindings.Add("Checked", obj, "Rebuild");
sellableCheckBox.Visible = true;
rebuildCheckBox.Visible = true;
}
break;
case GameType.SoleSurvivor:
{
basePriorityLabel.Visible = basePriorityNud.Visible = false;
prebuiltCheckBox.Visible = false;
sellableCheckBox.Visible = false;
rebuildCheckBox.Visible = false;
}
break;
}
}
break;
}
// Collapse control to minimum required height.
this.Height = tableLayoutPanel1.PreferredSize.Height;
houseComboBox.DataBindings.Add("SelectedValue", obj, "House");
strengthNud.DataBindings.Add("Value", obj, "Strength");
directionComboBox.DataBindings.Add("SelectedValue", obj, "Direction");
UpdateDataSource();
}
private void Obj_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "Type":
{
Rebind();
}
break;
case "BasePriority":
{
if (obj is Building building)
{
prebuiltCheckBox.Enabled = building.BasePriority >= 0;
// Fix for the illegal state of it being set to not rebuild and not be prebuilt.
if (building.BasePriority < 0 && !building.IsPrebuilt)
{
building.IsPrebuilt = true;
}
AdjustToStructurePrebuiltStatus(building, GetHouseComboBox());
}
}
break;
case "IsPrebuilt":
{
if (obj is Building building)
{
if (!building.IsPrebuilt)
{
originalHouse = building.House;
originalTrigger = building.Trigger;
originalStrength = building.Strength;
}
AdjustToStructurePrebuiltStatus(building, GetHouseComboBox());
if (building.IsPrebuilt)
{
if (originalHouse != null) building.House = originalHouse;
if (originalTrigger != null) building.Trigger = originalTrigger;
if (originalStrength != 0) building.Strength = originalStrength;
}
}
}
break;
}
// The undo/redo system now handles plugin dirty state.
}
private PropertiesComboBox GetHouseComboBox()
{
return houseComboBox;
}
private void AdjustToStructurePrebuiltStatus(Building building, PropertiesComboBox houseComboBox)
{
if (building.BasePriority >= 0 && !building.IsPrebuilt)
{
HouseType house = Plugin.Map.GetBaseHouse(Plugin.GameType);
if (house.ID < 0)
{
// Fix for changing the combobox to one only contain "None".
houseComboBox.DataBindings.Clear();
houseComboBox.DataSource = house.Yield().Select(t => new TypeItem<HouseType>(t.Name, t)).ToArray();
houseComboBox.SelectedIndex = 0;
building.House = house;
houseComboBox.DataBindings.Add("SelectedValue", obj, "House");
}
else
{
var basePlayer = Plugin.Map.HouseTypes.Where(h => h.Equals(Plugin.Map.BasicSection.BasePlayer)).FirstOrDefault() ?? Plugin.Map.HouseTypes.First();
building.House = basePlayer;
}
}
else
{
// Fix for restoring "None" to a normal House. Only needed for TD.
HouseType selected = houseComboBox.SelectedValue as HouseType;
if (selected != null && selected.ID < 0)
{
houseComboBox.DataBindings.Clear();
TypeItem<HouseType>[] houses = Plugin.Map.Houses.Select(t => new TypeItem<HouseType>(t.Type.Name, t.Type)).ToArray();
houseComboBox.DataSource = houses;
HouseType restoredHouse = null;
if (Plugin.GameType == GameType.TiberianDawn)
{
String opposing = TiberianDawn.HouseTypes.GetClassicOpposingPlayer(Plugin.Map.BasicSection.Player);
restoredHouse = Plugin.Map.Houses.Where(h => h.Type.Equals(opposing)).FirstOrDefault()?.Type;
}
if (restoredHouse == null)
{
restoredHouse = houses.First().Type;
}
building.House = restoredHouse;
houseComboBox.DataBindings.Add("SelectedValue", obj, "House");
}
}
if (!building.IsPrebuilt)
{
building.Strength = 256;
building.Direction = Plugin.Map.BuildingDirectionTypes.Where(d => d.Equals(FacingType.North)).First();
building.Trigger = Trigger.None;
building.Sellable = false;
building.Rebuild = false;
}
if (directionComboBox.Visible)
{
directionComboBox.Enabled = building.IsPrebuilt;
}
houseComboBox.Enabled = building.IsPrebuilt;
strengthNud.Enabled = building.IsPrebuilt;
triggerComboBox.Enabled = building.IsPrebuilt;
if (sellableCheckBox.Visible)
{
sellableCheckBox.Enabled = building.IsPrebuilt;
}
if (rebuildCheckBox.Visible)
{
rebuildCheckBox.Enabled = building.IsPrebuilt;
}
}
private void comboBox_SelectedValueChanged(object sender, EventArgs e)
{
foreach (Binding binding in (sender as ComboBox).DataBindings)
{
binding.WriteValue();
}
}
private void nud_ValueChanged(object sender, EventArgs e)
{
foreach (Binding binding in (sender as NumericUpDown).DataBindings)
{
binding.WriteValue();
}
}
private void checkBox_CheckedChanged(object sender, EventArgs e)
{
foreach (Binding binding in (sender as CheckBox).DataBindings)
{
binding.WriteValue();
}
}
private void LblTriggerInfo_MouseEnter(Object sender, EventArgs e)
{
Control target = sender as Control;
string tooltip;
if (Object is Building bld && !bld.IsPrebuilt)
{
tooltip = "Triggers can only be linked to prebuilt structures.";
}
else if (Object is Unit un && un.Type.IsAircraft)
{
tooltip = "Triggers can not be linked to aircraft.";
}
else
{
tooltip = triggerToolTip;
}
ShowToolTip(target, tooltip);
}
private void ShowToolTip(Control target, string message)
{
if (target == null || message == null)
{
this.toolTip1.Hide(target);
return;
}
Point resPoint = target.PointToScreen(new Point(0, target.Height));
MethodInfo m = toolTip1.GetType().GetMethod("SetTool",
BindingFlags.Instance | BindingFlags.NonPublic);
// private void SetTool(IWin32Window win, string text, TipInfo.Type type, Point position)
m.Invoke(toolTip1, new object[] { target, message, 2, resPoint });
//this.toolTip1.Show(triggerToolTip, target, target.Width, 0, 10000);
}
private void LblTriggerInfo_MouseLeave(Object sender, EventArgs e)
{
Control target = sender as Control;
this.toolTip1.Hide(target);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
try
{
lblTriggerInfo.Image = null;
}
catch { /*ignore*/}
try
{
infoImage.Dispose();
}
catch { /*ignore*/}
infoImage = null;
components.Dispose();
}
base.Dispose(disposing);
}
}
public class ObjectPropertiesPopup : ToolStripDropDown
{
private readonly ToolStripControlHost host;
public ObjectProperties ObjectProperties { get; private set; }
public ObjectPropertiesPopup(IGamePlugin plugin, INotifyPropertyChanged obj)
{
ObjectProperties = new ObjectProperties();
ObjectProperties.Initialize(plugin, false);
ObjectProperties.Object = obj;
host = new ToolStripControlHost(ObjectProperties);
// Fix for the fact the popup got a different font and that made it an incorrect size.
this.Font = ObjectProperties.Font;
Padding = Margin = host.Padding = host.Margin = Padding.Empty;
MinimumSize = ObjectProperties.MinimumSize;
ObjectProperties.MinimumSize = ObjectProperties.Size;
MaximumSize = ObjectProperties.MaximumSize;
ObjectProperties.MaximumSize = ObjectProperties.Size;
Items.Add(host);
ObjectProperties.Size = ObjectProperties.PreferredSize;
Size = ObjectProperties.Size;
ObjectProperties.Disposed += (sender, e) =>
{
ObjectProperties = null;
Dispose(true);
};
}
protected override void OnClosed(ToolStripDropDownClosedEventArgs e)
{
base.OnClosed(e);
ObjectProperties.Object = null;
}
}
}