diff --git a/ParallelRoadTool/Extensions/LocaleManagerExtensions.cs b/ParallelRoadTool/Extensions/LocaleManagerExtensions.cs new file mode 100644 index 0000000..c7d0eaa --- /dev/null +++ b/ParallelRoadTool/Extensions/LocaleManagerExtensions.cs @@ -0,0 +1,101 @@ +using ColossalFramework.Globalization; +using ParallelRoadTool; +using ParallelRoadTool.Extensions.LocaleModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace ParallelRoadTool.Extensions +{ + /// + /// + /// + static class LocaleManagerExtensions + { + const string localeFieldName = "m_Locale"; + const string localizedStringsFieldName = "m_LocalizedStrings"; + const string localizedStringsCountFieldName = "m_LocalizedStringsCount"; + + static FieldInfo localeField = typeof(LocaleManager).GetField(localeFieldName, BindingFlags.Instance | BindingFlags.NonPublic); + static FieldInfo localizedStringsField = typeof(Locale).GetField(localizedStringsFieldName, BindingFlags.Instance | BindingFlags.NonPublic); + static FieldInfo localizedStringsCountField = typeof(Locale).GetField(localizedStringsCountFieldName, BindingFlags.Instance | BindingFlags.NonPublic); + + public static Locale GetLocale(this LocaleManager localeManager) + { + return (Locale)localeField.GetValue(localeManager); + } + + public static Dictionary GetLocalizedStrings(this Locale locale) + { + return (Dictionary)localizedStringsField.GetValue(locale); + } + + public static Dictionary GetLocalizedStringsCount(this Locale locale) + { + return (Dictionary)localizedStringsCountField.GetValue(locale); + } + + public static void RemoveRange(this LocaleManager localeManager, Locale.Key id) + { + Locale locale = localeManager.GetLocale(); + + // Set index to 0 so we can check for the string count + id.m_Index = 0; + + if (!locale.Exists(id)) + { + DebugUtils.Log($"Could not remove locale range {id}; localized string {id} does not exist!"); + return; + } + + Dictionary localizedStrings = locale.GetLocalizedStrings(); + Dictionary localizedStringsCount = locale.GetLocalizedStringsCount(); + + for (int index = 0, lastIndex = locale.CountUnchecked(id); index <= lastIndex; index++, id.m_Index = index) + { + localizedStrings.Remove(id); + localizedStringsCount.Remove(id); + } + + DebugUtils.Log($"Removed locale range {id.m_Identifier}[{id.m_Key}]."); + } + + public static void AddString(this LocaleManager localeManager, LocalizedString localizedString) + { + Locale locale = localeManager.GetLocale(); + + // Construct 0-index id for the localized string from argument + Locale.Key id; + id.m_Identifier = localizedString.Identifier; + id.m_Key = localizedString.Key; + id.m_Index = 0; + + // Check if the id already exists; if so find next index + if (locale.Exists(id)) + { + // Log message lags game on large namelists + // Log($"Localized string {localizedString.Identifier}[{localizedString.Key}] already exists, adding it with next available index."); + id.m_Index = locale.CountUnchecked(id); + } + + // Add the localized string + locale.AddLocalizedString(id, localizedString.Value); + + // Set the string counts accordingly + Dictionary localizedStringCounts = locale.GetLocalizedStringsCount(); + + // The count at the exact index appears to always be 0 + localizedStringCounts[id] = 0; + + // index = 0 appears to be a special case and indicates the count of localized strings with the same identifier and key + Locale.Key zeroIndexID = id; + zeroIndexID.m_Index = 0; + localizedStringCounts[zeroIndexID] = id.m_Index + 1; + + // Log message lags game on large namelists + // Log($"Added localized string {id} = '{localizedString.Value}', count = {localizedStringCounts[zeroIndexID]}."); + } + } +} \ No newline at end of file diff --git a/ParallelRoadTool/Extensions/LocaleModels/LocalizedString.cs b/ParallelRoadTool/Extensions/LocaleModels/LocalizedString.cs new file mode 100644 index 0000000..38a1409 --- /dev/null +++ b/ParallelRoadTool/Extensions/LocaleModels/LocalizedString.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Serialization; + +namespace ParallelRoadTool.Extensions.LocaleModels +{ + /// + /// + /// + public struct LocalizedString + { + [XmlAttribute(AttributeName = "identifier")] + public string Identifier; + [XmlAttribute(AttributeName = "key")] + public string Key; + [XmlText] + public string Value; + } +} diff --git a/ParallelRoadTool/Extensions/LocaleModels/LocalizedStringKey.cs b/ParallelRoadTool/Extensions/LocaleModels/LocalizedStringKey.cs new file mode 100644 index 0000000..f2acd78 --- /dev/null +++ b/ParallelRoadTool/Extensions/LocaleModels/LocalizedStringKey.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Serialization; + +namespace ParallelRoadTool.Extensions.LocaleModels +{ + /// + /// + /// + public struct LocalizedStringKey + { + [XmlAttribute(AttributeName = "identifier")] + public string Identifier; + [XmlAttribute(AttributeName = "key")] + public string Key; + } +} diff --git a/ParallelRoadTool/Extensions/LocaleModels/NameList.cs b/ParallelRoadTool/Extensions/LocaleModels/NameList.cs new file mode 100644 index 0000000..c88830f --- /dev/null +++ b/ParallelRoadTool/Extensions/LocaleModels/NameList.cs @@ -0,0 +1,32 @@ +using ColossalFramework; +using ColossalFramework.Globalization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Serialization; + +namespace ParallelRoadTool.Extensions.LocaleModels +{ + /// + /// + /// + public class NameList + { + [XmlAttribute(AttributeName = "name")] + public string Name; + [XmlArray(ElementName = "strings")] + public LocalizedString[] LocalizedStrings; + + public void Apply() + { + LocaleManager localeManager = SingletonLite.instance; + foreach (LocalizedString localizedString in LocalizedStrings) + { + localeManager.AddString(localizedString); + } + + DebugUtils.Log($"Namelist {Name} applied."); + } + } +} diff --git a/ParallelRoadTool/Localization/en.xml b/ParallelRoadTool/Localization/en.xml new file mode 100644 index 0000000..79ea929 --- /dev/null +++ b/ParallelRoadTool/Localization/en.xml @@ -0,0 +1,19 @@ + + + + Toggle snapping for existing nodes + Toggle Parallel Road Tool + Toggle reverse direction for this road + Remove network + Add network + + Same as selected + Decrease horizontal offset of parallel networks + Increase horizontal offset of parallel networks + Decrease vertical offset of parallel networks + Increase vertical offset of parallel networks + + \ No newline at end of file diff --git a/ParallelRoadTool/OptionsKeymapping.cs b/ParallelRoadTool/OptionsKeymapping.cs index adbb9e4..1d25b84 100644 --- a/ParallelRoadTool/OptionsKeymapping.cs +++ b/ParallelRoadTool/OptionsKeymapping.cs @@ -27,9 +27,9 @@ public class OptionsKeymapping : UICustomControl private void Awake() { - AddKeymapping("Toggle Parallel Roads", toggleParallelRoads); - AddKeymapping("Decrease HorizontalOffset of Parallel Roads", decreaseOffset); - AddKeymapping("Increase HorizontalOffset of Parallel Roads", increaseOffset); + AddKeymapping(Locale.Get("PRT_TOOLTIPS", "ToolToggleButton"), toggleParallelRoads); + AddKeymapping(Locale.Get("PRT_TEXTS", "DecreaseHorizontalOffsetOption"), decreaseOffset); + AddKeymapping(Locale.Get("PRT_TEXTS", "IncreaseHorizontalOffsetOption"), increaseOffset); } private void AddKeymapping(string label, SavedInputKey savedInputKey) diff --git a/ParallelRoadTool/ParallelRoadTool.cs b/ParallelRoadTool/ParallelRoadTool.cs index 4780307..34a087e 100644 --- a/ParallelRoadTool/ParallelRoadTool.cs +++ b/ParallelRoadTool/ParallelRoadTool.cs @@ -1,9 +1,17 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml; +using System.Xml.Serialization; using ColossalFramework; +using ColossalFramework.Globalization; +using ColossalFramework.Plugins; using ColossalFramework.UI; using ICities; using ParallelRoadTool.Detours; +using ParallelRoadTool.Extensions.LocaleModels; using ParallelRoadTool.UI; using UnityEngine; @@ -221,6 +229,54 @@ public override void OnCreated(ILoading loading) { ParallelRoadTool.Instance = new GameObject("ParallelRoadTool").AddComponent(); }*/ + + // Add post locale change event handlers + LocaleManager.eventLocaleChanged += OnLocaleChanged; + + DebugUtils.Log("Added locale change event handlers."); + + // Reload the current locale once to effect changes + LocaleManager.ForceReload(); + } + + public override void OnReleased() + { + // Remove post locale change event handlers + LocaleManager.eventLocaleChanged -= OnLocaleChanged; + + DebugUtils.Log("Removed locale change event handlers."); + + // Reload the current locale once to effect changes + LocaleManager.ForceReload(); + } + + private void OnLocaleChanged() + { + DebugUtils.Log("Locale changed callback started."); + + XmlSerializer serializer = new XmlSerializer(typeof(NameList)); + + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = $"ParallelRoadTool.Localization.{LocaleManager.cultureInfo.TwoLetterISOLanguageName}.xml"; + + DebugUtils.Log($"Trying to read {resourceName} localization file..."); + + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + using (StreamReader reader = new StreamReader(stream)) + { + using (XmlReader xmlStream = XmlReader.Create(reader)) + { + if (serializer.CanDeserialize(xmlStream)) + { + NameList nameList = (NameList)serializer.Deserialize(xmlStream); + nameList.Apply(); + } + } + } + + DebugUtils.Log($"Namelists {resourceName} applied."); + + DebugUtils.Log("Locale changed callback finished."); } public override void OnLevelLoaded(LoadMode mode) diff --git a/ParallelRoadTool/ParallelRoadTool.csproj b/ParallelRoadTool/ParallelRoadTool.csproj index 2fd8062..0a24449 100644 --- a/ParallelRoadTool/ParallelRoadTool.csproj +++ b/ParallelRoadTool/ParallelRoadTool.csproj @@ -54,6 +54,10 @@ + + + + @@ -69,7 +73,8 @@ - + + @@ -93,6 +98,7 @@ + @@ -115,6 +121,7 @@ + mkdir "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)" diff --git a/ParallelRoadTool/UI/UIMainWindow.cs b/ParallelRoadTool/UI/UIMainWindow.cs index 9ec14c4..9d0c311 100644 --- a/ParallelRoadTool/UI/UIMainWindow.cs +++ b/ParallelRoadTool/UI/UIMainWindow.cs @@ -1,5 +1,6 @@ using System; using ColossalFramework; +using ColossalFramework.Globalization; using ColossalFramework.UI; using ParallelRoadTool.UI.Base; using UnityEngine; @@ -22,10 +23,11 @@ public class UIMainWindow : UIPanel private UIOptionsPanel _mainPanel; private UINetList _netList; - private NetInfo _netToolSelection; + private NetInfo _netToolSelection; + private UIRightDragHandle _buttonDragHandle; + private UICheckBox _toolToggleButton; private UICheckBox _snappingToggleButton; - private UIRightDragHandle _buttonDragHandle; // We use this to prevent clicks while user is dragging the button private bool _isDragging; @@ -39,7 +41,6 @@ public class UIMainWindow : UIPanel private void UnsubscribeToUIEvents() { _toolToggleButton.eventCheckChanged -= ToolToggleButtonOnEventCheckChanged; - _mainPanel.OnToolToggled -= ToolToggleButtonOnEventCheckChanged; _snappingToggleButton.eventCheckChanged -= SnappingToggleButtonOnEventCheckChanged; _buttonDragHandle.eventDragStart -= ButtonDragHandleOnEventDragStart; _buttonDragHandle.eventDragEnd -= ButtonDragHandleOnEventDragEnd; @@ -48,7 +49,6 @@ private void UnsubscribeToUIEvents() private void SubscribeToUIEvents() { _toolToggleButton.eventCheckChanged += ToolToggleButtonOnEventCheckChanged; - _mainPanel.OnToolToggled += ToolToggleButtonOnEventCheckChanged; _snappingToggleButton.eventCheckChanged += SnappingToggleButtonOnEventCheckChanged; _buttonDragHandle.eventDragStart += ButtonDragHandleOnEventDragStart; _buttonDragHandle.eventDragEnd += ButtonDragHandleOnEventDragEnd; @@ -151,7 +151,7 @@ public override void Start() space.size = new Vector2(1, 1); // Add options - _snappingToggleButton = UIUtil.CreateCheckBox(_mainPanel, "Snapping", "Toggle snapping for all nodes", false); + _snappingToggleButton = UIUtil.CreateCheckBox(_mainPanel, "Snapping", Locale.Get("PRT_TOOLTIPS", "SnappingToggleButton"), false); _snappingToggleButton.relativePosition = new Vector3(166, 38); _snappingToggleButton.BringToFront(); @@ -168,7 +168,7 @@ public override void Start() if (button != null) Destroy(button); - _toolToggleButton = UIUtil.CreateCheckBox(tsBar, "Parallel", "Parallel Road Tool", false); + _toolToggleButton = UIUtil.CreateCheckBox(tsBar, "Parallel", Locale.Get("PRT_TOOLTIPS", "ToolToggleButton"), false); if (SavedToggleX.value != -1000 && SavedToggleY.value != -1000) { _toolToggleButton.absolutePosition = new Vector3(SavedToggleX.value, SavedToggleY.value); diff --git a/ParallelRoadTool/UI/UINetTypeItem.cs b/ParallelRoadTool/UI/UINetTypeItem.cs index 391a217..1259234 100644 --- a/ParallelRoadTool/UI/UINetTypeItem.cs +++ b/ParallelRoadTool/UI/UINetTypeItem.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using ColossalFramework.Globalization; using ColossalFramework.UI; using ParallelRoadTool.UI.Base; using UnityEngine; @@ -52,7 +53,7 @@ public override void Start() DropDown.relativePosition = Vector2.zero; DropDown.eventSelectedIndexChanged += DropDown_eventSelectedIndexChanged; - ReverseCheckbox = UIUtil.CreateCheckBox(this, "Reverse", "Toggle reverse road", false); + ReverseCheckbox = UIUtil.CreateCheckBox(this, "Reverse", Locale.Get("PRT_TOOLTIPS", "ReverseToggleButton"), false); ReverseCheckbox.relativePosition = new Vector3(LabelWidth + ColumnPadding, 2); ReverseCheckbox.eventCheckChanged += ReverseCheckboxOnEventCheckChanged; @@ -76,7 +77,7 @@ public override void Start() Label.relativePosition = new Vector3(10, 12); Label.isVisible = false; - DeleteButton = UIUtil.CreateUiButton(this, string.Empty, "Remove network", new Vector2(36, 36), "Remove"); + DeleteButton = UIUtil.CreateUiButton(this, string.Empty, Locale.Get("PRT_TOOLTIPS", "RemoveNetworkButton"), new Vector2(36, 36), "Remove"); DeleteButton.zOrder = 0; DeleteButton.textScale = 0.8f; DeleteButton.relativePosition = @@ -84,7 +85,7 @@ public override void Start() DeleteButton.eventClicked += DeleteButton_eventClicked; - AddButton = UIUtil.CreateUiButton(this, string.Empty, "Add network", new Vector2(36, 36), "Add"); + AddButton = UIUtil.CreateUiButton(this, string.Empty, Locale.Get("PRT_TOOLTIPS", "AddNetworkButton"), new Vector2(36, 36), "Add"); AddButton.zOrder = 1; AddButton.isVisible = false; AddButton.textScale = 0.8f; @@ -132,7 +133,7 @@ public void RenderItem() DropDown.isVisible = false; Label.isVisible = true; AddButton.isVisible = true; - Label.text = $"Same as selected"; + Label.text = Locale.Get("PRT_TEXTS", "SameAsSelectedLabel"); } private void DropDown_eventSelectedIndexChanged(UIComponent component, int index) diff --git a/ParallelRoadTool/UI/UIOptionsPanel.cs b/ParallelRoadTool/UI/UIOptionsPanel.cs index 5f8b707..c0d918f 100644 --- a/ParallelRoadTool/UI/UIOptionsPanel.cs +++ b/ParallelRoadTool/UI/UIOptionsPanel.cs @@ -5,12 +5,6 @@ namespace ParallelRoadTool.UI { public class UIOptionsPanel : UIPanel { - #region Events - - public event PropertyChangedEventHandler OnToolToggled; - - #endregion - public override void Start() { name = "PRT_OptionsPanel"; @@ -25,7 +19,6 @@ public override void Start() autoLayout = true; autoSize = false; - DebugUtils.Log($"UIOptionsPanel created {size} | {position}"); } } diff --git a/ParallelRoadTool/UI/UITipsWindow.cs b/ParallelRoadTool/UI/UITipsWindow.cs new file mode 100644 index 0000000..c6c2a61 --- /dev/null +++ b/ParallelRoadTool/UI/UITipsWindow.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +using ColossalFramework.UI; +using ParallelRoadTool.UI.Base; + +namespace ParallelRoadTool.UI +{ + public class UITipsWindow : UILabel + { + public static UITipsWindow instance; + + private string[] m_tips = + { + "New in 2.1.0: It is now possible to Export a selection into a file and Import it later in game or in the editor", + "Tip: Hold Alt to deselect objects using the marquee selection", + "Tip: A building with an orange highlight will despawn when the simulation is running", + "Tip: While cloning, Right Click to rotate 45° clockwise", + "Tip: Hold Ctrl while rotating to snap to 45° angles", + "Tip: Hold Shift to select multiple objects to move at once", + "Tip: Use Left Click to drag objects around", + "Tip: Hold Alt while dragging objects to reverse the Snapping option", + "Tip: While holding Right Click, move the mouse left and right to rotate objects", + "Tip: Use Alt for finer movements with the keyboard", + "Tip: Use Shift for faster movements with the keyboard", + "Tip: When Follow Terrain is disabled, objects will keep their height when moved", + "Tip: Right Click to clear the selection", + "Tip: Buildings, Trees, Props and Nodes can all be moved", + "Tip: Movable objects are highlighted when hovered", + "Tip: Hover various things to discover what can be moved", + "Tip: Look for the tiny green circle\nThat's the center of rotation", + "Tip: Disable tips in the mod options\nEsc > Options > Move It! > Hide tips" + }; + private int m_currentTip = -1; + + public override void Start() + { + atlas = UIUtil.GetAtlas("Ingame"); + backgroundSprite = "GenericPanelWhite"; + + size = new Vector2(300, 100); + padding = new RectOffset(10, 10, 10, 10); + textColor = new Color32(109, 109, 109, 255); + textScale = 0.9f; + + wordWrap = true; + autoHeight = true; + + instance = this; + + NextTip(); + } + + protected override void OnMouseEnter(UIMouseEventParameter p) + { + textColor = new Color32(0, 0, 0, 255); + } + + protected override void OnMouseLeave(UIMouseEventParameter p) + { + textColor = new Color32(109, 109, 109, 255); + } + + public void NextTip() + { + m_currentTip = (m_currentTip + 1) % m_tips.Length; + text = m_tips[m_currentTip]; + } + + protected override void OnClick(UIMouseEventParameter p) + { + NextTip(); + } + + protected override void OnVisibilityChanged() + { + if (isVisible) + { + RefreshPosition(); + } + base.OnVisibilityChanged(); + } + + protected override void OnSizeChanged() + { + RefreshPosition(); + } + + public void RefreshPosition() + { + float x = GetUIView().GetScreenResolution().x - width - 10f; + float y; + + //if (UIToolOptionPanel.instance != null && MoveItTool.marqueeSelection) + //{ + // y = UIToolOptionPanel.instance.filtersPanel.absolutePosition.y - height - 10f; + //} + //else + //{ + UIComponent thumbnailBar = GetUIView().FindUIComponent("ThumbnailBar"); + if (thumbnailBar != null) + { + y = thumbnailBar.absolutePosition.y - height - 30f; + } + else + { + y = Screen.height - height - 200; + } + //} + + absolutePosition = new Vector3(x, y); + } + } +}