//
/// \file
/// Implements the TTooltip class and helper classes
//
// Part of OWLNext - the next generation Object Windows Library
// Copyright (c) 1995, 1996 by Borland International, All Rights Reserved
// Copyright (c) 2020 by Vidar Hasfjord
//
// For more information, including license details, see
// http://owlnext.sourceforge.net
//
 
#include <owl/pch.h>
 
#include <owl/window.h>
#include <owl/gdiobjec.h>
#include <owl/tooltip.h>
#include <owl/commctrl.h>
#include <owl/uimetric.h>
#include <limits>
 
namespace {
 
//
// This function solves a problem for Unicode builds without a manifest for Common Controls
// version 6. In this case, the classic non-themed version of Common Controls is used (5.x),
// and this version expects the old size of the TOOLINFO structure. Consequently, naively using
// the size of TOOLINFO to initialize TOOLINFO::cbSize will cause tool tips to not show.
//
auto GetToolInfoStructSize_() -> int
{
  static const auto v = owl::GetCommCtrlVersion();
  return static_cast<int>(v >= 0x60000 ? sizeof(TOOLINFO) : TTTOOLINFO_V1_SIZE);
}
 
} // namespace
 
namespace owl {
 
OWL_DIAGINFO;
DIAG_DECLARE_GROUP(OwlCommCtrl);
 
//-------------------------------------------------------------------------------------------------
// TToolInfo
//
 
//#if defined(OWL5_COMPAT)
 
//
/// This is the default constructor of TToolInfo.
///
/// It is used mainly when retrieving information about the current tool of the tooltip control or
/// for initializing a brand new tool to be registered with the control.
///
/// Usage:
///
/// \code
///   TToolInfo ti;
///   tooltip.GetCurrentTool(ti);
/// \endcode
//
TToolInfo::TToolInfo(bool allocCache)
{
  memset(this, 0, sizeof(TOOLINFO));
  cbSize = GetToolInfoStructSize_();
  if (allocCache)
  {
    CHECK(DefTipTextCacheSize > 0);
    CacheText.resize(DefTipTextCacheSize);
    lpszText = &CacheText[0];
  }
}
 
//
/// Constructor for a tool implemented as a rectangular area within a window's client area.
///
/// The window designated by parameter \p window receives the TTN_NEEDTEXT notification in case the
/// parameter \p txt is set to LPSTR_TEXTCALLBACK (the default).
//
TToolInfo::TToolInfo(HWND window, const TRect& rc, uint toolId, LPCTSTR txt)
{
  memset(this, 0, sizeof(TOOLINFO));
  cbSize = GetToolInfoStructSize_();
  SetToolInfo(window, toolId);
  SetRect(rc);
 
  // NOTE: When we're using the Common Control implementation we don't want
  //       to cache the text since we won't keep a copy of the TToolInfo
  //       structure around.
  //
  SetText(txt, false);
}
 
//
/// String overload.
//
TToolInfo::TToolInfo(HWND window, const TRect& rc, uint toolId, const tstring& txt)
{
  memset(this, 0, sizeof(TOOLINFO));
  cbSize = GetToolInfoStructSize_();
  SetToolInfo(window, toolId);
  SetRect(rc);
  SetText(txt);
}
 
//
/// Constructor for a tool implemented as a rectangular area within a window's client area.
///
/// The parameters \p strRes and \p hInst specify a string resource of the tip text to be used by
/// the tooltip window.
//
TToolInfo::TToolInfo(HWND window, const TRect& rc, uint toolId, int strRes, HINSTANCE hInst)
{
  memset(this, 0, sizeof(TOOLINFO));
  cbSize = GetToolInfoStructSize_();
  SetToolInfo(window, toolId, rc);
  SetText(strRes, hInst);
}
 
//
/// Constructor for a tool implemented as a window (child/controls).
/// The window designated by parameter \p parent receives the TTN_NEEDTEXT notification in case the
/// parameter \p txt is set to LPSTR_TEXTCALLBACK (the default).
//
TToolInfo::TToolInfo(HWND parent, HWND toolWnd, LPCTSTR txt)
{
  memset(this, 0, sizeof(TOOLINFO));
  cbSize = GetToolInfoStructSize_();
  SetToolInfo(toolWnd, parent);
  SetText(txt, false);
}
 
//
/// String overload.
//
TToolInfo::TToolInfo(HWND parent, HWND toolWnd, const tstring& txt)
{
  memset(this, 0, sizeof(TOOLINFO));
  cbSize = GetToolInfoStructSize_();
  SetToolInfo(toolWnd, parent);
  SetText(txt);
}
 
//
/// Constructor for a tool implemented as a window (child/control).
/// 
/// The parameters \p strRes and \p hInst specify a string resource to be used as the tip text by
/// the tooltip window.
//
TToolInfo::TToolInfo(HWND parent, HWND toolWnd, int strRes, HINSTANCE hInst)
{
  memset(this, 0, sizeof(TOOLINFO));
  cbSize = GetToolInfoStructSize_();
  SetToolInfo(toolWnd, parent);
  SetText(strRes, hInst);
}
 
TToolInfo::TToolInfo(const TToolInfo& other)
{
  // Use assignment operator
  //
  *this = other;
}
 
//
/// Performs a deep copy of the cached text, if any.
//
auto TToolInfo::operator =(const TToolInfo& other) -> TToolInfo&
{
  if (&other != this)
  {
    *((TOOLINFO*) this) = *((TOOLINFO*) &other);
    if (other.lpszText == other.GetCacheText())
      SetText(other.GetCacheText());
    /***
    else
     'other.lpszText' is assumed to be NULL, LPSTR_CALLBACK or pointing
      to a buffer with a long lifetime. In all three cases a shallow copy of
      the pointer is safe.
    ***/
  }
  return *this;
}
 
//
/// Tests wether the given object refers to the same tool as this.
///
/// If the IDs of the two tools (or the container window handles of two tools for cases when the
/// flag `TTF_IDISHWND` is set) are the same, and the handle of the window containing them matches,
/// we infer that the structures are referring to the same tool.
//
auto TToolInfo::operator==(const TToolInfo& ti) const -> bool
{
  return (uId == ti.uId && hwnd == ti.hwnd) ? true : false;
}
 
//
/// Sets the tip text of this tool to the given string resource.
//
void TToolInfo::SetText(int resId, HINSTANCE hinstance)
{
  lpszText = CONST_CAST(LPTSTR, MAKEINTRESOURCE(resId));
  hinst = hinstance;
}
 
//
/// Sets the bounding rectangle of the tool.
/// The coordinates are relative to the upper-left corner of the client area of the window.
///
/// \note This flag is only valid if the tool is a rectangle within the window and not a control
/// parented to the window.
//
void TToolInfo::SetRect(const TRect& rc)
{
  rect = rc;
}
 
//
/// Enables subclassing to perform automatic message handling for the tooltip.
/// Subclassing has to be enabled for dialog controls.
//
void TToolInfo::EnableSubclassing(bool enable)
{
  enable ? (uFlags |= TTF_SUBCLASS) : (uFlags &= ~TTF_SUBCLASS);
}
 
//
/// Overload; see GetToolRect(TRect&) const for more information.
//
auto TToolInfo::GetToolRect() const -> TRect
{
  auto r = TRect{};
  GetToolRect(r);
  return r;
}
 
//
/// Returns the cached text.
//
auto TToolInfo::GetCacheText() const -> LPCTSTR
{
  return CacheText.c_str();
}
 
//
/// Clears the text cache.
//
void TToolInfo::FlushCacheText()
{
  CacheText.clear();
}
 
//
/// Sets the `hwnd` and `uId` members of the `TOOLINFO` base.
///
/// The field `TOOLINFO::hwnd` is set to \p window, and the field `TOOLINFO::uId` is set to
/// \p toolId. The flag `TTF_IDISHWND` is cleared in the field `TOOLINFO::uFlags`.
//
void TToolInfo::SetToolInfo(HWND window, uint toolId)
{
  PRECONDITION(::IsWindow(window));
  hwnd = window;
  uFlags &= ~TTF_IDISHWND;
  uId = toolId;
}
 
//
/// Sets the `hwnd`, `uId` and `rect` members of the `TOOLINFO` base.
///
/// The field `TOOLINFO::hwnd` is set to \p window, the field `TOOLINFO::uId` is set to \p toolId,
/// and the field `TOOLINFO::rect` is set to \p rc. The flag `TTF_IDISHWND` is cleared in the field
/// `TOOLINFO::uFlags`.
//
void TToolInfo::SetToolInfo(HWND window, uint toolId, const TRect& rc)
{
  SetToolInfo(window, toolId);
  SetRect(rc);
}
 
//
/// Sets the `hwnd` and `uId` members of the `TOOLINFO` base.
///
/// The field `TOOLINFO::hwnd` is set to \p parent, and the field `TOOLINFO::uId` is set to
/// \p toolHwnd. The flag `TTF_IDISHWND` is set in the field `TOOLINFO::uFlags`.
//
void TToolInfo::SetToolInfo(HWND toolHwnd, HWND parent)
{
  PRECONDITION(::IsWindow(toolHwnd));
  PRECONDITION(::IsWindow(parent));
  hwnd = parent;
  uFlags |= TTF_IDISHWND;
  uId = reinterpret_cast<UINT_PTR>(toolHwnd);
}
 
//
/// Sets the text of this tool by providing a buffer that contains the string.
/// The boolean 'copy' flag specifies whether the method should make a local copy of the string.
//
void TToolInfo::SetText(LPCTSTR text, bool copy)
{
  if (text == LPSTR_TEXTCALLBACK || !copy || !text)
  {
    lpszText = const_cast<LPTSTR>(text);
    CacheText.clear();
  }
  else
  {
    CacheText = text;
    if (CacheText.empty()) // Ensure we're not empty.
      CacheText.resize(1);
    lpszText = &CacheText[0];
  }
}
 
//
/// Returns the actual HWND linked to a tool.
///
/// \returns For tools implemented as a rectangle within a client area, the window's handle is
/// returned. For tools associated with a control, the handle of the control is returned.
//
auto TToolInfo::GetToolWindow() const -> HWND
{
  return (uFlags & TTF_IDISHWND) ? HWND(uId) : hwnd;
}
 
//
/// Retrieves the actual RECT linked to a tool.
///
/// For tools implemented as a rectangle within a client area, that rectangle is retrieved. For tools
/// associated with a control, the latter's client area is retrieved.
//
void TToolInfo::GetToolRect(TRect& rc) const
{
  if (uFlags & TTF_IDISHWND)
  {
    CHECK(::IsWindow(HWND(uId)));
    ::GetClientRect(HWND(uId), &rc);
  }
  else
  {
    rc = rect;
  }
}
 
//
/// Determines whether the given point within the given window lies within this tool. 
///
/// For tools implemented as a rectangle within a window's client area, we simply check that the
/// given point is within that rectangle. For tools representing a child window, we check that the
/// point is within the client area of the child window.
///
/// \returns Returns `true` if the point is within this tool; otherwise `false`.
///
/// \note The given point must be specified in client coordinates relative to the given window.
//
auto TToolInfo::IsPointInTool(HWND w, const TPoint& pt) const -> bool
{
  HWND hTool = GetToolWindow();
  if ((hTool && w == hTool) || w == hwnd)
  {
    TRect rc;
    GetToolRect(rc);
    if (rc.Contains(pt))
      return true;
  }
  return false;
}
 
//#endif // OWL5_COMPAT
 
//-------------------------------------------------------------------------------------------------
// TTooltipText
//
 
//
/// Sets text of tooltip to specified buffer.
/// NOTE: The buffer pointed to by the specified parameter must be
/// valid for as long as the TTooltipText points to it.
/// For temporary buffers, use the 'CopyText' method instead.
//
void TTooltipText::SetText(LPCTSTR buff)
{
  lpszText = (LPTSTR) buff;
}
 
//
/// Sets the text of the tooltip. The text is copied into the
/// buffer owned by the 'TTooltipText'.
/// \note The internal buffer can only handle 80 characters including the null-terminator.
/// Longer strings will be truncated. Use SetText instead to avoid this.
//
void TTooltipText::CopyText(LPCTSTR text)
{
  PRECONDITION(text != nullptr);
  const size_t maxLen = COUNTOF(szText) - 1;
  WARNX(OwlCommCtrl, _tcslen(text) > maxLen, 0, _T("Tip text is too large (truncated)."));
  szText[0] = _T('\0');
  _tcsncat(szText, text, maxLen);
}
 
//
/// Sets the text of the tooltip. The 'resId' identifies a string resource
/// found in the module pointed to by the 'hInstance' parameter.
//
void TTooltipText::SetText(int resId, HINSTANCE hInstance)
{
  lpszText = CONST_CAST(LPTSTR, MAKEINTRESOURCE(resId));
  hinst = hInstance;
}
 
//-------------------------------------------------------------------------------------------------
// TTooltipEnabler
//
 
//
/// Constructs enabler object to be sent to a window so that the latter can provide
/// the text of the specified tool.
//
TTooltipEnabler::TTooltipEnabler(TTooltipText& tt, HWND hReceiver)
  : TCommandEnabler{(tt.uFlags & TTF_IDISHWND) ?
  ::GetDlgCtrlID(reinterpret_cast<HWND>(tt.hdr.idFrom)) :
  static_cast<uint>(tt.hdr.idFrom), hReceiver},
  TipText{tt}
{
  Flags |= NonSender;
}
 
//
/// Sets the text of the tool specified by the TTooltipEnabler object.
///
/// \note The text is copied to the internal buffer in the TTooltipText structure.
/// This buffer can only handle 80 characters including the null-terminator.
/// Longer strings will be truncated.
//
void TTooltipEnabler::SetText(LPCTSTR text)
{
  TCommandEnabler::Enable(true);// only to set Handled
  TipText.CopyText(text);
}
 
//
/// SetCheck does nothing but serve as a place-holder function.
//
void TTooltipEnabler::SetCheck([[maybe_unused]] int state)
{}
 
//-------------------------------------------------------------------------------------------------
// TTooltip
//
 
//
/// Creates a tooltip with the TTS_ALWAYSTIP style set as specified.
///
/// Parameter \p alwaysTip determines whether the tooltip will have the TTS_ALWAYSTIP style. If
/// `true` (the default) the tooltip will be active regardless of whether its owner is active or
/// inactive.
//
TTooltip::TTooltip(TWindow* parent, bool alwaysTip, TModule* module)
  : TControl{parent, 0, _T(""), 0, 0, 0, 0, module}
{
  InitializeCommonControls(ICC_BAR_CLASSES);
  SetStyle(WS_POPUP | WS_DISABLED | (alwaysTip ? TTS_ALWAYSTIP : 0));
 
  // Most tooltips do not need to be topmost. However, when the tool container window they are
  // servicing is reparented (for example, docking toolbars which are docked/undocked), the
  // tooltip's parent window may no longer be the tool container window. In that case, the tip may
  // show up behind the tool container window (unless, the tooltip has WS_EX_TOPMOST set).
  //
  ModifyExStyle(0, WS_EX_WINDOWEDGE | WS_EX_TOPMOST);
}
 
//
/// Creates a tooltip that is automatically created when the parent is.
///
/// This constructor allows `wfAutoCreate` to be passed as an argument, so that the tooltip is
/// automatically created when the parent is. No other flags are supported and will cause an
/// assertion in diagnostics builds. The tooltip is initialized with the default style
/// TTS_ALWAYSTIP, meaning that the tooltip will be active regardless of whether its parent is
/// active or inactive. You can override the tooltip styles using TWindow::ModifyStyle and
/// TWindow::ModifyExStyle.
//
TTooltip::TTooltip(TWindow* parent, TWindowFlag f, TModule* m)
  : TControl{parent, 0, _T(""), 0, 0, 0, 0, m}
{
  PRECONDITION(f == wfAutoCreate);
  InitializeCommonControls(ICC_BAR_CLASSES);
 
  // See comment about WS_EX_TOPMOST in the other constructor.
  //
  SetStyle(WS_POPUP | WS_DISABLED | TTS_ALWAYSTIP);
  ModifyExStyle(0, WS_EX_WINDOWEDGE | WS_EX_TOPMOST);
  SetFlag(f);
}
 
//
/// Creates an alias of an existing tooltip control.
///
/// This constructor is useful when used with controls that automatically create a tooltip (e.g.
/// Tab controls with TCS_TOOLTIPS style).
//
TTooltip::TTooltip(HWND tooltip, TModule* module)
  : TControl{tooltip, module}
{
  PRECONDITION(tooltip);
  InitializeCommonControls(ICC_BAR_CLASSES);
}
 
//
/// Retrieves the tip text associated with the specified tool.
///
/// \note If the tip text is larger than 79 characters (the standard tip text limit), then repeated
/// calls to GetText(int bufSize, TTTOOLINFO&) will be made with increasing buffer size, until the
/// whole text has been retrieved untruncated.
///
/// \sa GetText(int bufSize, TTTOOLINFO&) is called to perform the text retrieval.
//
auto TTooltip::GetText(TToolId tool) const -> tstring
{
  auto ti = InitToolInfo(tool);
  for (auto bufSize = 80;; bufSize *= 2)
  {
    auto buf = tstring(static_cast<size_t>(bufSize - 1), _T('\0')); // The null-terminator is additional.
    ti.lpszText = const_cast<LPTSTR>(buf.data());
    GetText(bufSize, ti);
    const auto n = static_cast<decltype(bufSize)>(_tcslen(ti.lpszText));
    CHECK(n < bufSize);
    if (n == bufSize - 1) // Possibly truncated?
    {
      if (bufSize > std::numeric_limits<decltype(bufSize)>::max() / 2)
        throw TXTooltip{_T("TTooltip::GetText: Text too large")};
    }
    else
      return ti.lpszText;
  }
}
 
//
/// Retrieves the registered tool information for the given tool ID, if any.
///
/// \returns If successful, the tool is returned; otherwise if the underlying retrieval function
/// (see below) should throw a TXTooltip exception, typically due to the non-existence of a tool
/// with the given ID, then the exception is caught and `std::nullopt` is returned. Note that other
/// exceptions are not handled, so do not rely on `std::nullopt` for general error handling. Be
/// prepared for other exceptions to be thrown.
///
/// \note The returned TTool will only hold the first 79 characters of the tip text (plus the
/// null-terminator), if the text is any longer than that. This truncation is a limitation of the
/// Windows API. Use GetText to retrieve longer text.
///
/// \sa GetText can be used to retrieve long tip texts.
/// \sa GetToolInfo is used to perform the information retrieval for TCtrlToolId and TRectToolId.
/// \sa EnumTools is used to perform the information retrieval for TToolIndex.
/// \sa <a href="https://docs.microsoft.com/en-gb/windows/win32/controls/ttm-enumtools">
/// TTM_ENUMTOOLS</a> documentation, section Parameters | lParam, for the text size limitation.
//
auto TTooltip::GetTool(TToolId tool) const -> std::optional<TTool>
{
  try
  {
    const auto getToolInfo = [&](TToolId tool)
    {
      auto buf = tstring(static_cast<size_t>(79), _T('\0')); // 80 character limitation, including null-terminator.
      auto ti = InitToolInfo(tool, false);
      ti.lpszText = const_cast<LPTSTR>(buf.data());
      GetToolInfo(ti);
      return TTool{ti};
    };
    const auto enumTools = [&](TToolIndex i)
    {
      auto buf = tstring(static_cast<size_t>(79), _T('\0')); // 80 character limitation, including null-terminator.
      auto ti = InitToolInfo(i, false);
      ti.lpszText = const_cast<LPTSTR>(buf.data());
      EnumTools(i, ti);
      return TTool{ti};
    };
    return std::make_optional<TTool>(std::visit(TOverloaded{getToolInfo, enumTools}, tool));
  }
  catch ([[maybe_unused]] const TXTooltip& x)
  {
    WARNX(OwlCommCtrl, true, 1, _T("TTooltip::GetTool: TXTooltip exception: ") << x.why());
    return std::nullopt;
  }
}
 
//
/// Returns TOOLTIPS_CLASS, the window class name of the Windows API tooltip control.
///
/// \sa <a href="https://docs.microsoft.com/en-us/windows/win32/controls/common-control-window-classes">
/// Window Classes</a> provided by the common control library in the Windows API.
//
auto TTooltip::GetWindowClassName() -> TWindowClassName
{
  return TWindowClassName{TOOLTIPS_CLASS};
}
 
} // OWL namespace

V1004 The 'text' pointer was used unsafely after it was verified against nullptr. Check lines: 394, 396.

V813 Decreased performance. The 'tool' argument should probably be rendered as a constant reference.