diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f47f621bce..89d4155e9a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -213,7 +213,23 @@ Setting Up Your Environment * Click on the resulting link to go to that class's documentation page. * The Unity documentation for that class will start with some faint grey text at the top of the page that says, "Implemented in:", which tells you which DLL you need to reference to get code using that class to compile properly. - 3. If you do not have a copy of KSP locally, you may + 3. Make sure your installation of KSP has LinuxGuruGamer's ClivkThroughBlocker + mod installed. kOS now needs it in order to compile. After it is + installed, copy its DLL file into Resources/ from your GameData folder. + + Copy This file: + + * $KSP/GameData/00_ClickThroughBlocker/Plugins/ClickThroughBlocker.dll + + To here: + + * Resources/ + + Then *make a Reference to Resoruces/ClickThroughBlocker.dll in your kOS project + file*. This is needed for it to let you compile parts of kOS that use classes + from ClickThroughBlocker. + + 4. If you do not have a copy of KSP locally, you may download dummy assemblies at https://github.com/KSP-KOS/KSP_LIB 3. Make sure you are targeting this version of .Net: ".Net 4.0 Framework". diff --git a/src/kOS/Module/kOSCustomParameters.cs b/src/kOS/Module/kOSCustomParameters.cs index 65d270b222..32bccdfbb6 100644 --- a/src/kOS/Module/kOSCustomParameters.cs +++ b/src/kOS/Module/kOSCustomParameters.cs @@ -1,6 +1,7 @@ using KSP.IO; using System; using System.Reflection; +using System.Linq; namespace kOS.Module { @@ -46,6 +47,9 @@ public static kOSCustomParameters Instance [GameParameters.CustomIntParameterUI("")] public int version = 0; + [GameParameters.CustomParameterUI("")] + public bool passedClickThroughCheck = false; + // these values constrain and back the InstructionsPerUpdate property so that it is clamped both in the // user interface and when set from within a script. private const int ipuMin = 50; @@ -202,6 +206,95 @@ public void CheckMigrateSettings() } } + public void CheckClickThroughBlockerExists() + { + if (passedClickThroughCheck) + return; + bool clickThroughExists = false; + + var loadedCTBAssembly = AssemblyLoader.loadedAssemblies.FirstOrDefault(a => a.dllName.Equals("ClickThroughBlocker")); + if (loadedCTBAssembly != null) + { + // Must be at least version 0.10 of ClickThroughBlocker: + if (loadedCTBAssembly.versionMajor > 0 || loadedCTBAssembly.versionMinor >= 10) + { + Type ctbType = loadedCTBAssembly.assembly.GetType("ClickThroughFix.CTB", false); + if (ctbType != null) + { + if (ctbType.GetField("focusFollowsclick") != null) + { + clickThroughExists = true; + } + } + } + } + + string popupText = + "=======================================\n" + + "kOS is Checking for ClickThroughBlocker\n" + + "=======================================\n\n" + + "Starting with kOS v1.3, kOS has become dependent on the existence of the ClickThroughBlocker mod. " + + "(And it must be at least version 0.10 of ClickThroughBlocker.)\n\n"; + + if (clickThroughExists) + { + popupText += + " <<<< CHECK SUCCEEDED >>>>>\n\n" + + "kOS has found ClickThroughBlocker installed, and it appears to be a version that will work with kOS.\n" + + "\n" + + "Please note that while in the past the kOS terminal has always been click-to-focus, from now " + + "on it will behave however ClickThroughBlocker is set to act, which may be focus-follows-mouse.\n" + + "You can use ClickThroughBlocker's settings to change this behvior like this:\n\n" + + "[Hit Escape] Settings ->\n" + + " Difficulty Options ->\n" + + " ClickThroughBlocker ->\n" + + " [x] Focus Follows Click\n\n"; + } + else + { + popupText += + " !!! CHECK FAILED !!!\n\n" + + "kOS couldn't find a version of ClickThroughBlocker that works with kOS. This could be " + + "because ClickThroughBlocker is not installed at all, or it could be because its version is too old " + + "(or too new, if ClickThroughBlocker ever renames some things that kOS is using).\n" + + "\n\n" + + "To use kOS v1.3 or higher you'll need to quit Kerbal Space Program and install a version of ClickThroughBlocker that it supports.\n"; + } + + string buttonText; + global::Callback clickThroughAck; + if (clickThroughExists) + { + clickThroughAck = AcceptClickThrough; + buttonText = "Acknowledged."; + } + else + { + clickThroughAck = FailedClickThrough; + buttonText = "Acknowledged. I'll have to quit and change my mods."; + } + + kOSSettingsChecker.QueueDialog( + 0.75f, 0.6f, + new MultiOptionDialog( + "ClickThroughBlockerCheck", + popupText, + "kOS ClickThroughBlocker Check", + HighLogic.UISkin, + new DialogGUIButton(buttonText, clickThroughAck, true) + )); + } + + public void AcceptClickThrough() + { + passedClickThroughCheck = true; + } + + public void FailedClickThrough() + { + passedClickThroughCheck = false; + } + public void MigrateSettingsNormal() { MigrateSettings(false); diff --git a/src/kOS/Module/kOSSettingsChecker.cs b/src/kOS/Module/kOSSettingsChecker.cs index 89acb97f43..3e01f6be24 100644 --- a/src/kOS/Module/kOSSettingsChecker.cs +++ b/src/kOS/Module/kOSSettingsChecker.cs @@ -22,6 +22,7 @@ private void CheckSettings() SafeHouse.Logger.SuperVerbose("kOSSettingsChecker.CheckSettings()"); HighLogic.CurrentGame.Parameters.CustomParams().CheckMigrateSettings(); HighLogic.CurrentGame.Parameters.CustomParams().CheckNewManagers(); + HighLogic.CurrentGame.Parameters.CustomParams().CheckClickThroughBlockerExists(); } // Because rapidly showing dialogs can prevent some from being shown, we can just queue up diff --git a/src/kOS/Properties/AssemblyInfo.cs b/src/kOS/Properties/AssemblyInfo.cs index 52fa1b3b68..f9de84228d 100644 --- a/src/kOS/Properties/AssemblyInfo.cs +++ b/src/kOS/Properties/AssemblyInfo.cs @@ -34,3 +34,8 @@ [assembly: AssemblyFileVersion("1.2.1.0")] [assembly: AssemblyVersion("1.2.1.0")] [assembly: KSPAssembly("kOS", 1, 7)] + +// No longer a hard dependancy, because we want to tell the user why kOS isn't working +// if ClickThroughBlocker is not there, rather than just have it silently refuse to +// load kOS with no explanation as would happen if this line was enabled: +// [assembly: KSPAssemblyDependency("ClickThroughBlocker", 1, 0)] \ No newline at end of file diff --git a/src/kOS/Screen/DelegateDialog.cs b/src/kOS/Screen/DelegateDialog.cs index 8836ac6e64..d8cce5fcf7 100644 --- a/src/kOS/Screen/DelegateDialog.cs +++ b/src/kOS/Screen/DelegateDialog.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using ClickThroughFix; // Needs ClickThroughBlocker DLL to be in the Reference directory. using UnityEngine; namespace kOS.Screen @@ -38,7 +39,7 @@ public void OnGUI() if (invoked) { float guessWidth = GUI.skin.label.CalcSize( new GUIContent(message) ).x; - GUILayout.Window( parent.GetUniqueId()+1, new Rect( parent.GetRect().xMin+200, + ClickThruBlocker.GUILayoutWindow( parent.GetUniqueId()+1, new Rect( parent.GetRect().xMin+200, parent.GetRect().yMin+10, guessWidth, 0) , DrawConfirm, "Confirm", GUILayout.ExpandWidth(true) ); diff --git a/src/kOS/Screen/GUIWindow.cs b/src/kOS/Screen/GUIWindow.cs index a5f6e30e8c..2d5e54ad9b 100644 --- a/src/kOS/Screen/GUIWindow.cs +++ b/src/kOS/Screen/GUIWindow.cs @@ -14,6 +14,7 @@ using kOS.Utilities; using kOS.Communication; using kOS.Safe; +using ClickThroughFix; // Needs ClickThroughBlocker DLL to be in the Reference directory. namespace kOS.Screen { @@ -116,7 +117,7 @@ void OnGUI() GUI.skin = HighLogic.Skin; - WindowRect = GUILayout.Window(UniqueId, WindowRect, WidgetGui, TitleText, style); + WindowRect = ClickThruBlocker.GUILayoutWindow(UniqueId, WindowRect, WidgetGui, TitleText, style); if (currentPopup != null) { var r = RectExtensions.EnsureCompletelyVisible(currentPopup.popupRect); @@ -124,7 +125,7 @@ void OnGUI() currentPopup.PopDown(); } else { GUI.BringWindowToFront(UniqueId + 1); - currentPopup.popupRect = GUILayout.Window(UniqueId + 1, r, PopupGui, "", style); + currentPopup.popupRect = ClickThruBlocker.GUILayoutWindow(UniqueId + 1, r, PopupGui, "", style); } } } diff --git a/src/kOS/Screen/KOSManagedWindow.cs b/src/kOS/Screen/KOSManagedWindow.cs index 5759c8b67e..95e2496bf5 100644 --- a/src/kOS/Screen/KOSManagedWindow.cs +++ b/src/kOS/Screen/KOSManagedWindow.cs @@ -1,14 +1,23 @@ using System.Collections.Generic; using UnityEngine; +using ClickThroughFix; // Needs ClickThroughBlocker DLL to be in the Reference directory. namespace kOS.Screen { /// /// kOSManagedWindow is for any Unity Monobehavior that you'd like to - /// have contain a GUI.Window, and you need kOS to keep track of the + /// have contain an IMGUI Window, and you need kOS to keep track of the /// window stacking for which click is on top of which other click. /// Unity's built in systems for this don't work well at all, so /// we had to make up our own. + /// + /// Issue #2697 - In addition to KOS's own system to handle this, + /// This also is now layered on top of Linuxgurugamer's ClickThroughBlocker + /// mod, so it uses its wrappers around GUI.Window. That was needed because + /// if SOME mods use ClickThruBlocker windows, then those windows will get first + /// dibs on events before kOS gets to, making kOS helpless to intercept events it's + /// trying to protect other windows from seeing. ClickThruBlocker is a mod that + /// once some mods use it, then all the other mods have to as well. /// public abstract class KOSManagedWindow : MonoBehaviour { @@ -222,14 +231,13 @@ public bool IsOpen } /// - /// Pass in the absolute GUI screen location of a mouse click to decide whether - /// or not this widget gets keyboard focus because of that click. - /// (Clicking outside the window takes focus away. Clicking inside - /// the window gives focus to the window and brings it to the front.) - /// + /// Pass in the absolute GUI screen location of the mouse to decide whether + /// or not this window gets keyboard focus because of that position. + /// (If you want focus-follows-mouse logic, call this every Update(). If you + /// want click-to-focus logic, only call this in Update()s where a click just happened.) /// Absolute position of mouse on whole screen /// True if the window got focused, false if it didn't. - public bool FocusClickLocationCheck(Vector2 absMousePos) + public bool FocusMouseLocationCheck(Vector2 absMousePos) { bool wasInside = false; if (IsInsideMyExposedPortion(absMousePos)) @@ -286,18 +294,17 @@ public bool IsInsideMyExposedPortion(Vector2 posAbsolute) /// /// When you subclass KOSManagedWindow, make sure that you call this - /// from inside your Update. It does not use OnGUI because of the fact - /// that the OnGUI event handler is broken - it only sends MouseDown - /// and MouseUp events when the mouse is OUTSIDE the window, which is - /// utterly backward, and it's hard to work out how to fix this, - /// given how badly documented the Unity GUI API is. If anyone who - /// actually understands the Unity GUI system wants to fix this, - /// please feel free to do so. + /// from inside your Update() to check for focus change on the window. + /// Calling this will maybe call GetFocus() or LoseFocus() depending on + /// what the mouse is doing. + /// Note, you call this during *Update()*, NOT the OnGUI() call. + /// It does not use OnGUI() because the raw mousebutton state it + /// needs to see can get consumed and wiped by Unity's IMGUI widgets + /// before application code like this can see it. /// - /// True if there was a mouseclick within this window. - public bool UpdateLogic() + public void UpdateLogic() { - if (!IsOpen) return false; + if (!IsOpen) return; // Input.mousePosition, unlike Event.current.MousePosition, puts the origin at the // lower-left instead of upper-left of the screen, thus the subtraction in the y coord below: @@ -305,23 +312,37 @@ public bool UpdateLogic() // Mouse coord within the window, rather than within the screen. mousePosRelative = new Vector2( mousePosAbsolute.x - windowRect.xMin, mousePosAbsolute.y - windowRect.yMin); - - bool clickUp = false; + + // Could maybe cache the CustomParams call once up front to get a reference to the CTB instance, then only + // repeat the ".focusFollowsclick" part each update. The reason that's not being done here is that I + // noticed ClickThroughBlocker's OWN code always does it like this, and for all I know there might be + // an important reason. It always gets this value by using the fully qualified long chain you see + // here, starting from HighLogic, each update. : + bool clickToFocus = HighLogic.CurrentGame.Parameters.CustomParams().focusFollowsclick; + if (Input.GetMouseButtonDown(0)) { mouseButtonDownPosAbsolute = mousePosAbsolute; mouseButtonDownPosRelative = mousePosRelative; } - - if (Input.GetMouseButtonUp(0)) + + bool mousePositionCanSetFocus = !(clickToFocus); // Always true in focus-follows-mouse mode + + if (clickToFocus) { - clickUp = true; - if (Vector2.Distance(mousePosAbsolute,mouseButtonDownPosAbsolute) <= dragTolerance) + if (Input.GetMouseButtonUp(0)) { - FocusClickLocationCheck(mousePosAbsolute); + if (Vector2.Distance(mousePosAbsolute, mouseButtonDownPosAbsolute) <= dragTolerance) + { + mousePositionCanSetFocus = true; + } } } - return IsInsideMyExposedPortion(mousePosAbsolute) && clickUp; + + if (mousePositionCanSetFocus) + { + FocusMouseLocationCheck(mousePosAbsolute); + } } } } diff --git a/src/kOS/Screen/KOSNameTagWindow.cs b/src/kOS/Screen/KOSNameTagWindow.cs index 2a6b462c27..4dc414b5da 100644 --- a/src/kOS/Screen/KOSNameTagWindow.cs +++ b/src/kOS/Screen/KOSNameTagWindow.cs @@ -1,6 +1,7 @@ -using kOS.Utilities; +using kOS.Utilities; using UnityEngine; using kOS.Module; +using ClickThroughFix; // Needs ClickThroughBlocker DLL to be in the Reference directory. using System; namespace kOS.Screen @@ -124,7 +125,7 @@ public void OnGUI() EditorLogic.fetch.Lock(false, false, false, "KOSNameTagLock"); GUI.skin = HighLogic.Skin; - GUILayout.Window(myWindowId, windowRect, DrawWindow,"KOS nametag"); + ClickThruBlocker.GUILayoutWindow(myWindowId, windowRect, DrawWindow,"KOS nametag"); // Ensure that the first time the window is made, it gets keybaord focus, // but allow the focus to leave the window after that: diff --git a/src/kOS/Screen/KOSTextEditPopup.cs b/src/kOS/Screen/KOSTextEditPopup.cs index c5dfd823e6..f351a94c02 100644 --- a/src/kOS/Screen/KOSTextEditPopup.cs +++ b/src/kOS/Screen/KOSTextEditPopup.cs @@ -6,6 +6,7 @@ using kOS.Safe.Exceptions; using kOS.Safe.Utilities; using kOS.Module; +using ClickThroughFix; // Needs ClickThroughBlocker DLL to be in the Reference directory. namespace kOS.Screen { @@ -170,7 +171,7 @@ public void OnGUI() CalcOuterCoords(); // force windowRect to lock to bottom edge of the parents CalcInnerCoords(); - WindowRect = GUI.Window(UniqueId, WindowRect, ProcessWindow, ""); + WindowRect = ClickThruBlocker.GUIWindow(UniqueId, WindowRect, ProcessWindow, ""); // Some mouse global state data used by several of the checks: if (consumeEvent) diff --git a/src/kOS/Screen/KOSToolbarWindow.cs b/src/kOS/Screen/KOSToolbarWindow.cs index ce18c7f4cc..e5e461db9c 100644 --- a/src/kOS/Screen/KOSToolbarWindow.cs +++ b/src/kOS/Screen/KOSToolbarWindow.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; +using ClickThroughFix; // Needs ClickThroughBlocker DLL to be in the Reference directory. + namespace kOS.Screen { @@ -443,7 +445,7 @@ public void OnGUI() GUI.skin = HighLogic.Skin; - windowRect = GUILayout.Window(UNIQUE_ID, windowRect, DrawWindow, "kOS " + versionString); + windowRect = ClickThruBlocker.GUILayoutWindow(UNIQUE_ID, windowRect, DrawWindow, "kOS " + versionString); windowRect = RectExtensions.ClampToRectAngle(windowRect, rectToFit); } diff --git a/src/kOS/Screen/ListPickerDialog.cs b/src/kOS/Screen/ListPickerDialog.cs index 483c3d3c18..c902c13740 100644 --- a/src/kOS/Screen/ListPickerDialog.cs +++ b/src/kOS/Screen/ListPickerDialog.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using UnityEngine; +using ClickThroughFix; // Needs ClickThroughBlocker DLL to be in the Reference directory. + namespace kOS.Screen { @@ -110,7 +112,7 @@ public void OnGUI() // Make sure it shifts enough to the left to fit the biggest string: outerWindowRect.x = Mathf.Min(outerWindowRect.x, UnityEngine.Screen.width - outerWindowRect.width - 60); - outerWindowRect = GUILayout.Window( + outerWindowRect = ClickThruBlocker.GUILayoutWindow( title.GetHashCode(), outerWindowRect, DrawInnards, diff --git a/src/kOS/Screen/TermWindow.cs b/src/kOS/Screen/TermWindow.cs index b9eba082f7..a15bb85202 100644 --- a/src/kOS/Screen/TermWindow.cs +++ b/src/kOS/Screen/TermWindow.cs @@ -10,6 +10,7 @@ using kOS.Safe.UserIO; using KSP.UI.Dialogs; using kOS.Safe.Utilities; +using ClickThroughFix; // Needs ClickThroughBlocker DLL to be in the Reference directory. namespace kOS.Screen { @@ -360,7 +361,7 @@ void OnGUI() // Should probably make "gui screen name for my CPU part" into some sort of utility method: ChangeTitle(CalcualteTitle()); - WindowRect = GUI.Window(UniqueId, WindowRect, TerminalGui, TitleText); + WindowRect = ClickThruBlocker.GUIWindow(UniqueId, WindowRect, TerminalGui, TitleText); if (consumeEvent) { diff --git a/src/kOS/UserIO/TelnetMainServer.cs b/src/kOS/UserIO/TelnetMainServer.cs index f8c9919b3c..3759201f9a 100644 --- a/src/kOS/UserIO/TelnetMainServer.cs +++ b/src/kOS/UserIO/TelnetMainServer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using System.Net.NetworkInformation; using System.Collections.Generic; @@ -8,6 +8,8 @@ using kOS.Suffixed; using UnityEngine; using Debug = UnityEngine.Debug; +using ClickThroughFix; // Needs ClickThroughBlocker DLL to be in the Reference directory. + namespace kOS.UserIO { @@ -371,10 +373,10 @@ public bool SetBindAddrFromString(string s) void OnGUI() { if (activeOptInDialog) - optInRect = GUILayout.Window(401123, // any made up number unlikely to clash is okay here + optInRect = ClickThruBlocker.GUILayoutWindow(401123, // any made up number unlikely to clash is okay here optInRect, OptInOnGui, "kOS Telnet Opt-In Permisssion"); if (activeRealIPDialog) - realIPRect = GUILayout.Window(401124, // any made up number unlikely to clash is okay here + realIPRect = ClickThruBlocker.GUILayoutWindow(401124, // any made up number unlikely to clash is okay here realIPRect, RealIPOnGui, "kOS Telnet Non-Loopback Permisssion"); if (activeBgSimDialog) bgSimRect = GUILayout.Window(401125, // any made up number unlikely to clash is okay here diff --git a/src/kOS/kOS.csproj b/src/kOS/kOS.csproj index f7f8495605..4a7d441a02 100644 --- a/src/kOS/kOS.csproj +++ b/src/kOS/kOS.csproj @@ -37,6 +37,10 @@ ..\..\Resources\Assembly-CSharp-firstpass.dll + + False + ..\..\Resources\ClickThroughBlocker.dll + ..\..\Resources\GameData\kOS\Plugins\ICSharpCode.SharpZipLib.dll