Polished tooltips and implemented extended information.
Polished tooltips and implemented extended information.

--- a/KerbalEngineer/BuildEngineer/BuildButton.cs
+++ b/KerbalEngineer/BuildEngineer/BuildButton.cs
@@ -42,11 +42,11 @@
         {
             _tooltipTitleStyle = new GUIStyle(GUI.skin.label);
             _tooltipTitleStyle.fontSize = 13;
-            _tooltipTitleStyle.fontStyle = FontStyle.Normal;
+            _tooltipTitleStyle.fontStyle = FontStyle.Bold;
 
             _tooltipInfoStyle = new GUIStyle(GUI.skin.label);
             _tooltipInfoStyle.fontSize = 11;
-            _tooltipInfoStyle.fontStyle = FontStyle.Normal;
+            _tooltipInfoStyle.fontStyle = FontStyle.Bold;
         }
 
         #endregion

--- a/KerbalEngineer/BuildEngineer/BuildOverlay.cs
+++ b/KerbalEngineer/BuildEngineer/BuildOverlay.cs
@@ -2,6 +2,8 @@
 // Author:  CYBUTEK
 // License: Attribution-NonCommercial-ShareAlike 3.0 Unported
 
+using System.Collections.Generic;
+using System.Diagnostics;
 using KerbalEngineer.Extensions;
 using KerbalEngineer.Settings;
 using KerbalEngineer.Simulation;
@@ -24,9 +26,13 @@
         #region Fields
 
         private Rect _position = new Rect(265f, 0f, 0f, 0f);
-        private GUIStyle _windowStyle, _infoStyle, _tooltipStyle;
+        private GUIStyle _windowStyle, _titleStyle, _infoStyle, _tooltipTitleStyle, _tooltipInfoStyle;
         private int _windowID = EngineerGlobals.GetNextWindowID();
         private bool _hasInitStyles = false;
+
+        private Part _selectedPart = null;
+        private Stopwatch _tooltipInfoTimer = new Stopwatch();
+        private double _tooltipInfoDelay = 0.5d;
 
         #endregion
 
@@ -59,6 +65,14 @@
             _windowStyle = new GUIStyle(GUIStyle.none);
             _windowStyle.margin = new RectOffset();
             _windowStyle.padding = new RectOffset();
+
+            _titleStyle = new GUIStyle(HighLogic.Skin.label);
+            _titleStyle.normal.textColor = Color.white;
+            _titleStyle.margin = new RectOffset();
+            _titleStyle.padding = new RectOffset();
+            _titleStyle.fontSize = 11;
+            _titleStyle.fontStyle = FontStyle.Bold;
+            _titleStyle.stretchWidth = true;
 
             _infoStyle = new GUIStyle(HighLogic.Skin.label);
             _infoStyle.margin = new RectOffset();
@@ -67,10 +81,16 @@
             _infoStyle.fontStyle = FontStyle.Bold;
             _infoStyle.stretchWidth = true;
 
-            _tooltipStyle = new GUIStyle(HighLogic.Skin.label);
-            _tooltipStyle.fontSize = 11;
-            _tooltipStyle.fontStyle = FontStyle.Bold;
-            _tooltipStyle.stretchWidth = true;
+            _tooltipTitleStyle = new GUIStyle(HighLogic.Skin.label);
+            _tooltipTitleStyle.normal.textColor = Color.white;
+            _tooltipTitleStyle.fontSize = 11;
+            _tooltipTitleStyle.fontStyle = FontStyle.Bold;
+            _tooltipTitleStyle.stretchWidth = true;
+
+            _tooltipInfoStyle = new GUIStyle(HighLogic.Skin.label);
+            _tooltipInfoStyle.fontSize = 11;
+            _tooltipInfoStyle.fontStyle = FontStyle.Bold;
+            _tooltipInfoStyle.stretchWidth = true;
         }
 
         #endregion
@@ -116,18 +136,45 @@
                     // Find if a part is selected or being hovered over.
                     if (EditorLogic.SelectedPart != null)
                     {
+                        // Do not allow the extended information to be shown.
+                        if (_selectedPart != null)
+                        {
+                            _selectedPart = null;
+                            _tooltipInfoTimer.Reset();
+                        }
+
                         DrawTooltip(EditorLogic.SelectedPart);
                     }
                     else
                     {
+                        bool isPartSelected = false;
                         foreach (Part part in EditorLogic.SortedShipList)
-                        {
+                        { 
                             if (part.stackIcon.highlightIcon)
                             {
+                                // Start the extended information timer.
+                                if (part != _selectedPart)
+                                {
+                                    _selectedPart = part;
+                                    _tooltipInfoTimer.Reset();
+                                    _tooltipInfoTimer.Start();
+                                }
+                                isPartSelected = true;
+
                                 DrawTooltip(part);
                                 break;
                             }
                         }
+
+                        // If no part is being hovered over we must reset the extended information timer.
+                        if (!isPartSelected)
+                        {
+                            if (_selectedPart != null)
+                            {
+                                _selectedPart = null;
+                                _tooltipInfoTimer.Reset();
+                            }
+                        }
                     }
                 }
             }
@@ -140,9 +187,9 @@
 
             // Titles
             GUILayout.BeginVertical(GUILayout.Width(75f));
-            GUILayout.Label("Parts:", _infoStyle);
-            GUILayout.Label("Delta-V:", _infoStyle);
-            GUILayout.Label("TWR:", _infoStyle);
+            GUILayout.Label("Parts:", _titleStyle);
+            GUILayout.Label("Delta-V:", _titleStyle);
+            GUILayout.Label("TWR:", _titleStyle);
             GUILayout.EndVertical();
 
             // Details
@@ -158,11 +205,106 @@
         // Draws the tooltip details of the selected/highlighted part.
         private void DrawTooltip(Part part)
         {
+            // Tooltip title (name of part).
             GUIContent content = new GUIContent(part.partInfo.title);
-            Vector2 size = _tooltipStyle.CalcSize(content);
+            Vector2 size = _tooltipTitleStyle.CalcSize(content);
             Rect position = new Rect(Event.current.mousePosition.x + 16f, Event.current.mousePosition.y, size.x, size.y).ClampInsideScreen();
             if (position.x < Event.current.mousePosition.x + 16f) position.y += 16f;
-            GUI.Label(position, content, _tooltipStyle);
+            GUI.Label(position, content, _tooltipTitleStyle);
+
+            // After hovering for a period of time, show extended information.
+            if (_tooltipInfoTimer.Elapsed.TotalSeconds >= _tooltipInfoDelay)
+            {
+                // Stop the timer as it is no longer needed.
+                if (_tooltipInfoTimer.IsRunning)
+                    _tooltipInfoTimer.Stop();
+
+                // Show the dry mass of the part if applicable.
+                if (part.physicalSignificance == Part.PhysicalSignificance.FULL)
+                    DrawTooltipInfo(ref position, "Dry Mass: " + part.GetDryMass().ToMass());
+
+                // Show resources contained within the part.
+                if (part.ContainsResources())
+                {
+                    // Show the wet mass of the part if applicable.
+                    if (part.GetResourceMass() > 0f)
+                        DrawTooltipInfo(ref position, "Wet Mass: " + part.GetWetMass().ToMass());
+
+                    // List all the resources contained within the part.
+                    foreach (PartResource resource in part.Resources)
+                    {
+                        double density = resource.GetDensity();
+                        if (density > 0d)
+                            DrawTooltipInfo(ref position, resource.info.name + ": " + resource.GetMass().ToMass() + " (" + resource.amount + ")");
+                        else
+                            DrawTooltipInfo(ref position, resource.info.name + ": " + resource.amount);
+                    }
+                }
+
+                // Show details for engines.
+                if (part.IsEngine())
+                {
+                    DrawTooltipInfo(ref position, "Maximum Thrust: " + part.GetMaxThrust().ToForce());
+                    DrawTooltipInfo(ref position, "Specific Impulse: " + part.GetSpecificImpulse(1f) + " / " + part.GetSpecificImpulse(0f) + "s");
+
+                    // Thrust vectoring.
+                    if (part.HasModule("ModuleGimbal"))
+                        DrawTooltipInfo(ref position, "Thrust Vectoring Enabled");
+
+                    // Contains alternator.
+                    if (part.HasModule("ModuleAlternator"))
+                        DrawTooltipInfo(ref position, "Contains Alternator");
+                }
+
+                // Show details for solar panels.
+                if (part.IsSolarPanel())
+                {
+                    ModuleDeployableSolarPanel module = part.GetModuleDeployableSolarPanel();
+                    DrawTooltipInfo(ref position, "Charge Rate: " + module.chargeRate.ToDouble().ToRate());
+                }
+
+                // Show details for generators.
+                if (part.IsGenerator())
+                {
+                    foreach (ModuleGenerator.GeneratorResource resource in part.GetModuleGenerator().inputList)
+                        DrawTooltipInfo(ref position, "Input: " + resource.name + " (" + resource.rate.ToDouble().ToRate() + ")");
+
+                    foreach (ModuleGenerator.GeneratorResource resource in part.GetModuleGenerator().outputList)
+                        DrawTooltipInfo(ref position, "Output: " + resource.name + " (" + resource.rate.ToDouble().ToRate() + ")");
+                }
+
+                // Show details for parachutes.
+                if (part.IsParachute())
+                {
+                    ModuleParachute module = part.GetModuleParachute();
+                    DrawTooltipInfo(ref position, "Semi Deployed Drag: " + module.semiDeployedDrag);
+                    DrawTooltipInfo(ref position, "Fully Deployed Drag: " + module.fullyDeployedDrag);
+                    DrawTooltipInfo(ref position, "Deployment Altitude: " + module.deployAltitude.ToDouble().ToDistance());
+                }
+
+                // Contains stability augmentation system.
+                if (part.HasModule("ModuleSAS"))
+                    DrawTooltipInfo(ref position, "Contains SAS");
+
+                // Contains reaction wheels.
+                if (part.HasModule("ModuleReactionWheel"))
+                    DrawTooltipInfo(ref position, "Contains Reaction Wheels");
+
+                // Show if the part has an animation that can only be used once.
+                if (part.HasOneShotAnimation())
+                    DrawTooltipInfo(ref position, "Single Activation Only");
+            }
+        }
+
+        // Draws a line of extended information below the previous.
+        private void DrawTooltipInfo(ref Rect position, string value)
+        {
+            GUIContent content = new GUIContent(value);
+            Vector2 size = _tooltipInfoStyle.CalcSize(content);
+            position.y += 16f;
+            position.width = size.x;
+            position.height = size.y;
+            GUI.Label(position, content, _tooltipInfoStyle);
         }
 
         #endregion

--- a/KerbalEngineer/Extensions/DoubleExtensions.cs
+++ b/KerbalEngineer/Extensions/DoubleExtensions.cs
@@ -39,6 +39,49 @@
                 return value.ToString("0") + " m/s";
             else
                 return value.ToString("0");
+        }
+
+        /// <summary>
+        /// Convert to string formatted as a distance.
+        /// </summary>
+        public static string ToDistance(this double value)
+        {
+            if (value < 1000000d)
+            {
+                if (value < 1d)
+                {
+                    value *= 1000d;
+                    return value.ToString("#,0.") + " mm";
+                }
+                else
+                {
+                    return value.ToString("#,0.") + " m";
+                }
+            }
+            else
+            {
+                value /= 1000d;
+                if (value >= 1000000d)
+                {
+                    value /= 1000d;
+                    return value.ToString("#,0." + " Mm");
+                }
+                else
+                {
+                    return value.ToString("#,0." + " km");
+                }
+            }
+        }
+
+        /// <summary>
+        /// Convert to string formatted as a rate.
+        /// </summary>
+        public static string ToRate(this double value)
+        {
+            if (value > 0)
+                return value.ToString("0.0") + "/sec";
+            else
+                return (60d * value).ToString("0.0") + "/min";
         }
 
         /// <summary>

--- /dev/null
+++ b/KerbalEngineer/Extensions/FloatExtensions.cs
@@ -1,1 +1,18 @@
+// Name:    Kerbal Engineer Redux
+// Author:  CYBUTEK
+// License: Attribution-NonCommercial-ShareAlike 3.0 Unported
 
+namespace KerbalEngineer.Extensions
+{
+    public static class FloatExtensions
+    {
+        /// <summary>
+        /// Convert to a single precision floating point number.
+        /// </summary>
+        public static double ToDouble(this float value)
+        {
+            return (double)value;
+        }
+    }
+}
+

--- a/KerbalEngineer/Extensions/PartExtensions.cs
+++ b/KerbalEngineer/Extensions/PartExtensions.cs
@@ -40,7 +40,6 @@
         /// </summary>
         public static T GetModule<T>(this Part part, string className) where T : PartModule
         {
-
             return (T)Convert.ChangeType(part.Modules[className], typeof(T));
         }
 
@@ -53,11 +52,59 @@
         }
 
         /// <summary>
+        /// Gets a ModuleEngines typed PartModule.
+        /// </summary>
+        public static ModuleEngines GetModuleEngines(this Part part)
+        {
+            return part.GetModule<ModuleEngines>("ModuleEngines");
+        }
+
+        /// <summary>
+        /// Gets a ModuleGimbal typed PartModule.
+        /// </summary>
+        public static ModuleGimbal GetModuleGimbal(this Part part)
+        {
+            return part.GetModule<ModuleGimbal>("ModuleGimbal");
+        }
+
+        /// <summary>
+        /// Gets a ModuleDeployableSolarPanel typed PartModule.
+        /// </summary>
+        public static ModuleDeployableSolarPanel GetModuleDeployableSolarPanel(this Part part)
+        {
+            return part.GetModule<ModuleDeployableSolarPanel>("ModuleDeployableSolarPanel");
+        }
+
+        /// <summary>
+        /// Gets a ModuleAlternator typed PartModule.
+        /// </summary>
+        public static ModuleAlternator GetModuleAlternator(this Part part)
+        {
+            return part.GetModule<ModuleAlternator>("ModuleAlternator");
+        }
+
+        /// <summary>
+        /// Gets a ModuleGenerator typed PartModule.
+        /// </summary>
+        public static ModuleGenerator GetModuleGenerator(this Part part)
+        {
+            return part.GetModule<ModuleGenerator>("ModuleGenerator");
+        }
+
+        /// <summary>
+        /// Gets a ModuleParachute typed PartModule.
+        /// </summary>
+        public static ModuleParachute GetModuleParachute(this Part part)
+        {
+            return part.GetModule<ModuleParachute>("ModuleParachute");
+        }
+
+        /// <summary>
         /// Gets the total mass of the part including resources.
         /// </summary>
-        public static double GetTotalMass(this Part part)
-        {
-            return (part.physicalSignificance == Part.PhysicalSignificance.FULL) ? part.mass + part.GetResourceMass() : 0d;
+        public static double GetWetMass(this Part part)
+        {
+            return (part.physicalSignificance == Part.PhysicalSignificance.FULL) ? part.mass + part.GetResourceMass() : part.GetResourceMass();
         }
 
         /// <summary>
@@ -73,7 +120,12 @@
         /// </summary>
         public static double GetMaxThrust(this Part part)
         {
-            return (part.IsEngine()) ? part.GetModule<ModuleEngines>("ModuleEngines").maxThrust : 0d;
+            return (part.IsEngine()) ? part.GetModuleEngines().maxThrust : 0d;
+        }
+
+        public static double GetSpecificImpulse(this Part part, float atmosphere)
+        {
+            return (part.IsEngine()) ? part.GetModuleEngines().atmosphereCurve.Evaluate(atmosphere) : 0d;
         }
 
         /// <summary>
@@ -81,7 +133,20 @@
         /// </summary>
         public static bool EngineHasFuel(this Part part)
         {
-            return part.HasModule("ModuleEngines") && !part.GetModule<ModuleEngines>("ModuleEngines").getFlameoutState;
+            return part.IsEngine() && !part.GetModuleEngines().getFlameoutState;
+        }
+
+        /// <summary>
+        /// Gets whether the part contains resources.
+        /// </summary>
+        public static bool ContainsResources(this Part part)
+        {
+            return part.Resources.list.Count(p => p.amount > 0d) > 0;
+        }
+
+        public static bool ContainsResource(this Part part, int resourceID)
+        {
+            return part.Resources.Contains(resourceID);
         }
 
         /// <summary>
@@ -119,11 +184,43 @@
         }
 
         /// <summary>
+        /// Gets whether the part is a deployable solar panel.
+        /// </summary>
+        public static bool IsSolarPanel(this Part part)
+        {
+            return part.HasModule("ModuleDeployableSolarPanel");
+        }
+
+        /// <summary>
+        /// Gets whether the part is a generator.
+        /// </summary>
+        public static bool IsGenerator(this Part part)
+        {
+            return part.HasModule("ModuleGenerator");
+        }
+
+        /// <summary>
+        /// Gets whether the part is a command module.
+        /// </summary>
+        public static bool IsCommandModule(this Part part)
+        {
+            return part.HasModule("ModuleCommand");
+        }
+
+        /// <summary>
+        /// Gets whether the part is a parachute.
+        /// </summary>
+        public static bool IsParachute(this Part part)
+        {
+            return part.HasModule("ModuleParachute");
+        }
+
+        /// <summary>
         /// Gets whether the part is a solid rocket motor.
         /// </summary>
         public static bool IsSolidRocket(this Part part)
         {
-            return part.IsEngine() && part.GetModule<ModuleEngines>("ModuleEngines").throttleLocked;
+            return part.IsEngine() && part.GetModuleEngines().throttleLocked;
         }
 
         /// <summary>
@@ -164,6 +261,19 @@
 
             return false;
         }
+
+        /// <summary>
+        /// Gets whether the part has a one shot animation.
+        /// </summary>
+        public static bool HasOneShotAnimation(this Part part)
+        {
+            if (part.HasModule("ModuleAnimateGeneric"))
+            {
+                return part.GetModule<ModuleAnimateGeneric>("ModuleAnimateGeneric").isOneShot;
+            }
+
+            return false;
+        }
     }
 }
 

--- /dev/null
+++ b/KerbalEngineer/Extensions/PartResourceExtensions.cs
@@ -1,1 +1,36 @@
+// Name:    Kerbal Engineer Redux
+// Author:  CYBUTEK
+// License: Attribution-NonCommercial-ShareAlike 3.0 Unported
 
+using UnityEngine;
+
+namespace KerbalEngineer.Extensions
+{
+    public static class PartResourceExtensions
+    {
+        /// <summary>
+        /// Gets the definition object for the resource.
+        /// </summary>
+        public static PartResourceDefinition GetDefinition(this PartResource value)
+        {
+            return PartResourceLibrary.Instance.GetDefinition(value.info.id);
+        }
+
+        /// <summary>
+        /// Gets the density of the resource.
+        /// </summary>
+        public static double GetDensity(this PartResource value)
+        {
+            return value.GetDefinition().density;
+        }
+
+        /// <summary>
+        /// Gets the mass of the resource.
+        /// </summary>
+        public static double GetMass(this PartResource value)
+        {
+            return value.amount * value.GetDensity();
+        }
+    }
+}
+

--- a/KerbalEngineer/Extensions/RectExtensions.cs
+++ b/KerbalEngineer/Extensions/RectExtensions.cs
@@ -12,8 +12,6 @@
         /// <summary>
         /// Clamps the rectangle inside the screen region.
         /// </summary>
-        /// <param name="value"></param>
-        /// <returns></returns>
         public static Rect ClampInsideScreen(this Rect value)
         {
             if (value.x < 0f) value.x = 0f;

--- a/KerbalEngineer/KerbalEngineer.csproj
+++ b/KerbalEngineer/KerbalEngineer.csproj
@@ -53,7 +53,9 @@
     <Compile Include="CelestialBodies.cs" />
     <Compile Include="EngineerGlobals.cs" />
     <Compile Include="Extensions\DoubleExtensions.cs" />
+    <Compile Include="Extensions\FloatExtensions.cs" />
     <Compile Include="Extensions\PartExtensions.cs" />
+    <Compile Include="Extensions\PartResourceExtensions.cs" />
     <Compile Include="Extensions\RectExtensions.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Settings\Setting.cs" />

 Binary files a/Output/KerbalEngineer/KerbalEngineer.dll and b/Output/KerbalEngineer/KerbalEngineer.dll differ