/* Copyright (c) 2007 Ben Howell
* This software is licensed under the MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using DecalTimer = Decal.Interop.Input.TimerClass;
namespace GoArrow.Huds
{
public class ExceptionEventArgs : EventArgs
{
public readonly Exception Exception;
public ExceptionEventArgs(Exception ex)
{
this.Exception = ex;
}
}
///
/// A Hud class that is managed by a HudManager must implement
/// this interface.
///
interface IManagedHud : IDisposable
{
///
/// This function is called once per frame so the Hud can stay updated.
///
void RepaintHeartbeat();
///
/// This is called either when the Hud is lost because of a graphics
/// reset, or if the Hud needs to be moved on top of other Huds. The
/// implementation of this function should disable and remove the Hud
/// from the render service (unless it has already been lost by a
/// graphics reset), then recreate the Hud right away, even if it is
/// not enabled. This will ensure proper Z-ordering of the Huds.
///
void RecreateHud();
///
/// This is called whenever there is a WindowsMessage. It is necessary
/// for Huds to use this rather than directly subscribing to the
/// Core.WindowMessage event so the manager can control the order in
/// which Huds process the messages (Huds on top process the messages
/// first).
///
void WindowMessage(WindowMessageEventArgs e);
///
/// Gets the Manager for this hud, or null if this hud has not yet
/// registered with a manager.
///
HudManager Manager { get; }
///
/// This property should be true if the mouse is hovering over the hud
/// AND the type of Hud obscures other Huds (such as a WindowHud). This
/// is used to help other Huds determine if the mouse is hovering on a
/// part of them thatn is not obscured by another hud.
///
bool MouseHoveringObscuresOther { get; }
///
/// This property should return false until the Dispose() method is
/// called on the Hud.
///
bool Disposed { get; }
bool Visible { get; set; }
event EventHandler VisibleChanged;
}
///
/// Manages the Z-ordering, graphics reset events, and WindowMessage events
/// of a set of WindowHuds.
///
class HudManager : IDisposable
{
#region WindowMessage Constants
public const short WM_MOUSEMOVE = 0x0200;
public const short WM_LBUTTONDOWN = 0x0201;
public const short WM_LBUTTONUP = 0x0202;
public const short WM_RBUTTONDOWN = 0x0204;
public const short WM_RBUTTONUP = 0x0205;
public const short WM_MBUTTONDOWN = 0x0207;
public const short WM_MBUTTONUP = 0x0208;
public const short WM_MOUSEWHEEL = 0x020A;
// Range of WM_MOUSE* events
public const short WM_MOUSEFIRST = 0x0200;
public const short WM_MOUSELAST = 0x020A;
#endregion
///
/// All of the non-AlwaysOnTop huds that are managed by this manager
/// and not disposed.
///
private LinkedList mHudsList = new LinkedList();
///
/// All of the AlwaysOnTop huds that are managed by this manager and
/// not disposed.
///
private LinkedList mHudsOnTopList = new LinkedList();
///
/// This is necessary for using a foreach loop to go through all the
/// HUDs when the order of the huds may be changed by the loop (such
/// as during WindowMessage processing).
///
private List mHudsListCopy = new List();
private bool mHudsListChanged = false;
private PluginHost mHost;
private CoreManager mCore;
private MyClasses.MetaViewWrappers.IView mDefaultView;
DefaultViewActiveDelegate mDefaultViewActive;
private DecalTimer mRepaintHeartbeat;
private ToolTipHud mToolTip;
private Point mMousePos;
private bool mDisposed = false;
///
/// Occurs when this HudManager or any IManagedHud managed by this
/// encounters an unhandled exception in one of its event handlers
/// (like WindowsMessage or GraphicsReset).
///
public event EventHandler ExceptionHandler;
/// Occurs once per frame.
public event EventHandler Heartbeat;
public event EventHandler RegionChange3D;
///
/// Constructs a new instance of a HudManager. You must also register
/// the GraphicsReset() function for the PluginBase.GraphicsReset event.
///
/// PluginBase.Host
/// PluginBase.Core
/// If this is true, the heartbeat
/// timer will start ticking right away. This is generally
/// undesirable if this HudManager is created in the Startup()
/// method of the plugin. If this is false, you must call
/// at a later time, such as during
/// the PlayerLogin event.
public HudManager(PluginHost host, CoreManager core, MyClasses.MetaViewWrappers.IView defaultView, DefaultViewActiveDelegate defaultViewActive, bool startHeartbeatNow)
{
mHost = host;
mCore = core;
mDefaultView = defaultView;
mDefaultViewActive = defaultViewActive;
mToolTip = new ToolTipHud(this);
mRepaintHeartbeat = new DecalTimer();
mRepaintHeartbeat.Timeout += new Decal.Interop.Input.ITimerEvents_TimeoutEventHandler(RepaintHeartbeatDispatch);
if (startHeartbeatNow)
StartHeartbeat();
//Core.WindowMessage += new EventHandler(WindowMessageDispatch);
}
///
/// Starts the heartbeat timer if it was not started when this
/// HudManager was created.
///
public void StartHeartbeat()
{
if (!mRepaintHeartbeat.Running)
{
// The timeout is redicuously short, but Decal Timers only
// fire at most once per frame.
mRepaintHeartbeat.Start(1);
}
}
///
/// Cleans up the HudManager and Disposes all windows that are
/// being managed by this HudManager. Use this function when the
/// plugin is shutting down. Also be sure to unregister
/// from the GraphicsReset event.
///
public void Dispose()
{
if (Disposed)
return;
if (mRepaintHeartbeat.Running)
mRepaintHeartbeat.Stop();
mRepaintHeartbeat.Timeout -= RepaintHeartbeatDispatch;
mRepaintHeartbeat = null;
//Core.WindowMessage -= WindowMessageDispatch;
// Need to use a copy of the list because the Dispose() method of
// windows modifies mWindowList.
UpdateHudsListCopy();
foreach (IManagedHud hud in mHudsListCopy)
{
hud.Dispose();
}
mHudsOnTopList.Clear();
mHudsList.Clear();
mHudsListCopy.Clear();
mHost = null;
mCore = null;
mDefaultView = null;
mDefaultViewActive = null;
mDisposed = true;
}
///
/// Gets whether this HudManager has been disposed.
///
public bool Disposed
{
get { return mDisposed; }
}
///
/// Sets a Hud's always on top status. When a hud is always on top, it
/// will be painted above all other non-always-on-top huds.
///
/// The hud to set.
/// Whether the given hud should be always on
/// top of other huds.
public void SetAlwaysOnTop(IManagedHud hud, bool alwaysOnTop)
{
if (alwaysOnTop)
{
if (mHudsList.Remove(hud))
{
mHudsOnTopList.AddFirst(hud);
hud.RecreateHud();
}
}
else if (mHudsOnTopList.Remove(hud))
{
mHudsList.AddFirst(hud);
hud.RecreateHud();
RecreateInReverseOrder(mHudsOnTopList, false);
}
}
///
/// Checks if a Hud is always on top of other huds.
///
/// The hud to check.
/// True if the specified hud is set to always be on top of
/// other huds. This function will return false if the hud is not
/// managed by this HudManager.
public bool IsAlwaysOnTop(IManagedHud hud)
{
return mHudsOnTopList.Contains(hud);
}
///
/// Puts the specified hud on top of all other Huds. This function does
/// nothing if the hud is not managed by this manager or has been
/// disposed.
///
/// The hud to move to the top.
/// Whether to force a recreate of the
/// given HUD, even if it is already at the front.
public void BringToFront(IManagedHud hud, bool forceRecreateHud)
{
// Check if the hud is already on top
if (mHudsList.Count > 0 && mHudsList.First.Value == hud
|| mHudsOnTopList.Count > 0 && mHudsOnTopList.First.Value == hud)
{
if (forceRecreateHud)
{
RecreateHud(hud);
}
return;
}
if (mHudsList.Remove(hud))
{
mHudsList.AddFirst(hud);
hud.RecreateHud();
// Recreate the AlwaysOnTop huds to keep them on top of this one
RecreateInReverseOrder(mHudsOnTopList, false);
}
else if (mHudsOnTopList.Remove(hud))
{
mHudsOnTopList.AddFirst(hud);
hud.RecreateHud();
}
mHudsListChanged = true;
}
///
/// Puts the specified hud behind all other Huds that are managed by
/// this HudManager. This function does nothing if the hud is not
/// managed by this manager or has been disposed.
///
///
/// This function has the side effect of moving all WindowHuds that
/// are managed by this HudManager above all other HUDs in Decal,
/// but only if the specified hud is not already behind all other
/// windows managed by this manager.
///
/// The hud to move to the back.
/// Whether to force a recreate of the
/// given HUD, even if it is already at the back.
public void SendToBack(IManagedHud hud, bool forceRecreateHud)
{
// Check if the hud is already at the back
if (mHudsList.Count > 0 && mHudsList.Last.Value == hud
|| mHudsOnTopList.Count > 0 && mHudsOnTopList.Last.Value == hud)
{
if (forceRecreateHud)
{
RecreateHud(hud);
}
return;
}
if (mHudsList.Remove(hud))
{
mHudsList.AddLast(hud);
RecreateInReverseOrder(mHudsList, true);
RecreateInReverseOrder(mHudsOnTopList, false);
}
else if (mHudsOnTopList.Remove(hud))
{
mHudsOnTopList.AddLast(hud);
RecreateInReverseOrder(mHudsOnTopList, true);
}
mHudsListChanged = true;
}
///
/// Recreates the specified hud and any huds that are on top of it, to
/// maintain Z-ordering. Use this function instead of calling the HUD's
/// RecreateHud() function directly. This function does nothing if the
/// hud is not managed by this manager or has been disposed.
///
/// The HUD to recreate.
public void RecreateHud(IManagedHud hud)
{
LinkedListNode hudNode;
if ((hudNode = mHudsList.Find(hud)) != null)
{
RecreateInReverseOrder(mHudsList, hudNode);
RecreateInReverseOrder(mHudsOnTopList, false);
}
else if ((hudNode = mHudsOnTopList.Find(hud)) != null)
{
RecreateInReverseOrder(mHudsOnTopList, hudNode);
}
}
///
/// Register this function to receive PluginBase.GraphicsReset events.
///
public void GraphicsReset(object sender, EventArgs e)
{
try
{
if (!Disposed)
{
// Recreate the huds in reverse order to mainain z-ordering
RecreateInReverseOrder(mHudsList, false);
RecreateInReverseOrder(mHudsOnTopList, false);
}
}
catch (Exception ex) { HandleException(ex); }
}
public PluginHost Host
{
get { return mHost; }
}
public CoreManager Core
{
get { return mCore; }
}
public MyClasses.MetaViewWrappers.IView DefaultView
{
get { return mDefaultView; }
}
public bool DefaultViewActive
{
get { return mDefaultViewActive(); }
}
///
/// Fires the ExceptionHandler event. Huds should call this function
/// in the event of an unhandled exception that is in event handling
/// code (such as a timer's tick).
///
/// The exception that occurred.
public void HandleException(Exception ex)
{
if (ExceptionHandler != null)
ExceptionHandler(null, new ExceptionEventArgs(ex));
}
///
/// Adds a new hud to the HudManager, on top of all other huds. If the
/// Hud has been registered with another HudManager, it will be
/// unregistered from that manager. The hud will receive GraphicsReset,
/// WindowMessage, and RepaintHeartbeat events.
/// Calls RecreateHud() on the given hud to make sure that it
/// is on top.
///
/// The hud to add.
/// Indicates if the hud is always on top of
/// other huds.
public void RegisterHud(IManagedHud hud, bool alwaysOnTop)
{
if (hud.Manager != null)
{
hud.Manager.UnregisterHud(hud);
}
if (alwaysOnTop)
{
mHudsOnTopList.AddFirst(hud);
hud.RecreateHud();
}
else
{
mHudsList.AddFirst(hud);
hud.RecreateHud();
RecreateInReverseOrder(mHudsOnTopList, false);
}
mHudsListChanged = true;
}
///
/// Removes a hud from the HudManager. It will no longer receive
/// GraphicsReset, WindowMessage, or RepaintHeartbeat events.
///
/// The hud to remove.
public void UnregisterHud(IManagedHud hud)
{
mHudsList.Remove(hud);
mHudsOnTopList.Remove(hud);
mHudsListChanged = true;
}
///
/// Checks if the mouse is hovering on this hud and NOT hovering on
/// any hud that's on top of the specified hud.
///
///
/// This will usually be called during WindowMessageDispatch by a
/// hud during a WM_MOUSEMOVE event. Thus, not all huds will have
/// been notified that the mouse has moved yet. This is okay because
/// all huds on top of this hud WILL have been notified since
/// they're notified in Z-order, and the huds that are on top are
/// the only huds that matter.
///
/// The hud to check.
/// True if the mouse is hovering on the specified hud and
/// NOT hovering on a hud that's on top of the hud.
public bool MouseHoveringOnHud(IManagedHud hudToCheck)
{
//UpdateHudsListCopy();
if (DefaultViewActive && DefaultView.Position.Contains(mMousePos))
{
return false;
}
foreach (IManagedHud hud in mHudsListCopy)
{
if (hud == hudToCheck)
{
return hud.MouseHoveringObscuresOther;
}
else if (hud.MouseHoveringObscuresOther)
{
return false;
}
}
return false;
}
public void ShowToolTip(Point location, string message)
{
mToolTip.Show(location, message);
}
public void ShowToolTip(Point location, string message, int hideDelayMillis)
{
mToolTip.Show(location, message, hideDelayMillis);
}
public void HideToolTip()
{
mToolTip.Hide();
}
private void UpdateHudsListCopy()
{
if (mHudsListChanged)
{
mHudsListCopy.Clear();
mHudsListCopy.AddRange(mHudsOnTopList);
mHudsListCopy.AddRange(mHudsList);
mHudsListChanged = false;
}
}
private void RecreateInReverseOrder(LinkedList hudsList, bool skipLast)
{
if (hudsList.Count > 0)
{
RecreateInReverseOrder(hudsList, skipLast ? hudsList.Last.Previous : hudsList.Last);
}
}
private void RecreateInReverseOrder(LinkedList hudsList, LinkedListNode start)
{
for (LinkedListNode i = start; i != null; i = i.Previous)
{
i.Value.RecreateHud();
}
}
public void DispatchRegionChange3D(object sender, RegionChange3DEventArgs e)
{
try
{
// Forward the event
if (RegionChange3D != null) { RegionChange3D(sender, e); }
}
catch (Exception ex) { HandleException(ex); }
}
public void DispatchWindowMessage(object sender, WindowMessageEventArgs e)
{
try
{
if (e.Msg >= WM_MOUSEFIRST && e.Msg <= WM_MOUSELAST && !Disposed)
{
if (e.Msg == WM_MOUSEMOVE)
{
mMousePos = new Point(e.LParam);
}
// Don't handle mouse events when the mouse is on the view
else if (DefaultViewActive &&
(e.Msg == WM_LBUTTONDOWN || e.Msg == WM_MBUTTONDOWN ||
e.Msg == WM_RBUTTONDOWN || e.Msg == WM_MOUSEWHEEL)
&& DefaultView.Position.Contains(new Point(e.LParam)))
{
return;
}
// Make a copy of the list in case it is modified while
// processing the message (like if one of the mouse event
// handlers calls BringToFront()).
UpdateHudsListCopy();
bool origEat = e.Eat;
if (!e.Eat)
{
foreach (IManagedHud hud in mHudsListCopy)
{
// It's possible for the hud to be disposed if a
// mouse event handler for another one disposes it.
if (!hud.Disposed)
{
hud.WindowMessage(e);
if (e.Eat)
break;
}
}
// Don't let huds eat mouse moves
if (e.Eat != origEat && e.Msg == WM_MOUSEMOVE)
{
e.Eat = origEat;
}
}
}
}
catch (Exception ex) { HandleException(ex); }
}
///
/// This is called once per frame by the mRepaintHeartbeat timer.
/// It just tells each of the huds to repaint if they need to.
///
private void RepaintHeartbeatDispatch(Decal.Interop.Input.Timer Source)
{
try
{
if (!Disposed)
{
if (Heartbeat != null)
Heartbeat(this, EventArgs.Empty);
// Make a copy of the list in case it is modified while
// repainting. As of writing this comment that won't happen,
// but it's a quick check...
UpdateHudsListCopy();
foreach (IManagedHud hud in mHudsListCopy)
{
hud.RepaintHeartbeat();
}
}
}
catch (Exception ex) { HandleException(ex); }
}
}
}